mirror of
https://notabug.org/SuperSaltyGamer/ame
synced 2026-01-15 16:42:54 -03:00
Refactor everything
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 120,
|
|
||||||
"useTabs": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
|
||||||
44
README.md
44
README.md
@@ -2,15 +2,53 @@
|
|||||||
|
|
||||||
Various userscripts for the music hoarding community.
|
Various userscripts for the music hoarding community.
|
||||||
|
|
||||||
### Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Install [ViolentMonkey](https://violentmonkey.github.io) or [TamperMonkey](https://tampermonkey.net) then proceed to installing any of the following userscripts:
|
Install [ViolentMonkey](https://violentmonkey.github.io) or [TamperMonkey](https://tampermonkey.net) then proceed to installing any of the editions:
|
||||||
|
|
||||||
* [Apple Music](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/applemusic.user.js)
|
* [Apple Music](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/applemusic.user.js)
|
||||||
* [MusicBrainz](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/musicbrainz.user.js)
|
* [MusicBrainz](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/musicbrainz.user.js)
|
||||||
* [VGMdb](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/vgmdb.user.js)
|
* [VGMdb](https://notabug.org/SuperSaltyGamer/ame/raw/main/dist/vgmdb.user.js)
|
||||||
|
|
||||||
### References
|
## Editions
|
||||||
|
|
||||||
|
Feature rundown for all editions of Ame.
|
||||||
|
|
||||||
|
### Ame (Apple Music)
|
||||||
|
|
||||||
|
* Selectable release title, artist, description.
|
||||||
|
* Check release storefront availability.
|
||||||
|
* Link to full resolution release cover.
|
||||||
|
* Hide upselling modals and banners.
|
||||||
|
* Extended release info panel.
|
||||||
|
* Copy authorization token.
|
||||||
|
* Release quality badges.
|
||||||
|
* Check track quality.
|
||||||
|
* Lyrics downloading.
|
||||||
|
* Search MH Covers.
|
||||||
|
|
||||||
|
### Ame (MusicBrainz)
|
||||||
|
|
||||||
|
* Add covers directly from MH Covers.
|
||||||
|
* Batch download release scans.
|
||||||
|
* Enhanced search box:
|
||||||
|
* Automatic focus on page load.
|
||||||
|
* Search directly by ISRC, Catalog Number, Barcode.
|
||||||
|
* Search directly by EAC/XLD rip log.
|
||||||
|
* Attach TOC from rip log on release page.
|
||||||
|
* Related links:
|
||||||
|
* Ongaku no Mori link on release page.
|
||||||
|
* MH Covers link on release page.
|
||||||
|
|
||||||
|
### Ame (VGMdb)
|
||||||
|
|
||||||
|
* Seed release to MusicBrainz.
|
||||||
|
* Batch download album scans.
|
||||||
|
* Related links:
|
||||||
|
* Ongaku no Mori link on release page.
|
||||||
|
* MusicBrainz link on release page.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
Inspired by and/or used for reference:
|
Inspired by and/or used for reference:
|
||||||
|
|
||||||
|
|||||||
468
dist/applemusic.user.js
vendored
468
dist/applemusic.user.js
vendored
File diff suppressed because one or more lines are too long
32
dist/musicbrainz.user.js
vendored
32
dist/musicbrainz.user.js
vendored
File diff suppressed because one or more lines are too long
22
dist/vgmdb.user.js
vendored
22
dist/vgmdb.user.js
vendored
File diff suppressed because one or more lines are too long
226
package-lock.json
generated
226
package-lock.json
generated
@@ -10,12 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handsontable": "^13.0.0",
|
"handsontable": "^13.0.0",
|
||||||
"jszip": "^3.9.1",
|
"jszip": "^3.9.1",
|
||||||
"lighterhtml": "^4.2.0",
|
|
||||||
"path-to-regexp": "^6.2.1",
|
|
||||||
"xml-formatter": "^3.2.0"
|
"xml-formatter": "^3.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/tampermonkey": "^4.0.5",
|
"@types/tampermonkey": "^4.0.10",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^4.4.7"
|
"vite": "^4.4.7"
|
||||||
}
|
}
|
||||||
@@ -389,31 +387,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/tampermonkey": {
|
"node_modules/@types/tampermonkey": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tampermonkey/-/tampermonkey-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tampermonkey/-/tampermonkey-4.0.10.tgz",
|
||||||
"integrity": "sha512-FGPo7d+qZkDF7vyrwY1WNhcUnfDyVpt2uyL7krAu3WKCUMCfIUzOuvt8aSk8N2axHT8XPr9stAEDGVHLvag6Pw==",
|
"integrity": "sha512-E3SYtXgeXG/nnq6uAPiZh7i0XA8jLbtiXraxxHTnsSjzQcQMxWBzbcoGkHgiC+zbHXxxkynUT9zt85SpF8loNw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@ungap/create-content": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/create-content/-/create-content-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-CvmX0Mr5PfFARDBbSef0B+SAqSeMKaHOG/twJi9nbPtp/MiNPgyBLqZndiyO3RXQ0RXy6TqwarvB6KWzTmc4MQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@ungap/import-node": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/import-node/-/import-node-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-VuWVBAMRjoOc63n8Cc19brS7KlhYJ+57790LF+lVw60nMRemCrz1T6HnoNx74IEW3FS+TM+vveJ70C6NyTKODQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@ungap/trim": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/trim/-/trim-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-CfsUxeZ2R/O3EGCOe+IkAU32yHOdO+mCRmtavSIQ4HZN3Jiq/ynGzq8/asyamd28U26UJmpSV/TC7+p7qELKrg=="
|
|
||||||
},
|
|
||||||
"node_modules/@ungap/weakmap": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/weakmap/-/weakmap-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-GmVAWB+JuFKqSbzlofYK4qxk955gEv4Kd9/aj2hLOxneXMAm/J7OXcl5DlElS9tmkqwCcxGysSZGOrjzNvmjFQ=="
|
|
||||||
},
|
|
||||||
"node_modules/bignumber.js": {
|
"node_modules/bignumber.js": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz",
|
||||||
@@ -446,38 +424,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
},
|
},
|
||||||
"node_modules/domconstants": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/domconstants/-/domconstants-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-sPOoOckTxtwy5t8PFf6zl11gOEhOpl1k0ZCc/NfCNmHoMw8n9HnCQCzxWKX9gdBp+qM+2DTFkst++Yw6C41izQ=="
|
|
||||||
},
|
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "2.4.7",
|
"version": "2.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
|
||||||
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ=="
|
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ=="
|
||||||
},
|
},
|
||||||
"node_modules/domsanitizer": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/domsanitizer/-/domsanitizer-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-qglHc+5k5C2+WSEzck0WGbpa2exCKZuZKb0n4RU2Wuy7BPkpmf67mHD1BJWGxs0iJYl709f2YVeJMh06c1ILlA==",
|
|
||||||
"dependencies": {
|
|
||||||
"domconstants": "^0.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/domtagger": {
|
|
||||||
"version": "0.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/domtagger/-/domtagger-0.7.2.tgz",
|
|
||||||
"integrity": "sha512-h7g5eduvnLwowJJPkcB5lNzo8vd/Hx4e3I4IOtLpX0qB2wBiuryGLNa61MeFre4b6gMaQIhegMIZ2I8rQCAJwQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@ungap/create-content": "^0.2.0",
|
|
||||||
"@ungap/import-node": "^0.2.0",
|
|
||||||
"@ungap/trim": "^0.2.0",
|
|
||||||
"@ungap/weakmap": "^0.2.1",
|
|
||||||
"domconstants": "^0.1.2",
|
|
||||||
"domsanitizer": "^0.2.2",
|
|
||||||
"umap": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.18.17",
|
"version": "0.18.17",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
||||||
@@ -556,11 +507,6 @@
|
|||||||
"unorm": "^1.6.0"
|
"unorm": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hyperhtml-style": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/hyperhtml-style/-/hyperhtml-style-0.1.3.tgz",
|
|
||||||
"integrity": "sha512-IvLy8MzHTSJ0fDpSzrb8rcdnla6yROEmNBSxInEMyIFu2DQkbmpadTf6B4fHvnytN6iHL2gGwpe5/jHL3wMi+A=="
|
|
||||||
},
|
|
||||||
"node_modules/immediate": {
|
"node_modules/immediate": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
@@ -595,30 +541,6 @@
|
|||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lighterhtml": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lighterhtml/-/lighterhtml-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-HAb+Ri17iT+vYmFtarlt45O63BltoJY/ltDZGhnf2A1s4kno2j6su5KiZAgYD4/5AjODYQqflSy9KJZFwL5VwQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"@ungap/create-content": "^0.2.0",
|
|
||||||
"@ungap/weakmap": "^0.2.1",
|
|
||||||
"domsanitizer": "^0.2.3",
|
|
||||||
"domtagger": "^0.7.1",
|
|
||||||
"hyperhtml-style": "^0.1.2",
|
|
||||||
"udomdiff": "^1.1.0",
|
|
||||||
"uhandlers": "^0.4.2",
|
|
||||||
"umap": "^1.0.2",
|
|
||||||
"uwire": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lighterhtml/node_modules/uhandlers": {
|
|
||||||
"version": "0.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uhandlers/-/uhandlers-0.4.2.tgz",
|
|
||||||
"integrity": "sha512-4M3yo0saEReMHiUz3yTDX/e9Z1Z+X8fVvDEywdjvWHPKqzpI6xEPJ21llrBf6Tvf7E5xaPCf9l5JXaYZRU7QRA==",
|
|
||||||
"dependencies": {
|
|
||||||
"uarray": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
@@ -661,11 +583,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@@ -795,21 +712,6 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uarray": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uarray/-/uarray-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-LHmiAd5QuAv7pU2vbh+Zq9YOnqVK0H764p2Ozinpfy9ka58OID4IsGLiXsitqH7n0NAIDxvax1A/kDXpii/Ckg=="
|
|
||||||
},
|
|
||||||
"node_modules/udomdiff": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA=="
|
|
||||||
},
|
|
||||||
"node_modules/umap": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/umap/-/umap-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-bW127HgG4H4VAD6qlqO5vCC+7bnlYvZ6A6BdwyGblkWvlEG7VYpj1bcpf3iJpvyKmkPZWDIeZDmoULz67ec7NA=="
|
|
||||||
},
|
|
||||||
"node_modules/unorm": {
|
"node_modules/unorm": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
||||||
@@ -824,14 +726,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
},
|
},
|
||||||
"node_modules/uwire": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uwire/-/uwire-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-XJPmJnySabt8D0/wnfFFywgUOBnXszDEW32nEVIfOx1n6gLTZSp+X+70+blSnHKNiIUVSFPmmRuxOal0I/aB5g==",
|
|
||||||
"dependencies": {
|
|
||||||
"uarray": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "4.4.7",
|
"version": "4.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
||||||
@@ -1079,31 +973,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/tampermonkey": {
|
"@types/tampermonkey": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tampermonkey/-/tampermonkey-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tampermonkey/-/tampermonkey-4.0.10.tgz",
|
||||||
"integrity": "sha512-FGPo7d+qZkDF7vyrwY1WNhcUnfDyVpt2uyL7krAu3WKCUMCfIUzOuvt8aSk8N2axHT8XPr9stAEDGVHLvag6Pw==",
|
"integrity": "sha512-E3SYtXgeXG/nnq6uAPiZh7i0XA8jLbtiXraxxHTnsSjzQcQMxWBzbcoGkHgiC+zbHXxxkynUT9zt85SpF8loNw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@ungap/create-content": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/create-content/-/create-content-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-CvmX0Mr5PfFARDBbSef0B+SAqSeMKaHOG/twJi9nbPtp/MiNPgyBLqZndiyO3RXQ0RXy6TqwarvB6KWzTmc4MQ=="
|
|
||||||
},
|
|
||||||
"@ungap/import-node": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/import-node/-/import-node-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-VuWVBAMRjoOc63n8Cc19brS7KlhYJ+57790LF+lVw60nMRemCrz1T6HnoNx74IEW3FS+TM+vveJ70C6NyTKODQ=="
|
|
||||||
},
|
|
||||||
"@ungap/trim": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/trim/-/trim-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-CfsUxeZ2R/O3EGCOe+IkAU32yHOdO+mCRmtavSIQ4HZN3Jiq/ynGzq8/asyamd28U26UJmpSV/TC7+p7qELKrg=="
|
|
||||||
},
|
|
||||||
"@ungap/weakmap": {
|
|
||||||
"version": "0.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/weakmap/-/weakmap-0.2.1.tgz",
|
|
||||||
"integrity": "sha512-GmVAWB+JuFKqSbzlofYK4qxk955gEv4Kd9/aj2hLOxneXMAm/J7OXcl5DlElS9tmkqwCcxGysSZGOrjzNvmjFQ=="
|
|
||||||
},
|
|
||||||
"bignumber.js": {
|
"bignumber.js": {
|
||||||
"version": "8.1.1",
|
"version": "8.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-8.1.1.tgz",
|
||||||
@@ -1128,38 +1002,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
|
||||||
},
|
},
|
||||||
"domconstants": {
|
|
||||||
"version": "0.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/domconstants/-/domconstants-0.1.2.tgz",
|
|
||||||
"integrity": "sha512-sPOoOckTxtwy5t8PFf6zl11gOEhOpl1k0ZCc/NfCNmHoMw8n9HnCQCzxWKX9gdBp+qM+2DTFkst++Yw6C41izQ=="
|
|
||||||
},
|
|
||||||
"dompurify": {
|
"dompurify": {
|
||||||
"version": "2.4.7",
|
"version": "2.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz",
|
||||||
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ=="
|
"integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ=="
|
||||||
},
|
},
|
||||||
"domsanitizer": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/domsanitizer/-/domsanitizer-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-qglHc+5k5C2+WSEzck0WGbpa2exCKZuZKb0n4RU2Wuy7BPkpmf67mHD1BJWGxs0iJYl709f2YVeJMh06c1ILlA==",
|
|
||||||
"requires": {
|
|
||||||
"domconstants": "^0.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"domtagger": {
|
|
||||||
"version": "0.7.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/domtagger/-/domtagger-0.7.2.tgz",
|
|
||||||
"integrity": "sha512-h7g5eduvnLwowJJPkcB5lNzo8vd/Hx4e3I4IOtLpX0qB2wBiuryGLNa61MeFre4b6gMaQIhegMIZ2I8rQCAJwQ==",
|
|
||||||
"requires": {
|
|
||||||
"@ungap/create-content": "^0.2.0",
|
|
||||||
"@ungap/import-node": "^0.2.0",
|
|
||||||
"@ungap/trim": "^0.2.0",
|
|
||||||
"@ungap/weakmap": "^0.2.1",
|
|
||||||
"domconstants": "^0.1.2",
|
|
||||||
"domsanitizer": "^0.2.2",
|
|
||||||
"umap": "^1.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"esbuild": {
|
"esbuild": {
|
||||||
"version": "0.18.17",
|
"version": "0.18.17",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.17.tgz",
|
||||||
@@ -1222,11 +1069,6 @@
|
|||||||
"unorm": "^1.6.0"
|
"unorm": "^1.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hyperhtml-style": {
|
|
||||||
"version": "0.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/hyperhtml-style/-/hyperhtml-style-0.1.3.tgz",
|
|
||||||
"integrity": "sha512-IvLy8MzHTSJ0fDpSzrb8rcdnla6yROEmNBSxInEMyIFu2DQkbmpadTf6B4fHvnytN6iHL2gGwpe5/jHL3wMi+A=="
|
|
||||||
},
|
|
||||||
"immediate": {
|
"immediate": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
@@ -1261,32 +1103,6 @@
|
|||||||
"immediate": "~3.0.5"
|
"immediate": "~3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lighterhtml": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lighterhtml/-/lighterhtml-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-HAb+Ri17iT+vYmFtarlt45O63BltoJY/ltDZGhnf2A1s4kno2j6su5KiZAgYD4/5AjODYQqflSy9KJZFwL5VwQ==",
|
|
||||||
"requires": {
|
|
||||||
"@ungap/create-content": "^0.2.0",
|
|
||||||
"@ungap/weakmap": "^0.2.1",
|
|
||||||
"domsanitizer": "^0.2.3",
|
|
||||||
"domtagger": "^0.7.1",
|
|
||||||
"hyperhtml-style": "^0.1.2",
|
|
||||||
"udomdiff": "^1.1.0",
|
|
||||||
"uhandlers": "^0.4.2",
|
|
||||||
"umap": "^1.0.2",
|
|
||||||
"uwire": "^1.1.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"uhandlers": {
|
|
||||||
"version": "0.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/uhandlers/-/uhandlers-0.4.2.tgz",
|
|
||||||
"integrity": "sha512-4M3yo0saEReMHiUz3yTDX/e9Z1Z+X8fVvDEywdjvWHPKqzpI6xEPJ21llrBf6Tvf7E5xaPCf9l5JXaYZRU7QRA==",
|
|
||||||
"requires": {
|
|
||||||
"uarray": "^1.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"moment": {
|
"moment": {
|
||||||
"version": "2.29.4",
|
"version": "2.29.4",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||||
@@ -1311,11 +1127,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||||
},
|
},
|
||||||
"path-to-regexp": {
|
|
||||||
"version": "6.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
|
||||||
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
|
||||||
},
|
|
||||||
"picocolors": {
|
"picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
@@ -1408,21 +1219,6 @@
|
|||||||
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"uarray": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uarray/-/uarray-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-LHmiAd5QuAv7pU2vbh+Zq9YOnqVK0H764p2Ozinpfy9ka58OID4IsGLiXsitqH7n0NAIDxvax1A/kDXpii/Ckg=="
|
|
||||||
},
|
|
||||||
"udomdiff": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/udomdiff/-/udomdiff-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-aqjTs5x/wsShZBkVagdafJkP8S3UMGhkHKszsu1cszjjZ7iOp86+Qb3QOFYh01oWjPMy5ZTuxD6hw5uTKxd+VA=="
|
|
||||||
},
|
|
||||||
"umap": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/umap/-/umap-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-bW127HgG4H4VAD6qlqO5vCC+7bnlYvZ6A6BdwyGblkWvlEG7VYpj1bcpf3iJpvyKmkPZWDIeZDmoULz67ec7NA=="
|
|
||||||
},
|
|
||||||
"unorm": {
|
"unorm": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
|
||||||
@@ -1434,14 +1230,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||||
},
|
},
|
||||||
"uwire": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/uwire/-/uwire-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-XJPmJnySabt8D0/wnfFFywgUOBnXszDEW32nEVIfOx1n6gLTZSp+X+70+blSnHKNiIUVSFPmmRuxOal0I/aB5g==",
|
|
||||||
"requires": {
|
|
||||||
"uarray": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vite": {
|
"vite": {
|
||||||
"version": "4.4.7",
|
"version": "4.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.7.tgz",
|
||||||
|
|||||||
@@ -10,12 +10,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handsontable": "^13.0.0",
|
"handsontable": "^13.0.0",
|
||||||
"jszip": "^3.9.1",
|
"jszip": "^3.9.1",
|
||||||
"lighterhtml": "^4.2.0",
|
|
||||||
"path-to-regexp": "^6.2.1",
|
|
||||||
"xml-formatter": "^3.2.0"
|
"xml-formatter": "^3.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/tampermonkey": "^4.0.5",
|
"@types/tampermonkey": "^4.0.10",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"vite": "^4.4.7"
|
"vite": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { basename, dirname } from "path";
|
import { join, basename, dirname } from "path";
|
||||||
import { LibraryFormats, Plugin, ResolvedConfig } from "vite";
|
import { LibraryFormats, Plugin, ResolvedConfig } from "vite";
|
||||||
|
|
||||||
export interface UserScriptOptions {
|
export interface UserScriptOptions {
|
||||||
@@ -56,10 +56,9 @@ export function UserScriptPlugin(options: UserScriptOptions): Plugin {
|
|||||||
if (config.mode === "production") {
|
if (config.mode === "production") {
|
||||||
if (options.cdn) url = `${options.cdn}${chunk.name}.user.js`;
|
if (options.cdn) url = `${options.cdn}${chunk.name}.user.js`;
|
||||||
} else {
|
} else {
|
||||||
if (options.port) url = `http://localhost:${options.port}/${chunk.name}.user.js`;
|
if (options.port) url = `http://localhost:${join(options.port.toString(), config.build.outDir, chunk.name).replaceAll("\\", "/")}.user.js`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
if (url) {
|
if (url) {
|
||||||
header = header.replaceAll(
|
header = header.replaceAll(
|
||||||
`// ==/UserScript==`,
|
`// ==/UserScript==`,
|
||||||
|
|||||||
@@ -1,9 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78 20" height="32">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 78 20" height="32">
|
||||||
<defs>
|
<path d="M43.15 3.42a1.294 1.294 0 0 0-1.119.665 1.29 1.29 0 0 0-.161.655h2.5a1.206 1.206 0 0 0-.577-1.144 1.205 1.205 0 0 0-.643-.176ZM25 2.27h-.08l-1 2.79h2Zm11.1 1.2c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.34 1.71.83 0 1.33-.64 1.33-1.71 0-1.07-.55-1.71-1.38-1.71Zm-5.34 0c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.33 1.71.82 0 1.25-.64 1.25-1.71 0-1.07-.46-1.71-1.29-1.71Zm-5.29-2.36 2.38 6.53h-1.09l-.6-1.77h-2.51l-.6 1.77H22l2.37-6.53ZM40.12.8v6.84h-1V.8Zm3 1.83a2.172 2.172 0 0 1 2.21 2.48v.33h-3.5a1.327 1.327 0 0 0 .617 1.262c.211.131.455.199.703.198a1.183 1.183 0 0 0 1.18-.62h.94a1.999 1.999 0 0 1-2.15 1.41 2.245 2.245 0 0 1-2.093-1.276 2.238 2.238 0 0 1-.197-1.264 2.282 2.282 0 0 1 2.33-2.52ZM31 2.64c1.26 0 2.06 1 2.06 2.54S32.24 7.72 31 7.72a1.617 1.617 0 0 1-1.53-.82h-.07v2.37h-1V2.73h.94v.81h.07a1.738 1.738 0 0 1 1.59-.9Zm5.34 0c1.27 0 2.06 1 2.06 2.54s-.79 2.54-2.05 2.54a1.61 1.61 0 0 1-1.52-.82h-.08v2.37h-1V2.73h.94v.81h.07a1.753 1.753 0 0 1 1.56-.9Zm.26 8.27a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm-7.3 0a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm43 2.8A1.278 1.278 0 0 0 71 15h2.5a1.203 1.203 0 0 0-.587-1.119 1.214 1.214 0 0 0-.633-.171Zm-39.46 0c-.85 0-1.32.64-1.32 1.64 0 1 .47 1.65 1.32 1.65.85 0 1.34-.64 1.34-1.65 0-1.01-.53-1.59-1.36-1.59Zm-8-1.49h-1.31V17h1.24c1.37 0 2.17-.87 2.17-2.39a2.099 2.099 0 0 0-.2-1.177 2.096 2.096 0 0 0-1.97-1.163Zm36 3.41-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72a1.17 1.17 0 0 0 1.28-1.11ZM44 15.68l-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72A1.168 1.168 0 0 0 44 16.11ZM24.88 11.4a2.894 2.894 0 0 1 2.83 1.572c.261.515.362 1.096.29 1.668 0 2.08-1.13 3.28-3.09 3.28h-2.4V11.4Zm4.89 1.6v4.91h-1V13Zm21.58-1.6 2.09 5.1h.08l2.1-5.1h1.19v6.52h-1v-4.78h-.01l-1.93 4.67h-.77l-1.93-4.67h-.06v4.78h-1V11.4ZM47 11.09v6.83h-1v-6.83ZM37.07 13v4.91h-1V13Zm40.49-.08c.149.015.296.041.44.08v.92a2.427 2.427 0 0 0-.55-.06 1.174 1.174 0 0 0-1.079.541 1.165 1.165 0 0 0-.181.599v2.93h-1V13h.94v.75h.07a1.34 1.34 0 0 1 1.36-.82Zm-38.19-1.15V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm29.17 0V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm-8.73 1.15c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69h-.08a1.678 1.678 0 0 1-1.49.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47l1.34-.07v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-.94c.05-.83.85-1.37 1.97-1.37Zm-16.75 0c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69H44a1.682 1.682 0 0 1-1.5.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47L44 15v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-1c.14-.83.94-1.37 2.06-1.37Zm21.44 0c1.12 0 1.84.51 1.94 1.36h-.94c-.09-.36-.44-.6-1-.6s-1 .26-1 .66.25.49.79.62l.82.19c.94.22 1.39.62 1.39 1.34 0 .91-.87 1.53-2.06 1.53s-2-.53-2-1.39h1c.082.212.233.39.429.505.197.114.426.158.651.125.61 0 1-.28 1-.68s-.23-.51-.73-.63l-.87-.2c-.94-.22-1.38-.63-1.38-1.36 0-.73.84-1.47 1.96-1.47Zm7.79 0a2.158 2.158 0 0 1 2.21 2.47v.34H71a1.326 1.326 0 0 0 .62 1.253c.21.13.453.198.7.197a1.174 1.174 0 0 0 1.18-.61h.94A2 2 0 0 1 72.29 18a2.237 2.237 0 0 1-2.085-1.265A2.245 2.245 0 0 1 70 15.48a2.29 2.29 0 0 1 .205-1.263 2.28 2.28 0 0 1 2.085-1.297Zm-39.74 0a1.73 1.73 0 0 1 1.56.86h.07V13h.94v5c0 1.12-.89 1.83-2.29 1.83-1.24 0-2-.55-2.15-1.37h1c.08.37.49.6 1.15.6.82 0 1.3-.4 1.3-1.06v-1h-.07a1.665 1.665 0 0 1-.647.632 1.667 1.667 0 0 1-.883.198c-1.27 0-2.05-1-2.05-2.45 0-1.45.8-2.45 2.07-2.45ZM0 0" />
|
||||||
<clipPath id="a">
|
|
||||||
<path d="M43.15 3.42a1.294 1.294 0 0 0-1.119.665 1.29 1.29 0 0 0-.161.655h2.5a1.206 1.206 0 0 0-.577-1.144 1.205 1.205 0 0 0-.643-.176ZM25 2.27h-.08l-1 2.79h2Zm11.1 1.2c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.34 1.71.83 0 1.33-.64 1.33-1.71 0-1.07-.55-1.71-1.38-1.71Zm-5.34 0c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.33 1.71.82 0 1.25-.64 1.25-1.71 0-1.07-.46-1.71-1.29-1.71Zm-5.29-2.36 2.38 6.53h-1.09l-.6-1.77h-2.51l-.6 1.77H22l2.37-6.53ZM40.12.8v6.84h-1V.8Zm3 1.83a2.172 2.172 0 0 1 2.21 2.48v.33h-3.5a1.327 1.327 0 0 0 .617 1.262c.211.131.455.199.703.198a1.183 1.183 0 0 0 1.18-.62h.94a1.999 1.999 0 0 1-2.15 1.41 2.245 2.245 0 0 1-2.093-1.276 2.238 2.238 0 0 1-.197-1.264 2.282 2.282 0 0 1 2.33-2.52ZM31 2.64c1.26 0 2.06 1 2.06 2.54S32.24 7.72 31 7.72a1.617 1.617 0 0 1-1.53-.82h-.07v2.37h-1V2.73h.94v.81h.07a1.738 1.738 0 0 1 1.59-.9Zm5.34 0c1.27 0 2.06 1 2.06 2.54s-.79 2.54-2.05 2.54a1.61 1.61 0 0 1-1.52-.82h-.08v2.37h-1V2.73h.94v.81h.07a1.753 1.753 0 0 1 1.56-.9Zm.26 8.27a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm-7.3 0a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm43 2.8A1.278 1.278 0 0 0 71 15h2.5a1.203 1.203 0 0 0-.587-1.119 1.214 1.214 0 0 0-.633-.171Zm-39.46 0c-.85 0-1.32.64-1.32 1.64 0 1 .47 1.65 1.32 1.65.85 0 1.34-.64 1.34-1.65 0-1.01-.53-1.59-1.36-1.59Zm-8-1.49h-1.31V17h1.24c1.37 0 2.17-.87 2.17-2.39a2.099 2.099 0 0 0-.2-1.177 2.096 2.096 0 0 0-1.97-1.163Zm36 3.41-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72a1.17 1.17 0 0 0 1.28-1.11ZM44 15.68l-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72A1.168 1.168 0 0 0 44 16.11ZM24.88 11.4a2.894 2.894 0 0 1 2.83 1.572c.261.515.362 1.096.29 1.668 0 2.08-1.13 3.28-3.09 3.28h-2.4V11.4Zm4.89 1.6v4.91h-1V13Zm21.58-1.6 2.09 5.1h.08l2.1-5.1h1.19v6.52h-1v-4.78h-.01l-1.93 4.67h-.77l-1.93-4.67h-.06v4.78h-1V11.4ZM47 11.09v6.83h-1v-6.83ZM37.07 13v4.91h-1V13Zm40.49-.08c.149.015.296.041.44.08v.92a2.427 2.427 0 0 0-.55-.06 1.174 1.174 0 0 0-1.079.541 1.165 1.165 0 0 0-.181.599v2.93h-1V13h.94v.75h.07a1.34 1.34 0 0 1 1.36-.82Zm-38.19-1.15V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm29.17 0V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm-8.73 1.15c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69h-.08a1.678 1.678 0 0 1-1.49.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47l1.34-.07v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-.94c.05-.83.85-1.37 1.97-1.37Zm-16.75 0c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69H44a1.682 1.682 0 0 1-1.5.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47L44 15v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-1c.14-.83.94-1.37 2.06-1.37Zm21.44 0c1.12 0 1.84.51 1.94 1.36h-.94c-.09-.36-.44-.6-1-.6s-1 .26-1 .66.25.49.79.62l.82.19c.94.22 1.39.62 1.39 1.34 0 .91-.87 1.53-2.06 1.53s-2-.53-2-1.39h1c.082.212.233.39.429.505.197.114.426.158.651.125.61 0 1-.28 1-.68s-.23-.51-.73-.63l-.87-.2c-.94-.22-1.38-.63-1.38-1.36 0-.73.84-1.47 1.96-1.47Zm7.79 0a2.158 2.158 0 0 1 2.21 2.47v.34H71a1.326 1.326 0 0 0 .62 1.253c.21.13.453.198.7.197a1.174 1.174 0 0 0 1.18-.61h.94A2 2 0 0 1 72.29 18a2.237 2.237 0 0 1-2.085-1.265A2.245 2.245 0 0 1 70 15.48a2.29 2.29 0 0 1 .205-1.263 2.28 2.28 0 0 1 2.085-1.297Zm-39.74 0a1.73 1.73 0 0 1 1.56.86h.07V13h.94v5c0 1.12-.89 1.83-2.29 1.83-1.24 0-2-.55-2.15-1.37h1c.08.37.49.6 1.15.6.82 0 1.3-.4 1.3-1.06v-1h-.07a1.665 1.665 0 0 1-.647.632 1.667 1.667 0 0 1-.883.198c-1.27 0-2.05-1-2.05-2.45 0-1.45.8-2.45 2.07-2.45ZM0 0"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
<path d="M17.48 1.22A2.78 2.78 0 0 1 19 3.88v11.25a2.722 2.722 0 0 1-1.34 2.59 2.204 2.204 0 0 1-1.1.28 4.2 4.2 0 0 1-2.68-1.24l-4.24-3.38a.81.81 0 0 1-.295-.757.805.805 0 0 1 .175-.383.84.84 0 0 1 1.16-.12l4.25 3.4c.81.69 1.52 1 1.9.77.14-.08.5-.28.5-1.18V3.88a1.229 1.229 0 0 0-.56-1.19c-.79-.36-1.72.63-2.08 1.07-.55.59-2.34 2.94-4.18 5.39l-.62.85-1.58 2.11c-1.52 2-2.84 3.81-3.31 4.38-1.23 1.7-2.47 1.66-3.29 1.33A2.998 2.998 0 0 1 0 14.76V9.59a2.785 2.785 0 0 1 1.45-2.78 3.084 3.084 0 0 1 3.42.83L6.47 9a.805.805 0 0 1 .28.761.795.795 0 0 1-.18.379.832.832 0 0 1-1.16.09L3.8 8.87c-1-.9-1.43-.71-1.62-.62-.19.09-.52.26-.53 1.35v5.16c0 .48.09 1.32.68 1.56.17.07.6.24 1.34-.78C4.13 15 5.45 13.2 7 11.15L7.85 10l.65-.87c2.16-2.89 4.25-5.65 4.94-6.4C15.26.54 16.87 1 17.48 1.22ZM43.15 3.42a1.294 1.294 0 0 0-1.119.665 1.29 1.29 0 0 0-.161.655h2.5a1.206 1.206 0 0 0-.577-1.144 1.205 1.205 0 0 0-.643-.176ZM25 2.27h-.08l-1 2.79h2Zm11.1 1.2c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.34 1.71.83 0 1.33-.64 1.33-1.71 0-1.07-.55-1.71-1.38-1.71Zm-5.34 0c-.83 0-1.34.66-1.34 1.71 0 1.05.51 1.71 1.33 1.71.82 0 1.25-.64 1.25-1.71 0-1.07-.46-1.71-1.29-1.71Zm-5.29-2.36 2.38 6.53h-1.09l-.6-1.77h-2.51l-.6 1.77H22l2.37-6.53ZM40.12.8v6.84h-1V.8Zm3 1.83a2.172 2.172 0 0 1 2.21 2.48v.33h-3.5a1.327 1.327 0 0 0 .617 1.262c.211.131.455.199.703.198a1.183 1.183 0 0 0 1.18-.62h.94a1.999 1.999 0 0 1-2.15 1.41 2.245 2.245 0 0 1-2.093-1.276 2.238 2.238 0 0 1-.197-1.264 2.282 2.282 0 0 1 2.33-2.52ZM31 2.64c1.26 0 2.06 1 2.06 2.54S32.24 7.72 31 7.72a1.617 1.617 0 0 1-1.53-.82h-.07v2.37h-1V2.73h.94v.81h.07a1.738 1.738 0 0 1 1.59-.9Zm5.34 0c1.27 0 2.06 1 2.06 2.54s-.79 2.54-2.05 2.54a1.61 1.61 0 0 1-1.52-.82h-.08v2.37h-1V2.73h.94v.81h.07a1.753 1.753 0 0 1 1.56-.9Zm.26 8.27a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm-7.3 0a.621.621 0 0 1 .557.893.625.625 0 0 1-.702.33.621.621 0 0 1 .125-1.223Zm43 2.8A1.278 1.278 0 0 0 71 15h2.5a1.203 1.203 0 0 0-.587-1.119 1.214 1.214 0 0 0-.633-.171Zm-39.46 0c-.85 0-1.32.64-1.32 1.64 0 1 .47 1.65 1.32 1.65.85 0 1.34-.64 1.34-1.65 0-1.01-.53-1.59-1.36-1.59Zm-8-1.49h-1.31V17h1.24c1.37 0 2.17-.87 2.17-2.39a2.099 2.099 0 0 0-.2-1.177 2.096 2.096 0 0 0-1.97-1.163Zm36 3.41-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72a1.17 1.17 0 0 0 1.28-1.11ZM44 15.68l-1.21.09c-.68 0-1 .28-1 .73 0 .45.39.72.92.72A1.168 1.168 0 0 0 44 16.11ZM24.88 11.4a2.894 2.894 0 0 1 2.83 1.572c.261.515.362 1.096.29 1.668 0 2.08-1.13 3.28-3.09 3.28h-2.4V11.4Zm4.89 1.6v4.91h-1V13Zm21.58-1.6 2.09 5.1h.08l2.1-5.1h1.19v6.52h-1v-4.78h-.01l-1.93 4.67h-.77l-1.93-4.67h-.06v4.78h-1V11.4ZM47 11.09v6.83h-1v-6.83ZM37.07 13v4.91h-1V13Zm40.49-.08c.149.015.296.041.44.08v.92a2.427 2.427 0 0 0-.55-.06 1.174 1.174 0 0 0-1.079.541 1.165 1.165 0 0 0-.181.599v2.93h-1V13h.94v.75h.07a1.34 1.34 0 0 1 1.36-.82Zm-38.19-1.15V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm29.17 0V13h1.07v.77h-1.07v2.6c0 .52.21.75.7.75.123.011.247.011.37 0v.78a4.26 4.26 0 0 1-.53 0c-1.09 0-1.52-.37-1.52-1.32V13.8h-.79V13h.79v-1.23Zm-8.73 1.15c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69h-.08a1.678 1.678 0 0 1-1.49.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47l1.34-.07v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-.94c.05-.83.85-1.37 1.97-1.37Zm-16.75 0c1.25 0 1.95.61 1.95 1.64v3.36h-.94v-.69H44a1.682 1.682 0 0 1-1.5.77 1.493 1.493 0 0 1-1.424-.675 1.497 1.497 0 0 1-.236-.785c0-.88.67-1.39 1.85-1.47L44 15v-.43c0-.53-.34-.84-1-.84-.66 0-.93.21-1 .56h-1c.14-.83.94-1.37 2.06-1.37Zm21.44 0c1.12 0 1.84.51 1.94 1.36h-.94c-.09-.36-.44-.6-1-.6s-1 .26-1 .66.25.49.79.62l.82.19c.94.22 1.39.62 1.39 1.34 0 .91-.87 1.53-2.06 1.53s-2-.53-2-1.39h1c.082.212.233.39.429.505.197.114.426.158.651.125.61 0 1-.28 1-.68s-.23-.51-.73-.63l-.87-.2c-.94-.22-1.38-.63-1.38-1.36 0-.73.84-1.47 1.96-1.47Zm7.79 0a2.158 2.158 0 0 1 2.21 2.47v.34H71a1.326 1.326 0 0 0 .62 1.253c.21.13.453.198.7.197a1.174 1.174 0 0 0 1.18-.61h.94A2 2 0 0 1 72.29 18a2.237 2.237 0 0 1-2.085-1.265A2.245 2.245 0 0 1 70 15.48a2.29 2.29 0 0 1 .205-1.263 2.28 2.28 0 0 1 2.085-1.297Zm-39.74 0a1.73 1.73 0 0 1 1.56.86h.07V13h.94v5c0 1.12-.89 1.83-2.29 1.83-1.24 0-2-.55-2.15-1.37h1c.08.37.49.6 1.15.6.82 0 1.3-.4 1.3-1.06v-1h-.07a1.665 1.665 0 0 1-.647.632 1.667 1.667 0 0 1-.883.198c-1.27 0-2.05-1-2.05-2.45 0-1.45.8-2.45 2.07-2.45Z" fill-rule="evenodd" />
|
|
||||||
<path clip-path="url(#a)" d="M-5-4.2h88v29H-5Z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 80" height="32">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 80" height="32">
|
||||||
<path d="M25.5 14.9c-6.9 1.7-13.4 6.9-15.4 12.4-.6 1.6-1.1 5.2-1.1 8.1 0 7.3 5.3 17.9 13 25.7 5.6 5.7 6 5.9 10.9 5.9H38l-6.3-6.4c-8-8.2-11.7-15-12.4-22.5-.7-7.6 2-13.2 7.9-16.2 3.8-2 5.3-2.1 16.3-1.6 7.8.3 14.6 1.3 19.4 2.7 4 1.1 7.6 1.8 7.9 1.5 2.3-2.2-12.4-7.5-26.4-9.5-8.4-1.1-14.4-1.2-18.9-.1z" />
|
<path d="M25.5 14.9c-6.9 1.7-13.4 6.9-15.4 12.4-.6 1.6-1.1 5.2-1.1 8.1 0 7.3 5.3 17.9 13 25.7 5.6 5.7 6 5.9 10.9 5.9H38l-6.3-6.4c-8-8.2-11.7-15-12.4-22.5-.7-7.6 2-13.2 7.9-16.2 3.8-2 5.3-2.1 16.3-1.6 7.8.3 14.6 1.3 19.4 2.7 4 1.1 7.6 1.8 7.9 1.5 2.3-2.2-12.4-7.5-26.4-9.5-8.4-1.1-14.4-1.2-18.9-.1z" />
|
||||||
<path d="M38 27.2c-12.8 6.5-11.9 21.3 2.1 34.3 4.6 4.2 6.7 5.5 9.1 5.5h3.1l-5.5-5.8c-7.2-7.5-10.2-13.3-9.5-18.3.6-4.7 3.2-8.4 7.4-10.4 3.5-1.7 17.3-2 23.6-.6 2.9.7 3.7.6 3.7-.5 0-1.8-3.5-3.2-12.8-5-10.5-2-16-1.8-21.2.8zM95.2 41.3c-3.4 8.9-6.2 16.6-6.2 17 0 .4.9.7 1.9.7 1.4 0 2.4-1.3 3.7-4.5l1.7-4.5h14.5l1.4 4.2c1.1 3.1 2.1 4.4 3.6 4.6 1.2.2 2.2 0 2.2-.5 0-.4-2.8-8.1-6.2-17-8.3-21.6-8.3-21.6-16.6 0zm13.8 4.4c0 .2-2.5.3-5.6.3-4.1 0-5.5-.3-5.2-1.3.3-.6 1.5-4.3 2.8-8.1l2.3-6.9 2.8 7.8c1.6 4.4 2.9 8 2.9 8.2zM126.2 41.3c-3.4 8.9-6.2 16.5-6.2 16.9 0 .5.9.8 1.9.8 1.4 0 2.4-1.3 3.7-4.5l1.7-4.5h14.5l1.4 4.2c1.1 3.2 2.1 4.4 3.7 4.6 2.8.4 3 1.2-4.4-18.1-8-20.9-8-20.9-16.3.6zm13.8 4.4c0 .2-2.5.3-5.6.3-4.1 0-5.5-.3-5.2-1.3.3-.6 1.5-4.3 2.8-8.1l2.3-7 2.8 8c1.6 4.3 2.9 8 2.9 8.1zM160.3 27.1c-8.3 4.1-11.6 15.2-7.1 23.9 2.8 5.5 7.3 8.2 13.6 8.3 6.2.1 9.2-1 9.2-3.3 0-1.7-.4-1.8-3.1-.8-4.3 1.5-10.6.4-13.5-2.3-3-2.8-4.7-8.8-3.8-13.5 1.4-7.6 9.1-12.5 16.4-10.4 1.9.6 3.6 1 3.7 1 1.1 0 0-3.9-1.3-4.4-3.2-1.2-10-.5-14.1 1.5z" />
|
<path d="M38 27.2c-12.8 6.5-11.9 21.3 2.1 34.3 4.6 4.2 6.7 5.5 9.1 5.5h3.1l-5.5-5.8c-7.2-7.5-10.2-13.3-9.5-18.3.6-4.7 3.2-8.4 7.4-10.4 3.5-1.7 17.3-2 23.6-.6 2.9.7 3.7.6 3.7-.5 0-1.8-3.5-3.2-12.8-5-10.5-2-16-1.8-21.2.8zM95.2 41.3c-3.4 8.9-6.2 16.6-6.2 17 0 .4.9.7 1.9.7 1.4 0 2.4-1.3 3.7-4.5l1.7-4.5h14.5l1.4 4.2c1.1 3.1 2.1 4.4 3.6 4.6 1.2.2 2.2 0 2.2-.5 0-.4-2.8-8.1-6.2-17-8.3-21.6-8.3-21.6-16.6 0zm13.8 4.4c0 .2-2.5.3-5.6.3-4.1 0-5.5-.3-5.2-1.3.3-.6 1.5-4.3 2.8-8.1l2.3-6.9 2.8 7.8c1.6 4.4 2.9 8 2.9 8.2zM126.2 41.3c-3.4 8.9-6.2 16.5-6.2 16.9 0 .5.9.8 1.9.8 1.4 0 2.4-1.3 3.7-4.5l1.7-4.5h14.5l1.4 4.2c1.1 3.2 2.1 4.4 3.7 4.6 2.8.4 3 1.2-4.4-18.1-8-20.9-8-20.9-16.3.6zm13.8 4.4c0 .2-2.5.3-5.6.3-4.1 0-5.5-.3-5.2-1.3.3-.6 1.5-4.3 2.8-8.1l2.3-7 2.8 8c1.6 4.3 2.9 8 2.9 8.1zM160.3 27.1c-8.3 4.1-11.6 15.2-7.1 23.9 2.8 5.5 7.3 8.2 13.6 8.3 6.2.1 9.2-1 9.2-3.3 0-1.7-.4-1.8-3.1-.8-4.3 1.5-10.6.4-13.5-2.3-3-2.8-4.7-8.8-3.8-13.5 1.4-7.6 9.1-12.5 16.4-10.4 1.9.6 3.6 1 3.7 1 1.1 0 0-3.9-1.3-4.4-3.2-1.2-10-.5-14.1 1.5z" />
|
||||||
<path d="M54.3 35.7c-2.8.5-7.2 4.9-7.9 8-1.1 4.1.6 9.1 4.6 14.1 5.5 7 9.5 9.2 16.5 9.2h5.7l-.4-14.1c-.3-13.4-.5-14.2-2.7-16-2.3-1.8-9.6-2.4-15.8-1.2z" />
|
<path d="M54.3 35.7c-2.8.5-7.2 4.9-7.9 8-1.1 4.1.6 9.1 4.6 14.1 5.5 7 9.5 9.2 16.5 9.2h5.7l-.4-14.1c-.3-13.4-.5-14.2-2.7-16-2.3-1.8-9.6-2.4-15.8-1.2z" />
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
12
src/applemusic/glue/router.ts
Normal file
12
src/applemusic/glue/router.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { offRoute, onRoute } from "../../common/router";
|
||||||
|
import { RouteCallback } from "../../common/router";
|
||||||
|
|
||||||
|
const ALBUM_PATTERN = "[a-z]{2}/album/.+/.+";
|
||||||
|
|
||||||
|
export function onAlbumRoute(cb: RouteCallback) {
|
||||||
|
onRoute(ALBUM_PATTERN, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function offAlbumRoute(cb: RouteCallback) {
|
||||||
|
offRoute(ALBUM_PATTERN, cb);
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { offRoute, onRoute } from "../../common";
|
|
||||||
import { RouteCallback } from "../../common/router";
|
|
||||||
|
|
||||||
const ALBUM_PATTERN = "/:country/album/:slug?/:id";
|
|
||||||
|
|
||||||
export function onAlbumRoute(cb: RouteCallback, once = false) {
|
|
||||||
onRoute(ALBUM_PATTERN, cb, once);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function offAlbumRoute(cb: RouteCallback, once = false) {
|
|
||||||
offRoute(ALBUM_PATTERN, cb, once);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { fromHTML, waitFor } from "../../common";
|
import { observeSelector } from "../../common/dom";
|
||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
import { ensureMenu } from "../../common/menu";
|
||||||
|
|
||||||
export type ButtonElement = Brand<HTMLElement, "button">;
|
type ButtonElement = Brand<HTMLElement, "applemusic-button">;
|
||||||
|
|
||||||
let navEl: HTMLElement | null = null;
|
|
||||||
|
|
||||||
export function createButtonElement(text: string, icon: string): ButtonElement {
|
export function createButtonElement(text: string, icon: string): ButtonElement {
|
||||||
return fromHTML<ButtonElement>(`
|
return fromHTML<ButtonElement>(`
|
||||||
<li class="ame-sidebar-button navigation-item navigation-item__personalized" role="listitem" data-ame>
|
<li class="ame-sidebar-button navigation-item" data-ame>
|
||||||
<a class="navigation-item__link" role="button" tabindex="0" data-ame>
|
<a class="navigation-item__link" tabindex="0" data-ame>
|
||||||
${icon}
|
${icon}
|
||||||
<span>${text}</span>
|
<span>${text}</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -15,54 +15,26 @@ export function createButtonElement(text: string, icon: string): ButtonElement {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showButtonElement(buttonEl: ButtonElement, index: number) {
|
export async function showSidebarButton(buttonEl: ButtonElement, index: number) {
|
||||||
if (!navEl) {
|
await observeSelector("amp-chrome-player"); // Wait for native menus to load.
|
||||||
navEl = await waitFor(".navigation__scrollable-container", "amp-chrome-player");
|
|
||||||
if (!navEl) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let categoryEl = document.querySelector("#ame-sidebar");
|
const menuEl = ensureMenu("#ame-sidebar", () => {
|
||||||
if (!categoryEl) {
|
const refEl = document.querySelector(".navigation__scrollable-container");
|
||||||
categoryEl = fromHTML(`
|
refEl?.appendChild(fromHTML(`
|
||||||
<div class="navigation-items navigation-items--personalized" data-ame>
|
<div class="navigation-items" data-ame>
|
||||||
<div class="navigation-items__header" data-ame>
|
<div class="navigation-items__header" data-ame>
|
||||||
<span>Ame</span>
|
<span>Ame</span>
|
||||||
</div>
|
</div>
|
||||||
<ul id="ame-sidebar" role="list" class="navigation-items__list" data-ame>
|
<ul class="navigation-items__list" data-ame>
|
||||||
|
<li id="ame-sidebar" style="display: none;"></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`));
|
||||||
|
});
|
||||||
|
|
||||||
navEl.appendChild(categoryEl);
|
menuEl.addMenuItem(buttonEl, index);
|
||||||
categoryEl = document.querySelector<HTMLElement>("#ame-sidebar")!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonEls = Array.from(categoryEl.querySelectorAll(".ame-sidebar-button")) as HTMLElement[];
|
|
||||||
buttonEl.setAttribute("data-index", index.toString());
|
|
||||||
|
|
||||||
if (buttonEls.length === 0) {
|
|
||||||
categoryEl.appendChild(buttonEl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bestDist = Number.MAX_VALUE;
|
|
||||||
let refEl = buttonEls[0];
|
|
||||||
|
|
||||||
for (const buttonEl of buttonEls) {
|
|
||||||
const dist = Math.abs(Number(buttonEl.getAttribute("data-index")) - index);
|
|
||||||
if (dist >= bestDist) continue;
|
|
||||||
|
|
||||||
bestDist = dist;
|
|
||||||
refEl = buttonEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index > Number(refEl.getAttribute("data-index"))) {
|
|
||||||
refEl.after(buttonEl);
|
|
||||||
} else {
|
|
||||||
refEl.before(buttonEl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hideButtonElement(buttonEl: ButtonElement) {
|
export async function hideSidebarButton(buttonEl: ButtonElement) {
|
||||||
buttonEl.remove();
|
buttonEl.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @namespace ame-applemusic
|
// @namespace ame-applemusic
|
||||||
// @name Ame (Apple Music)
|
// @name Ame (Apple Music)
|
||||||
// @version 1.8.1
|
// @version 1.8.2
|
||||||
// @author SuperSaltyGamer
|
// @author SuperSaltyGamer
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @match https://music.apple.com/*
|
// @match https://music.apple.com/*
|
||||||
@@ -10,46 +10,22 @@
|
|||||||
// @grant GM.xmlHttpRequest
|
// @grant GM.xmlHttpRequest
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
import { observe, waitFor } from '../common';
|
|
||||||
import { offAlbumRoute, onAlbumRoute } from './glue/routing';
|
|
||||||
import { hideButtonElement, showButtonElement } from './glue/sidebar';
|
|
||||||
import './modules/badges';
|
|
||||||
import { checkCountriesButtonEl } from './modules/countries';
|
|
||||||
import { searchCoversButtonEl } from './modules/covers';
|
|
||||||
import { copyAuthButtonEl } from './modules/dev';
|
|
||||||
import { checkQualitiesButtonEl } from './modules/qualities';
|
|
||||||
import { showInfoButtonEl } from './modules/info';
|
|
||||||
import './modules/qualities';
|
|
||||||
import styles from './style.css?inline';
|
|
||||||
import handsonStyles from "handsontable/dist/handsontable.full.min.css?inline";
|
import handsonStyles from "handsontable/dist/handsontable.full.min.css?inline";
|
||||||
|
import { observeSelector } from "../common/dom";
|
||||||
|
import "./modules/badges";
|
||||||
|
import "./modules/storefronts";
|
||||||
|
import "./modules/covers";
|
||||||
|
import "./modules/dev";
|
||||||
|
import "./modules/qualities";
|
||||||
|
import "./modules/info";
|
||||||
|
import "./modules/qualities";
|
||||||
|
import styles from "./style.css?inline";
|
||||||
|
|
||||||
GM.addStyle(handsonStyles);
|
GM.addStyle(handsonStyles);
|
||||||
GM.addStyle(styles);
|
GM.addStyle(styles);
|
||||||
|
|
||||||
// Add sidebar button for all pages.
|
|
||||||
waitFor('nav', 'amp-chrome-player').then((navEl) => {
|
|
||||||
if (!navEl) return;
|
|
||||||
|
|
||||||
showButtonElement(copyAuthButtonEl, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add sidebar buttons for album page.
|
|
||||||
onAlbumRoute(async () => {
|
|
||||||
showButtonElement(checkCountriesButtonEl, 100);
|
|
||||||
showButtonElement(checkQualitiesButtonEl, 200);
|
|
||||||
showButtonElement(searchCoversButtonEl, 300);
|
|
||||||
showButtonElement(showInfoButtonEl, 400);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove sidebar buttons for non-album pages.
|
|
||||||
offAlbumRoute(() => {
|
|
||||||
hideButtonElement(searchCoversButtonEl);
|
|
||||||
hideButtonElement(checkCountriesButtonEl);
|
|
||||||
hideButtonElement(showInfoButtonEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide trial upsell modal.
|
// Hide trial upsell modal.
|
||||||
observe('iframe[src^="/includes/commerce/subscribe"]', () => {
|
observeSelector<HTMLDialogElement>("iframe[src^='/includes/commerce/subscribe']", { timeout: 0 })
|
||||||
const backdropEl = document.querySelector<HTMLElement>('.backdrop');
|
.then((dialogEl) => {
|
||||||
backdropEl?.click();
|
dialogEl?.remove();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,31 @@
|
|||||||
import { fromHTML, waitFor } from "../../common";
|
import { fromHTML, observeSelector } from "../../common/dom";
|
||||||
import lossyBadge from "../assets/badges/lossy.svg?raw";
|
import lossyBadge from "../assets/badges/lossy.svg?raw";
|
||||||
import admBadge from "../assets/badges/adm.svg?raw";
|
import admBadge from "../assets/badges/adm.svg?raw";
|
||||||
import atmosBadge from "../assets/badges/atmos.svg?raw";
|
import atmosBadge from "../assets/badges/atmos.svg?raw";
|
||||||
import hiresLosslessBadge from "../assets/badges/hires-lossless.svg?raw";
|
import hiresLosslessBadge from "../assets/badges/hires-lossless.svg?raw";
|
||||||
import losslessBadge from "../assets/badges/lossless.svg?raw";
|
import losslessBadge from "../assets/badges/lossless.svg?raw";
|
||||||
import spatialBadge from "../assets/badges/spatial.svg?raw";
|
import spatialBadge from "../assets/badges/spatial.svg?raw";
|
||||||
import { onAlbumRoute } from "../glue/routing";
|
import { onAlbumRoute } from "../glue/router";
|
||||||
import { getAlbum } from "../services";
|
import { getAlbum, getPageAlbumId } from "../services/album";
|
||||||
|
|
||||||
// Reliably get album data for displaying quality badges.
|
|
||||||
onAlbumRoute(async () => {
|
onAlbumRoute(async () => {
|
||||||
if (document.querySelector(".ame-album-badges-container")) return;
|
const album = await getAlbum(getPageAlbumId());
|
||||||
|
|
||||||
const albumId = location.pathname.split("/")[4];
|
|
||||||
const album = await getAlbum(albumId);
|
|
||||||
if (!album) return;
|
if (!album) return;
|
||||||
|
|
||||||
const refEl = await waitFor(".headings__metadata-bottom", ".description");
|
const refEl = await observeSelector(".headings__metadata-bottom");
|
||||||
if (!refEl) return;
|
if (!refEl) return;
|
||||||
|
|
||||||
const audioTraits = album.attributes.audioTraits;
|
const audioTraits = album.attributes.audioTraits;
|
||||||
if (album.attributes.isMasteredForItunes) audioTraits.push("adm");
|
if (album.attributes.isMasteredForItunes) audioTraits.push("adm");
|
||||||
|
|
||||||
const containerEl = fromHTML(`<p class="ame-album-badges-container"></p>`);
|
const containerEl = fromHTML<HTMLParagraphElement>(`<p class="ame-album-badges-container"></p>`);
|
||||||
|
|
||||||
if (audioTraits.includes("lossy-stereo")) containerEl.innerHTML += lossyBadge;
|
if (audioTraits.includes("lossy-stereo")) containerEl.insertAdjacentHTML("beforeend", lossyBadge);
|
||||||
if (audioTraits.includes("lossless")) containerEl.innerHTML += losslessBadge;
|
if (audioTraits.includes("lossless")) containerEl.insertAdjacentHTML("beforeend", losslessBadge);
|
||||||
if (audioTraits.includes("hi-res-lossless")) containerEl.innerHTML += hiresLosslessBadge;
|
if (audioTraits.includes("hi-res-lossless")) containerEl.insertAdjacentHTML("beforeend", hiresLosslessBadge);
|
||||||
if (audioTraits.includes("atmos")) containerEl.innerHTML += atmosBadge;
|
if (audioTraits.includes("atmos")) containerEl.insertAdjacentHTML("beforeend", atmosBadge);
|
||||||
if (audioTraits.includes("adm")) containerEl.innerHTML += admBadge;
|
if (audioTraits.includes("adm")) containerEl.insertAdjacentHTML("beforeend", admBadge);
|
||||||
if (audioTraits.includes("spatial")) containerEl.innerHTML += spatialBadge;
|
if (audioTraits.includes("spatial")) containerEl.insertAdjacentHTML("beforeend", spatialBadge);
|
||||||
|
|
||||||
refEl.after(containerEl);
|
refEl.after(containerEl);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { fromHTML, sleep, waitFor } from '../../common';
|
|
||||||
import flagIcon from '../assets/icons/flag.svg?raw';
|
|
||||||
import { offAlbumRoute, onAlbumRoute } from '../glue/routing';
|
|
||||||
import { createButtonElement } from '../glue/sidebar';
|
|
||||||
import { getAlbum, getStorefronts } from '../services';
|
|
||||||
|
|
||||||
const PREFERRED_STOREFRONTS = [ 'jp', 'kr', 'us', 'de', 'fr', 'gb', 'in', 'it', 'es', 'br', 'au', 'nz', 'cn', 'hk' ];
|
|
||||||
PREFERRED_STOREFRONTS.reverse();
|
|
||||||
|
|
||||||
let globalJob: AbortController | null = null;
|
|
||||||
|
|
||||||
export const checkCountriesButtonEl = createButtonElement('Check Countries', flagIcon);
|
|
||||||
|
|
||||||
// Start checking countries when the sidebar button is clicked.
|
|
||||||
checkCountriesButtonEl.addEventListener('click', async () => {
|
|
||||||
const refEl = document.querySelector<HTMLElement>('.section');
|
|
||||||
if (refEl) await checkCountries(refEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start checking countries when the error page is shown.
|
|
||||||
onAlbumRoute(async () => {
|
|
||||||
const errorEl = await waitFor('.page-error');
|
|
||||||
if (errorEl) checkCountries(errorEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
onAlbumRoute(() => {
|
|
||||||
globalJob?.abort();
|
|
||||||
globalJob = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
offAlbumRoute(() => {
|
|
||||||
globalJob?.abort();
|
|
||||||
globalJob = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function checkCountries(refEl: HTMLElement) {
|
|
||||||
if (globalJob) return;
|
|
||||||
const job = new AbortController();
|
|
||||||
globalJob = job;
|
|
||||||
|
|
||||||
const albumId = location.pathname.split('/')[4];
|
|
||||||
|
|
||||||
const headerEl = fromHTML(`<div class="section ame-album-countries-header">Availability in the following storefronts:</div>`);
|
|
||||||
const containerEl = fromHTML(`
|
|
||||||
<div class="section ame-album-countries-container">
|
|
||||||
<div class="ame-color-primary"></div>
|
|
||||||
<div class="ame-color-secondary"></div>
|
|
||||||
<div class="ame-color-tertiary"></div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
const primaryContainerEl = containerEl.children[0] as HTMLElement;
|
|
||||||
const secondaryContainerEl = containerEl.children[1] as HTMLElement;
|
|
||||||
const tertiaryContainerEl = containerEl.children[2] as HTMLElement;
|
|
||||||
|
|
||||||
refEl.append(headerEl);
|
|
||||||
refEl.append(containerEl);
|
|
||||||
|
|
||||||
let storefronts = await getStorefronts();
|
|
||||||
|
|
||||||
storefronts = storefronts
|
|
||||||
.map(storefront => {
|
|
||||||
storefront.attributes.name = storefront.attributes.name.split(', ').reverse().join(' ');
|
|
||||||
return storefront;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
return Math.max(PREFERRED_STOREFRONTS.indexOf(b.id), 0) - Math.max(PREFERRED_STOREFRONTS.indexOf(a.id), 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const storefront of storefronts) {
|
|
||||||
if (job.signal.aborted) break;
|
|
||||||
|
|
||||||
const album = await getAlbum(albumId, storefront.id);
|
|
||||||
|
|
||||||
// Album totally unavailable.
|
|
||||||
if (!album) {
|
|
||||||
tertiaryContainerEl.append(fromHTML(`
|
|
||||||
<span data-storefront="${storefront.id}" title="Totally unavailable">${storefront.attributes.name}, </span>
|
|
||||||
`));
|
|
||||||
|
|
||||||
await sleep(222);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const discCount = Math.max(...album.relationships.tracks.data.map(track => track.attributes.discNumber));
|
|
||||||
const songTracks = album.relationships.tracks.data.filter(track => track.type === 'songs');
|
|
||||||
const otherTracks = album.relationships.tracks.data.filter(track => track.type !== 'songs');
|
|
||||||
const unavailableTracks = new Set();
|
|
||||||
for (let i = 1; i <= album.attributes.trackCount - otherTracks.length; i++) unavailableTracks.add(i);
|
|
||||||
|
|
||||||
songTracks.forEach((track, i) => {
|
|
||||||
if (!track.attributes.extendedAssetUrls) return;
|
|
||||||
if (!track.attributes.playParams) return;
|
|
||||||
unavailableTracks.delete(discCount > 1 ? i + 1 : track.attributes.trackNumber);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Album partially available.
|
|
||||||
if (unavailableTracks.size) {
|
|
||||||
if (discCount > 1) {
|
|
||||||
secondaryContainerEl.append(fromHTML(`
|
|
||||||
<span data-storefront="${storefront.id}"><a target="_blank" href="https://music.apple.com/${storefront.id}/album/${albumId}" title="Partially available, missing:\n${unavailableTracks.size} tracks">${storefront.attributes.name}</a>, </span>
|
|
||||||
`));
|
|
||||||
} else {
|
|
||||||
secondaryContainerEl.append(fromHTML(`
|
|
||||||
<span data-storefront="${storefront.id}"><a target="_blank" href="https://music.apple.com/${storefront.id}/album/${albumId}" title="Partially available, missing:\n${Array.from(unavailableTracks).join(', ')}">${storefront.attributes.name}</a>, </span>
|
|
||||||
`));
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(222);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Album fully available.
|
|
||||||
primaryContainerEl.append(fromHTML(`
|
|
||||||
<span data-storefront="${storefront.id}"><a target="_blank" href="https://music.apple.com/${storefront.id}/album/${albumId}" title="Fully available">${storefront.attributes.name}</a>, </span>
|
|
||||||
`));
|
|
||||||
|
|
||||||
await sleep(222);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,37 @@
|
|||||||
import paletteIcon from "../assets/icons/palette.svg?raw";
|
import paletteIcon from "../assets/icons/palette.svg?raw";
|
||||||
import { createButtonElement } from "../glue/sidebar";
|
import { offAlbumRoute, onAlbumRoute } from "../glue/router";
|
||||||
|
import { createButtonElement, hideSidebarButton, showSidebarButton } from "../glue/sidebar";
|
||||||
|
|
||||||
export const searchCoversButtonEl = createButtonElement("Search Covers", paletteIcon);
|
export const searchCoversButtonEl = createButtonElement("Search Covers", paletteIcon);
|
||||||
|
|
||||||
searchCoversButtonEl.addEventListener("click", () => {
|
searchCoversButtonEl.addEventListener("click", () => {
|
||||||
const titleEl = document.querySelector<HTMLElement>("h1.headings__title");
|
const titleEl = document.querySelector<HTMLElement>(".headings__title");
|
||||||
if (!titleEl) return;
|
if (!titleEl) return;
|
||||||
|
|
||||||
const artistEls = Array.from(document.querySelectorAll<HTMLElement>(".headings__subtitles > a"));
|
const artistEl = document.querySelector<HTMLElement>(".headings__subtitles > a");
|
||||||
const artist = artistEls.map(el => el.innerText).join(" ");
|
if (!artistEl) return;
|
||||||
const album = titleEl.innerText.replace(" - Single", "").replace(" - EP", "");
|
|
||||||
|
const artist = artistEl.innerText.trim();
|
||||||
|
const album = titleEl.innerText.trim().replace(/ - Single$/i, "").replace(/ - EP$/i, "");
|
||||||
|
|
||||||
open(`https://covers.musichoarders.xyz?artist=${encodeURIComponent(artist)}&album=${encodeURIComponent(album)}`, "_blank");
|
open(`https://covers.musichoarders.xyz?artist=${encodeURIComponent(artist)}&album=${encodeURIComponent(album)}`, "_blank");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onAlbumRoute(() => {
|
||||||
|
showSidebarButton(searchCoversButtonEl, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
offAlbumRoute(() => {
|
||||||
|
hideSidebarButton(searchCoversButtonEl);
|
||||||
|
});
|
||||||
|
|
||||||
// Replace release cover image with full sized variant on right click.
|
// Replace release cover image with full sized variant on right click.
|
||||||
addEventListener("mousedown", async e => {
|
addEventListener("mousedown", async e => {
|
||||||
if (e.button !== 2) return;
|
if (e.button !== 2) return;
|
||||||
|
|
||||||
const imgEl = e.target as HTMLImageElement;
|
const imgEl = e.target as HTMLImageElement;
|
||||||
if (!imgEl.matches(".artwork-component__image:not(.ame-fullsized)")) return;
|
if (!imgEl.matches(".artwork-component__image:not(.ame-full-sized)")) return;
|
||||||
imgEl.classList.add("ame-fullsized");
|
imgEl.classList.add("ame-full-sized");
|
||||||
|
|
||||||
const refSrcEl = document.querySelector<HTMLSourceElement>(".artwork__radiosity source");
|
const refSrcEl = document.querySelector<HTMLSourceElement>(".artwork__radiosity source");
|
||||||
if (!refSrcEl) return;
|
if (!refSrcEl) return;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import shieldIcon from "../assets/icons/shield.svg?raw";
|
import shieldIcon from "../assets/icons/shield.svg?raw";
|
||||||
import { createButtonElement } from "../glue/sidebar";
|
import { createButtonElement, showSidebarButton } from "../glue/sidebar";
|
||||||
import { getAuthToken } from "../services";
|
import { getAuthToken } from "../services/auth";
|
||||||
|
|
||||||
export const copyAuthButtonEl = createButtonElement("Copy Authorization", shieldIcon);
|
const copyAuthButtonEl = createButtonElement("Copy Authorization", shieldIcon);
|
||||||
|
|
||||||
copyAuthButtonEl.addEventListener("click", async () => {
|
copyAuthButtonEl.addEventListener("click", async () => {
|
||||||
GM.setClipboard(await getAuthToken());
|
GM.setClipboard(await getAuthToken());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
showSidebarButton(copyAuthButtonEl, 0);
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import Handsontable from "handsontable/base";
|
import Handsontable from "handsontable/base";
|
||||||
import { registerPlugin, AutoColumnSize, ManualColumnMove, CopyPaste, DragToScroll } from "handsontable/plugins";
|
import { registerPlugin, AutoColumnSize, ManualColumnMove, CopyPaste, DragToScroll } from "handsontable/plugins";
|
||||||
import infoIcon from "../assets/icons/info.svg?raw";
|
import infoIcon from "../assets/icons/info.svg?raw";
|
||||||
import { offAlbumRoute, onAlbumRoute } from "../glue/routing";
|
import { offAlbumRoute, onAlbumRoute } from "../glue/router";
|
||||||
import { createButtonElement } from "../glue/sidebar";
|
import { createButtonElement, hideSidebarButton, showSidebarButton } from "../glue/sidebar";
|
||||||
import { getAccountStorefront, getAlbum, getStorefronts } from "../services";
|
import { getAlbum } from "../services/album";
|
||||||
import { fetchCors, fromHTML } from "../../common";
|
import { getAccountStorefront, getStorefronts } from "../services/storefront";
|
||||||
|
import { fetchCors } from "../../common/fetch";
|
||||||
import { Album, Resource } from "../types";
|
import { Album, Resource } from "../types";
|
||||||
import { ripLyrics } from "./lyrics";
|
import { ripLyrics } from "./lyrics";
|
||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
|
||||||
registerPlugin(AutoColumnSize);
|
registerPlugin(AutoColumnSize);
|
||||||
registerPlugin(ManualColumnMove);
|
registerPlugin(ManualColumnMove);
|
||||||
@@ -85,10 +87,12 @@ let isVisible = false;
|
|||||||
let dockEl: HTMLElement | null = null;
|
let dockEl: HTMLElement | null = null;
|
||||||
|
|
||||||
onAlbumRoute(async () => {
|
onAlbumRoute(async () => {
|
||||||
|
showSidebarButton(showInfoButtonEl, 400);
|
||||||
hideDock();
|
hideDock();
|
||||||
});
|
});
|
||||||
|
|
||||||
offAlbumRoute(() => {
|
offAlbumRoute(() => {
|
||||||
|
hideSidebarButton(showInfoButtonEl);
|
||||||
hideDock();
|
hideDock();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +110,7 @@ async function showDock() {
|
|||||||
<div id="ame-dock-title">Album Info</div>
|
<div id="ame-dock-title">Album Info</div>
|
||||||
<div id="ame-dock-control">
|
<div id="ame-dock-control">
|
||||||
<select id="ame-dock-control-storefront">
|
<select id="ame-dock-control-storefront">
|
||||||
${storefronts.map(storefront => `<option value="${storefront.id}" ${storefront.id === activeStorefront ? "selected" : ""}>${storefront.attributes.name}</option>`).join("")}
|
${storefronts.map(storefront => `<option value="${storefront.id}" ?selected="${storefront.id === activeStorefront}">${storefront.attributes.name}</option>`).join("")}
|
||||||
</select>
|
</select>
|
||||||
<button id="ame-dock-control-isrc2mb">ISRC2MB</button>
|
<button id="ame-dock-control-isrc2mb">ISRC2MB</button>
|
||||||
<button id="ame-dock-control-lyrics">LYRICS (${getAccountStorefront()?.toUpperCase() || "N/A"})</button>
|
<button id="ame-dock-control-lyrics">LYRICS (${getAccountStorefront()?.toUpperCase() || "N/A"})</button>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import xmlFormatter from "xml-formatter";
|
import xmlFormatter from "xml-formatter";
|
||||||
import { downloadFile, sleep } from "../../common";
|
import { downloadFile, sleep } from "../../common/misc";
|
||||||
import { getAlbum } from "../services/album";
|
import { getAlbum } from "../services/album";
|
||||||
import { getLyrics } from "../services/lyrics";
|
import { getLyrics } from "../services/lyrics";
|
||||||
import { Lyrics, Track } from "../types";
|
import { Lyrics, Track } from "../types";
|
||||||
import { formatPath } from "../../common/format";
|
import { formatPath } from "../../common/format";
|
||||||
import { getAccountStorefront } from "../services";
|
import { getAccountStorefront } from "../services/storefront";
|
||||||
|
|
||||||
export async function ripLyrics(albumId: string) {
|
export async function ripLyrics(albumId: string) {
|
||||||
const accountStorefront = getAccountStorefront();
|
const accountStorefront = getAccountStorefront();
|
||||||
@@ -43,7 +43,9 @@ export async function ripLyrics(albumId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function zipLyrics(zip: JSZip, track: Track, lyrics: Lyrics, syllable: boolean) {
|
function zipLyrics(zip: JSZip, track: Track, lyrics: Lyrics, syllable: boolean) {
|
||||||
const filename = formatPath(`${track.discNumber}-${track.trackNumber.toString().padStart(2, "0")}. ${track.name.slice(0, 120)}`);
|
const filename = formatPath(
|
||||||
|
`${track.discNumber}-${track.trackNumber.toString().padStart(2, "0")}. ${track.name.slice(0, 120)}`
|
||||||
|
);
|
||||||
|
|
||||||
// Save raw lyrics.
|
// Save raw lyrics.
|
||||||
if (syllable && lyrics.ttml.includes("<span begin=")) {
|
if (syllable && lyrics.ttml.includes("<span begin=")) {
|
||||||
@@ -60,7 +62,7 @@ function zipLyrics(zip: JSZip, track: Track, lyrics: Lyrics, syllable: boolean)
|
|||||||
for (const lineNode of Array.from(lyricsDoc.querySelectorAll("p")) as HTMLParagraphElement[]) {
|
for (const lineNode of Array.from(lyricsDoc.querySelectorAll("p")) as HTMLParagraphElement[]) {
|
||||||
const timestamp = lineNode.getAttribute("begin");
|
const timestamp = lineNode.getAttribute("begin");
|
||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
out += `[${convertTimestamp(timestamp)}] ${lineNode.textContent}\n`;
|
out += `[${formatTimestamp(timestamp)}] ${lineNode.textContent}\n`;
|
||||||
} else {
|
} else {
|
||||||
out += `${lineNode.textContent}\n`;
|
out += `${lineNode.textContent}\n`;
|
||||||
}
|
}
|
||||||
@@ -70,7 +72,7 @@ function zipLyrics(zip: JSZip, track: Track, lyrics: Lyrics, syllable: boolean)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertTimestamp(timestamp: string): string {
|
function formatTimestamp(timestamp: string): string {
|
||||||
const parts = timestamp.split(/[:.]/g).reverse();
|
const parts = timestamp.split(/[:.]/g).reverse();
|
||||||
const mm = (parts[2] ?? "").padStart(2, "0");
|
const mm = (parts[2] ?? "").padStart(2, "0");
|
||||||
const ss = (parts[1] ?? "").padStart(2, "0");
|
const ss = (parts[1] ?? "").padStart(2, "0");
|
||||||
|
|||||||
@@ -1,50 +1,47 @@
|
|||||||
import { fetchCors, fromHTML, sleep } from '../../common';
|
import { sleep } from "../../common/misc";
|
||||||
import hqIcon from '../assets/icons/hq.svg?raw';
|
import hqIcon from "../assets/icons/hq.svg?raw";
|
||||||
import { offAlbumRoute, onAlbumRoute } from '../glue/routing';
|
import { offAlbumRoute, onAlbumRoute } from "../glue/router";
|
||||||
import { createButtonElement } from '../glue/sidebar';
|
import { createButtonElement, hideSidebarButton, showSidebarButton } from "../glue/sidebar";
|
||||||
import { getAlbum } from '../services';
|
import { getAlbum, getPageAlbumId } from "../services/album";
|
||||||
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
|
||||||
interface Quality {
|
interface Quality {
|
||||||
'FIRST-SEGMENT-URI': string;
|
"FIRST-SEGMENT-URI": string;
|
||||||
'AUDIO-FORMAT-ID': string;
|
"AUDIO-FORMAT-ID": string;
|
||||||
'CHANNEL-USAGE'?: string;
|
"CHANNEL-USAGE"?: string;
|
||||||
'CHANNEL-COUNT': string;
|
"CHANNEL-COUNT": string;
|
||||||
'BIT-RATE'?: number;
|
"BIT-RATE"?: number;
|
||||||
'SAMPLE-RATE': number;
|
"SAMPLE-RATE": number;
|
||||||
'BIT-DEPTH': number;
|
"BIT-DEPTH": number;
|
||||||
'IS-ATMOS'?: boolean;
|
"IS-ATMOS"?: boolean;
|
||||||
'__ACTUAL__'?: true;
|
"__ACTUAL__"?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FORMAT_ORDER = [ 'ec+3', 'alac', 'aac ', 'aach' ];
|
const FORMAT_ORDER = [ "ec+3", "alac", "aac ", "aach" ];
|
||||||
const CHANNEL_USAGE_ORDER = [ 'BINAURAL', 'DOWNMIX' ];
|
const CHANNEL_USAGE_ORDER = [ "BINAURAL", "DOWNMIX" ];
|
||||||
|
|
||||||
let globalJob: AbortController | null = null;
|
let globalJob: AbortController | null = null;
|
||||||
|
|
||||||
export const checkQualitiesButtonEl = createButtonElement('Check Qualities', hqIcon);
|
export const checkQualitiesButtonEl = createButtonElement("Check Qualities", hqIcon);
|
||||||
|
|
||||||
checkQualitiesButtonEl.addEventListener('click', async () => {
|
checkQualitiesButtonEl.addEventListener("click", async () => {
|
||||||
if (globalJob) return;
|
if (globalJob) return;
|
||||||
const job = new AbortController();
|
const job = new AbortController();
|
||||||
globalJob = job;
|
globalJob = job;
|
||||||
|
|
||||||
const country = location.pathname.split('/')[1];
|
const album = await getAlbum(getPageAlbumId());
|
||||||
const albumId = location.pathname.split('/')[4];
|
|
||||||
|
|
||||||
const album = await getAlbum(albumId, country);
|
|
||||||
if (!album) return;
|
if (!album) return;
|
||||||
|
|
||||||
const trackEls = Array.from(document.querySelectorAll<HTMLElement>('.songs-list-row__song-wrapper'));
|
const trackEls = Array.from(document.querySelectorAll<HTMLElement>(".songs-list-row__song-wrapper"));
|
||||||
|
|
||||||
for (const track of album.relationships.tracks.data) {
|
for (const track of album.relationships.tracks.data) {
|
||||||
if (job.signal.aborted) break;
|
if (job.signal.aborted) break;
|
||||||
if (track.type !== 'songs') continue;
|
if (track.type !== "songs") continue;
|
||||||
|
|
||||||
const trackEl = trackEls.shift();
|
const trackEl = trackEls.shift();
|
||||||
if (!trackEl) continue;
|
if (!trackEl) continue;
|
||||||
|
|
||||||
trackEl.querySelector('.ame-track-quality')?.remove();
|
|
||||||
|
|
||||||
if (!track.attributes.extendedAssetUrls) {
|
if (!track.attributes.extendedAssetUrls) {
|
||||||
trackEl.append(fromHTML(`<span class="ame-track-quality ame-color-warning">[unavailable]</span>`));
|
trackEl.append(fromHTML(`<span class="ame-track-quality ame-color-warning">[unavailable]</span>`));
|
||||||
continue;
|
continue;
|
||||||
@@ -56,17 +53,17 @@ checkQualitiesButtonEl.addEventListener('click', async () => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifest = await (await fetchCors(manifestUrl)).text();
|
const manifest = await fetchCors(manifestUrl).then((res) => res.text());
|
||||||
await sleep(150);
|
await sleep(150);
|
||||||
|
|
||||||
let data: Record<string, Quality> | null = null;
|
let data: Record<string, Quality> | null = null;
|
||||||
for (const line of manifest.split('\n')) {
|
for (const line of manifest.split("\n")) {
|
||||||
if (!line.startsWith('#EXT-X-SESSION-DATA:DATA-ID="com.apple.hls.audioAssetMetadata"')) continue;
|
if (!line.startsWith("#EXT-X-SESSION-DATA:DATA-ID=\"com.apple.hls.audioAssetMetadata\"")) continue;
|
||||||
data = JSON.parse(atob(line.split('VALUE=')[1].slice(1, -1)));
|
data = JSON.parse(atob(line.split("VALUE=")[1].slice(1, -1)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) throw new Error('Could not find data from track manifest.');
|
if (!data) throw new Error("Could not find data from track manifest.");
|
||||||
|
|
||||||
const qualities = Object.values(data);
|
const qualities = Object.values(data);
|
||||||
qualities.sort(sortQuality);
|
qualities.sort(sortQuality);
|
||||||
@@ -75,56 +72,62 @@ checkQualitiesButtonEl.addEventListener('click', async () => {
|
|||||||
if (realInfo) qualities.push(realInfo);
|
if (realInfo) qualities.push(realInfo);
|
||||||
|
|
||||||
qualities.sort(sortQuality);
|
qualities.sort(sortQuality);
|
||||||
const displayQuality = qualities.find(quality => parseInt(quality['CHANNEL-COUNT']) <= 2)!;
|
|
||||||
|
|
||||||
trackEl.append(fromHTML(`<span class="ame-track-quality ame-color-tertiary" title="${qualities.map(formatQuality).join('\n')}">${formatQuality(displayQuality)}</span>`));
|
// Only use tracks with 2 channels for the highest quality display.
|
||||||
|
const displayQuality = qualities.find(quality => parseInt(quality["CHANNEL-COUNT"]) <= 2)!;
|
||||||
|
|
||||||
|
trackEl.append(fromHTML(`<span class="ame-track-quality ame-color-tertiary" title="${qualities.map(formatQuality).join("\n")}">${formatQuality(displayQuality)}</span>`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onAlbumRoute(() => {
|
onAlbumRoute(() => {
|
||||||
|
showSidebarButton(checkQualitiesButtonEl, 200);
|
||||||
globalJob?.abort();
|
globalJob?.abort();
|
||||||
globalJob = null;
|
globalJob = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
offAlbumRoute(() => {
|
offAlbumRoute(() => {
|
||||||
|
hideSidebarButton(checkQualitiesButtonEl);
|
||||||
globalJob?.abort();
|
globalJob?.abort();
|
||||||
globalJob = null;
|
globalJob = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
function sortQuality(a: Quality, b: Quality): number {
|
function sortQuality(a: Quality, b: Quality): number {
|
||||||
return FORMAT_ORDER.indexOf(a['AUDIO-FORMAT-ID']) - FORMAT_ORDER.indexOf(b['AUDIO-FORMAT-ID']) ||
|
return FORMAT_ORDER.indexOf(a["AUDIO-FORMAT-ID"]) - FORMAT_ORDER.indexOf(b["AUDIO-FORMAT-ID"]) ||
|
||||||
b['BIT-DEPTH'] - a['BIT-DEPTH'] ||
|
b["BIT-DEPTH"] - a["BIT-DEPTH"] ||
|
||||||
b['SAMPLE-RATE'] - a['SAMPLE-RATE'] ||
|
b["SAMPLE-RATE"] - a["SAMPLE-RATE"] ||
|
||||||
(b['BIT-RATE'] ?? NaN) - (a['BIT-RATE'] ?? NaN) ||
|
(b["BIT-RATE"] ?? NaN) - (a["BIT-RATE"] ?? NaN) ||
|
||||||
CHANNEL_USAGE_ORDER.indexOf(a['CHANNEL-USAGE'] ?? '') - CHANNEL_USAGE_ORDER.indexOf(b['CHANNEL-USAGE'] ?? '') ||
|
CHANNEL_USAGE_ORDER.indexOf(a["CHANNEL-USAGE"] ?? "") - CHANNEL_USAGE_ORDER.indexOf(b["CHANNEL-USAGE"] ?? "") ||
|
||||||
-Number(a['__ACTUAL__']);
|
-Number(a["__ACTUAL__"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatQuality(quality: Quality): string {
|
function formatQuality(quality: Quality): string {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
parts.push(quality['AUDIO-FORMAT-ID']);
|
parts.push(quality["AUDIO-FORMAT-ID"]);
|
||||||
if (quality['CHANNEL-COUNT']) parts.push(`${quality['CHANNEL-COUNT']}ch`);
|
if (quality["CHANNEL-COUNT"]) parts.push(`${quality["CHANNEL-COUNT"]}ch`);
|
||||||
if (quality['BIT-RATE']) parts.push(`${Math.floor(Number(quality['BIT-RATE']) / 1000)}kbps`);
|
if (quality["BIT-RATE"]) parts.push(`${Math.floor(Number(quality["BIT-RATE"]) / 1000)}kbps`);
|
||||||
if (quality['BIT-DEPTH']) parts.push(`${quality['BIT-DEPTH']}bit`);
|
if (quality["BIT-DEPTH"]) parts.push(`${quality["BIT-DEPTH"]}bit`);
|
||||||
if (quality['SAMPLE-RATE']) parts.push(`${Math.floor(Number(quality['SAMPLE-RATE']) / 1000)}kHz`);
|
if (quality["SAMPLE-RATE"]) parts.push(`${Math.floor(Number(quality["SAMPLE-RATE"]) / 1000)}kHz`);
|
||||||
if (quality['CHANNEL-USAGE']) parts.push(quality['CHANNEL-USAGE'].toLowerCase());
|
if (quality["CHANNEL-USAGE"]) parts.push(quality["CHANNEL-USAGE"].toLowerCase());
|
||||||
if (quality['IS-ATMOS']) parts.push('atmos');
|
if (quality["IS-ATMOS"]) parts.push("atmos");
|
||||||
if (quality['__ACTUAL__']) parts.push('[ACTUAL]');
|
if (quality["__ACTUAL__"]) parts.push("[ACTUAL]");
|
||||||
|
|
||||||
return parts.join(' ');
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRealAlacQuality(manifestUrl: string, qualities: Quality[]): Promise<Quality | null> {
|
async function fetchRealAlacQuality(manifestUrl: string, qualities: Quality[]): Promise<Quality | null> {
|
||||||
const baseUrl = manifestUrl.split('/').slice(0, -1).join('/');
|
const baseUrl = manifestUrl.split("/").slice(0, -1).join("/");
|
||||||
const firstSegmentUrl = qualities.filter(quality => quality['AUDIO-FORMAT-ID'] === 'alac' && parseInt(quality['CHANNEL-COUNT']) <= 2).at(-1)?.['FIRST-SEGMENT-URI'];
|
|
||||||
|
// Only fetch the lowest quality 2 channel ALAC track.
|
||||||
|
const firstSegmentUrl = qualities.filter(quality => quality["AUDIO-FORMAT-ID"] === "alac" && parseInt(quality["CHANNEL-COUNT"]) <= 2).at(-1)?.["FIRST-SEGMENT-URI"];
|
||||||
if (!firstSegmentUrl) return null;
|
if (!firstSegmentUrl) return null;
|
||||||
|
|
||||||
const res = await (await fetchCors(`${baseUrl}/${firstSegmentUrl}`, {
|
const res = await fetchCors(`${baseUrl}/${firstSegmentUrl}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Range': 'bytes=0-16384'
|
"Range": "bytes=0-16384"
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
|
||||||
const view = new DataView(await res.arrayBuffer());
|
const view = new DataView(await res.arrayBuffer());
|
||||||
|
|
||||||
@@ -154,12 +157,12 @@ async function fetchRealAlacQuality(manifestUrl: string, qualities: Quality[]):
|
|||||||
pos += 8 + 28; // Move inside atom.
|
pos += 8 + 28; // Move inside atom.
|
||||||
case 0x61_6C_61_63: // alac
|
case 0x61_6C_61_63: // alac
|
||||||
return {
|
return {
|
||||||
'FIRST-SEGMENT-URI': firstSegmentUrl,
|
"FIRST-SEGMENT-URI": firstSegmentUrl,
|
||||||
'AUDIO-FORMAT-ID': 'alac',
|
"AUDIO-FORMAT-ID": "alac",
|
||||||
'CHANNEL-COUNT': view.getUint8(pos + 8 + 13).toString(),
|
"CHANNEL-COUNT": view.getUint8(pos + 8 + 13).toString(),
|
||||||
'BIT-DEPTH': view.getUint8(pos + 8 + 9),
|
"BIT-DEPTH": view.getUint8(pos + 8 + 9),
|
||||||
'SAMPLE-RATE': view.getInt32(pos + 8 + 24),
|
"SAMPLE-RATE": view.getInt32(pos + 8 + 24),
|
||||||
'__ACTUAL__': true
|
"__ACTUAL__": true
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
pos += atomSize;
|
pos += atomSize;
|
||||||
|
|||||||
114
src/applemusic/modules/storefronts.ts
Normal file
114
src/applemusic/modules/storefronts.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { sleep } from "../../common/misc";
|
||||||
|
import { fromHTML, observeSelector } from "../../common/dom";
|
||||||
|
import flagIcon from "../assets/icons/flag.svg?raw";
|
||||||
|
import { offAlbumRoute, onAlbumRoute } from "../glue/router";
|
||||||
|
import { createButtonElement, hideSidebarButton, showSidebarButton } from "../glue/sidebar";
|
||||||
|
import { getAlbum, getPageAlbumId } from "../services/album";
|
||||||
|
import { getStorefronts } from "../services/storefront";
|
||||||
|
|
||||||
|
const PREFERRED_STOREFRONTS = [ "jp", "kr", "us", "nz", "au", "de", "fr", "gb", "in", "it", "es", "br", "cn", "hk" ];
|
||||||
|
PREFERRED_STOREFRONTS.reverse();
|
||||||
|
|
||||||
|
let globalJob: AbortController | null = null;
|
||||||
|
|
||||||
|
const checkStorefrontsButtonEl = createButtonElement("Check Storefronts", flagIcon);
|
||||||
|
|
||||||
|
checkStorefrontsButtonEl.addEventListener("click", async () => {
|
||||||
|
// Only allow when album page loaded successfully.
|
||||||
|
const refEl = document.querySelector<HTMLElement>(".section");
|
||||||
|
if (refEl) await checkCountries(refEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
onAlbumRoute(async () => {
|
||||||
|
const errorEl = await observeSelector(".page-error");
|
||||||
|
if (errorEl) await checkCountries(errorEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
onAlbumRoute(() => {
|
||||||
|
showSidebarButton(checkStorefrontsButtonEl, 100);
|
||||||
|
globalJob?.abort();
|
||||||
|
globalJob = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
offAlbumRoute(() => {
|
||||||
|
hideSidebarButton(checkStorefrontsButtonEl);
|
||||||
|
globalJob?.abort();
|
||||||
|
globalJob = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function checkCountries(refEl: HTMLElement) {
|
||||||
|
if (globalJob) return;
|
||||||
|
const job = new AbortController();
|
||||||
|
globalJob = job;
|
||||||
|
|
||||||
|
const albumId = getPageAlbumId();
|
||||||
|
|
||||||
|
const headerEl = fromHTML(`<div class="section ame-album-storefronts-header">Availability in the following storefronts:</div>`);
|
||||||
|
const containerEl = fromHTML(`
|
||||||
|
<div class="section ame-album-storefronts-container">
|
||||||
|
<div class="ame-color-primary"></div>
|
||||||
|
<div class="ame-color-secondary"></div>
|
||||||
|
<div class="ame-color-tertiary"></div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const primaryContainerEl = containerEl.children[0] as HTMLElement;
|
||||||
|
const secondaryContainerEl = containerEl.children[1] as HTMLElement;
|
||||||
|
const tertiaryContainerEl = containerEl.children[2] as HTMLElement;
|
||||||
|
|
||||||
|
refEl.append(headerEl);
|
||||||
|
refEl.append(containerEl);
|
||||||
|
|
||||||
|
let storefronts = await getStorefronts();
|
||||||
|
|
||||||
|
storefronts = storefronts
|
||||||
|
.map(storefront => {
|
||||||
|
storefront.attributes.name = storefront.attributes.name.split(", ").slice(0, 1).join(" ");
|
||||||
|
return storefront;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
return Math.max(PREFERRED_STOREFRONTS.indexOf(b.id), 0) - Math.max(PREFERRED_STOREFRONTS.indexOf(a.id), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const storefront of storefronts) {
|
||||||
|
if (job.signal.aborted) break;
|
||||||
|
|
||||||
|
const album = await getAlbum(albumId, storefront.id);
|
||||||
|
|
||||||
|
// Album totally unavailable.
|
||||||
|
if (!album) {
|
||||||
|
tertiaryContainerEl.append(fromHTML(`<span data-storefront="${storefront.id}" title="Totally unavailable">${storefront.attributes.name}, </span>`));
|
||||||
|
await sleep(250);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discCount = Math.max(...album.relationships.tracks.data.map(track => track.attributes.discNumber));
|
||||||
|
const songTracks = album.relationships.tracks.data.filter(track => track.type === "songs");
|
||||||
|
const otherTracks = album.relationships.tracks.data.filter(track => track.type !== "songs");
|
||||||
|
const unavailableTracks = new Set();
|
||||||
|
for (let i = 1; i <= album.attributes.trackCount - otherTracks.length; i++) unavailableTracks.add(i);
|
||||||
|
|
||||||
|
songTracks.forEach((track, i) => {
|
||||||
|
if (!track.attributes.extendedAssetUrls) return;
|
||||||
|
if (!track.attributes.playParams) return;
|
||||||
|
unavailableTracks.delete(discCount > 1 ? i + 1 : track.attributes.trackNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Album partially available.
|
||||||
|
if (unavailableTracks.size) {
|
||||||
|
const missingText = discCount > 1
|
||||||
|
? `${unavailableTracks.size} tracks`
|
||||||
|
: Array.from(unavailableTracks).join(", ");
|
||||||
|
|
||||||
|
secondaryContainerEl.append(fromHTML(`<span data-storefront="${storefront.id}"><a target="_blank" href="https://music.apple.com/${storefront.id}/album/${albumId}" title="Partially available, missing:\n${missingText}">${storefront.attributes.name}</a>, </span>`));
|
||||||
|
|
||||||
|
await sleep(250);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Album fully available.
|
||||||
|
primaryContainerEl.append(fromHTML(`<span data-storefront="${storefront.id}"><a target="_blank" href="https://music.apple.com/${storefront.id}/album/${albumId}" title="Fully available">${storefront.attributes.name}</a>, </span>`));
|
||||||
|
|
||||||
|
await sleep(250);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
import { fetchCors } from '../../common';
|
import { fetchCors } from "../../common/fetch";
|
||||||
import { Album, ApiResponse, Resource } from '../types';
|
import { Album, ApiResponse, Resource } from "../types";
|
||||||
import { getAuthToken } from './auth';
|
import { getAuthToken } from "./auth";
|
||||||
|
import { getPageStorefront } from "./storefront";
|
||||||
|
|
||||||
|
export function getPageAlbumId(): string {
|
||||||
|
return location.pathname.split("/")[4];
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAlbum(id: string, storefront?: string): Promise<Resource<Album> | null> {
|
export async function getAlbum(id: string, storefront?: string): Promise<Resource<Album> | null> {
|
||||||
storefront ??= location.pathname.split('/')[1];
|
storefront ??= getPageStorefront();
|
||||||
|
|
||||||
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/albums/${id}?extend=extendedAssetUrls`, {
|
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/albums/${id}?extend=extendedAssetUrls`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Origin': 'https://music.apple.com',
|
"Origin": "https://music.apple.com",
|
||||||
'Referer': 'https://music.apple.com/',
|
"Referer": "https://music.apple.com/",
|
||||||
'Authorization': `Bearer ${await getAuthToken()}`
|
"Authorization": `Bearer ${await getAuthToken()}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 404) return null;
|
if (res.status === 404) return null;
|
||||||
|
|
||||||
const albums = await res.json<ApiResponse<Album>>();
|
const albums = await res.json<ApiResponse<Album>>();
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { fetchCors, readCookies } from '../../common';
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
import { readCookies } from "../../common/misc";
|
||||||
|
|
||||||
let cachedAuthToken = '';
|
let cachedAuthToken = "";
|
||||||
|
|
||||||
export async function getAuthToken(): Promise<string> {
|
export async function getAuthToken(): Promise<string> {
|
||||||
if (cachedAuthToken) return cachedAuthToken;
|
if (cachedAuthToken) return cachedAuthToken;
|
||||||
|
|
||||||
const scriptEl = document.querySelector<HTMLScriptElement>('script[type="module"]');
|
const scriptEl = document.querySelector<HTMLScriptElement>("script[type='module']");
|
||||||
if (!scriptEl) throw new Error('Failed to find script with auth token.');
|
if (!scriptEl) throw new Error("Failed to find script with auth token.");
|
||||||
|
|
||||||
const res = await fetchCors(scriptEl.src);
|
const res = await fetchCors(scriptEl.src);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
|
|
||||||
const match = body.match(/(?<=")eyJhbGciOiJ.+?(?=")/);
|
const match = body.match(/(?<=")eyJhbGciOiJ.+?(?=")/);
|
||||||
if (!match) throw new Error('Failed to find auth token from script.');
|
if (!match) throw new Error("Failed to find auth token from script.");
|
||||||
|
|
||||||
cachedAuthToken = match[0];
|
cachedAuthToken = match[0];
|
||||||
return cachedAuthToken;
|
return cachedAuthToken;
|
||||||
@@ -22,8 +23,3 @@ export function getUserToken(): string | null {
|
|||||||
const cookies = readCookies();
|
const cookies = readCookies();
|
||||||
return cookies["music-user-token"] || null;
|
return cookies["music-user-token"] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccountStorefront(): string | null {
|
|
||||||
const cookies = readCookies();
|
|
||||||
return cookies["itua"] || null;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { fetchCors } from '../../common';
|
|
||||||
import { ApiResponse, Resource, Storefront } from '../types';
|
|
||||||
import { getAuthToken } from './auth';
|
|
||||||
|
|
||||||
export * from './album';
|
|
||||||
export * from './auth';
|
|
||||||
|
|
||||||
export async function getStorefronts(): Promise<Resource<Storefront>[]> {
|
|
||||||
const res = await fetchCors('https://api.music.apple.com/v1/storefronts', {
|
|
||||||
headers: {
|
|
||||||
'Origin': 'https://music.apple.com',
|
|
||||||
'Referer': 'https://music.apple.com/',
|
|
||||||
'Authorization': `Bearer ${await getAuthToken()}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = await res.json<ApiResponse<Storefront>>();
|
|
||||||
return body.data;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { fetchCors } from "../../common";
|
import { fetchCors } from "../../common/fetch";
|
||||||
import { ApiResponse, Lyrics, Resource } from "../types";
|
import { ApiResponse, Lyrics, Resource } from "../types";
|
||||||
import { getAuthToken } from "./auth";
|
import { getAuthToken } from "./auth";
|
||||||
|
import { getPageStorefront } from "./storefront";
|
||||||
|
|
||||||
export async function getLyrics(id: string, syllable: boolean, storefront?: string): Promise<Resource<Lyrics> | null> {
|
export async function getLyrics(id: string, syllable: boolean, storefront?: string): Promise<Resource<Lyrics> | null> {
|
||||||
storefront ??= location.pathname.split("/")[1];
|
storefront ??= getPageStorefront();
|
||||||
|
|
||||||
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/songs/${id}/${syllable ? "syllable-lyrics" : "lyrics"}`, {
|
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/songs/${id}/${syllable ? "syllable-lyrics" : "lyrics"}`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -12,7 +13,6 @@ export async function getLyrics(id: string, syllable: boolean, storefront?: stri
|
|||||||
"Authorization": `Bearer ${await getAuthToken()}`
|
"Authorization": `Bearer ${await getAuthToken()}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 404) return null;
|
if (res.status === 404) return null;
|
||||||
|
|
||||||
const lyrics = await res.json<ApiResponse<Lyrics>>();
|
const lyrics = await res.json<ApiResponse<Lyrics>>();
|
||||||
|
|||||||
25
src/applemusic/services/storefront.ts
Normal file
25
src/applemusic/services/storefront.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
import { readCookies } from "../../common/misc";
|
||||||
|
import { ApiResponse, Resource, Storefront } from "../types";
|
||||||
|
import { getAuthToken } from "./auth";
|
||||||
|
|
||||||
|
export function getPageStorefront(): string {
|
||||||
|
return location.pathname.split("/")[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountStorefront(): string | null {
|
||||||
|
const cookies = readCookies();
|
||||||
|
return cookies["itua"] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStorefronts(): Promise<Resource<Storefront>[]> {
|
||||||
|
const res = await fetchCors("https://api.music.apple.com/v1/storefronts", {
|
||||||
|
headers: {
|
||||||
|
"Origin": "https://music.apple.com",
|
||||||
|
"Referer": "https://music.apple.com/",
|
||||||
|
"Authorization": `Bearer ${await getAuthToken()}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const body = await res.json<ApiResponse<Storefront>>();
|
||||||
|
return body.data;
|
||||||
|
}
|
||||||
@@ -28,16 +28,35 @@
|
|||||||
|
|
||||||
/* Hide Open in Music button in the sidebar. */
|
/* Hide Open in Music button in the sidebar. */
|
||||||
|
|
||||||
.navigation__scrollable-container + .navigation__native-cta {
|
.navigation__native-cta {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for sidebar buttons. */
|
/* Fix artwork breaking when overflown metadata exceeds its height. */
|
||||||
|
|
||||||
.navigation-items[data-ame] {
|
div[slot="artwork"] {
|
||||||
padding-top: 9px;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add more room for error messages and button text on error pages. */
|
||||||
|
|
||||||
|
.page-error {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 960px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make release metadata selectable. */
|
||||||
|
|
||||||
|
.headings__title, /* Release title */
|
||||||
|
.headings__subtitles *, /* Release artists */
|
||||||
|
.description * { /* Release description */
|
||||||
|
user-select: text;
|
||||||
|
-moz-user-select: text;
|
||||||
|
-webkit-user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles for sidebar buttons. */
|
||||||
|
|
||||||
.navigation-items__header[data-ame] {
|
.navigation-items__header[data-ame] {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
@@ -61,8 +80,8 @@
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
position: relative;
|
position: relative;
|
||||||
--linkHoverTextDecoration: none;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
--linkHoverTextDecoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-item__link[data-ame] {
|
.navigation-item__link[data-ame] {
|
||||||
@@ -92,123 +111,40 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sidebar buttons stick to the top and stack on top of each other. */
|
/* Styles for release quality badges. */
|
||||||
|
|
||||||
nav {
|
|
||||||
padding-bottom: .5em;
|
|
||||||
grid-template-rows: min-content min-content minmax(0, min-content) min-content min-content min-content min-content min-content min-content min-content min-content !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation__scrollable-container {
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation__native-cta {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add focus styles to sidebar buttons. */
|
|
||||||
|
|
||||||
.native-cta {
|
|
||||||
padding-top: 8px !important;
|
|
||||||
padding-bottom: 8px !important;
|
|
||||||
border-top: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.native-cta__button svg,
|
|
||||||
.native-cta__button .native-cta__label {
|
|
||||||
transition: 50ms linear color, 50ms linear fill;
|
|
||||||
}
|
|
||||||
|
|
||||||
.native-cta__button:active svg,
|
|
||||||
.native-cta__button:active .native-cta__label {
|
|
||||||
color: white !important;
|
|
||||||
fill: white !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make more room for error messages and button text. */
|
|
||||||
|
|
||||||
.page-error {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 900px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-error__title + .button button {
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Fix artwork breaking when overflown metadata exceeds its height. */
|
|
||||||
|
|
||||||
div[slot="artwork"] {
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make album metadata selectable */
|
|
||||||
|
|
||||||
.container-detail-header,
|
|
||||||
.headings,
|
|
||||||
.headings *,
|
|
||||||
.headings__subtitles *,
|
|
||||||
dialog *,
|
|
||||||
.section-content,
|
|
||||||
.tracklist-footer,
|
|
||||||
.tracklist-footer * {
|
|
||||||
user-select: text !important;
|
|
||||||
-moz-user-select: text !important;
|
|
||||||
-webkit-user-select: text !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headings__metadata-bottom {
|
|
||||||
user-select: none !important;
|
|
||||||
-moz-user-select: none !important;
|
|
||||||
-webkit-user-select: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.songs-list-row__song-name {
|
|
||||||
width: min-content;
|
|
||||||
user-select: all !important;
|
|
||||||
-moz-user-select: all !important;
|
|
||||||
-webkit-user-select: all !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for album quality badges. */
|
|
||||||
|
|
||||||
.headings__metadata:last-child {
|
|
||||||
margin-bottom: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ame-album-badges-container > svg {
|
.ame-album-badges-container > svg {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
fill: #999999;
|
fill: var(--systemSecondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for album country availabilities. */
|
/* Styles for release country availabilities. */
|
||||||
|
|
||||||
.ame-album-countries-header {
|
.ame-album-storefronts-header {
|
||||||
margin: 0 var(--bodyGutter);
|
margin: 0 var(--bodyGutter);
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ame-album-countries-container {
|
.ame-album-storefronts-container {
|
||||||
|
display: flex;
|
||||||
margin: 0 var(--bodyGutter);
|
margin: 0 var(--bodyGutter);
|
||||||
margin-bottom: var(--bodyGutter);
|
margin-bottom: var(--bodyGutter);
|
||||||
padding: 1em 0;
|
padding-top: 1em;
|
||||||
line-height: 2.2;
|
line-height: 2.2;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ame-album-storefronts-container > div:not(:last-child):not(:empty) {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ame-album-storefronts-container * {
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ame-album-countries-container * {
|
/* Styles for info table. */
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ame-album-countries-container div:not(:empty) {
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for docked info table. */
|
|
||||||
|
|
||||||
.ame-table-band {
|
.ame-table-band {
|
||||||
background-color: #eee !important;
|
background-color: #eee !important;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export interface Track {
|
|||||||
composerName: string;
|
composerName: string;
|
||||||
audioLocale: string;
|
audioLocale: string;
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
playParams: object | null;
|
playParams?: object;
|
||||||
hasLyrics: boolean;
|
hasLyrics: boolean;
|
||||||
durationInMillis: number;
|
durationInMillis: number;
|
||||||
genreNames: string[];
|
genreNames: string[];
|
||||||
|
|||||||
53
src/common/dom.ts
Normal file
53
src/common/dom.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
export function fromHTML<T extends HTMLElement>(value: string): T {
|
||||||
|
const el = document.createElement("template");
|
||||||
|
el.innerHTML = value;
|
||||||
|
return el.content.firstElementChild as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaitForOptions {
|
||||||
|
waitSelector?: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function observeSelector<T extends HTMLElement>(selector: string, options?: WaitForOptions): Promise<T | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const waitSelector = options?.waitSelector;
|
||||||
|
const timeout = options?.timeout ?? 3000;
|
||||||
|
|
||||||
|
if (timeout !== 0) {
|
||||||
|
const el = document.querySelector<T>(selector);
|
||||||
|
if (el) {
|
||||||
|
resolve(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disposeTimeout = setTimeout(() => {
|
||||||
|
if (timeout === 0) return;
|
||||||
|
observer.disconnect();
|
||||||
|
resolve(null);
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of Array.from(mutation.addedNodes)) {
|
||||||
|
if (!(node instanceof Element)) continue;
|
||||||
|
if (!node.matches(waitSelector ?? selector)) continue;
|
||||||
|
|
||||||
|
if (timeout !== 0) {
|
||||||
|
observer.disconnect();
|
||||||
|
clearTimeout(disposeTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(waitSelector ? document.querySelector<T>(selector) : node as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -9,20 +9,20 @@ export function fetchCors(url: string, init?: RequestInit): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GM.xmlHttpRequest({
|
GM.xmlHttpRequest({
|
||||||
method: init?.method ?? 'GET' as any,
|
url,
|
||||||
url: url,
|
method: init?.method ?? ("GET" as any),
|
||||||
headers: Object.fromEntries(new Headers(init?.headers) as any),
|
headers: Object.fromEntries(new Headers(init?.headers)),
|
||||||
responseType: 'blob',
|
responseType: "blob",
|
||||||
onload(res) {
|
onload(res) {
|
||||||
if (res.status !== 404 && (res.status < 200 || res.status > 299)) {
|
if ((res.status < 200 || res.status > 299) && res.status !== 404) {
|
||||||
reject(new Error(`Fetching "${url}" responded with an erroneous status code.`));
|
reject(new Error(`Fetching "${url}" responded with an erroneous status code.`));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = res.responseHeaders
|
const headers = res.responseHeaders
|
||||||
.split('\r\n')
|
.split("\r\n")
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.map(line => line.split(': '));
|
.map(line => line.split(": "));
|
||||||
|
|
||||||
const fetchRes = new Response(res.response, {
|
const fetchRes = new Response(res.response, {
|
||||||
headers: Object.fromEntries(headers),
|
headers: Object.fromEntries(headers),
|
||||||
@@ -30,16 +30,16 @@ export function fetchCors(url: string, init?: RequestInit): Promise<Response> {
|
|||||||
statusText: res.statusText
|
statusText: res.statusText
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(fetchRes, 'url', { value: url });
|
Object.defineProperty(fetchRes, "url", { value: url });
|
||||||
|
|
||||||
cache.set(url, fetchRes.clone());
|
cache.set(url, fetchRes.clone());
|
||||||
resolve(fetchRes);
|
resolve(fetchRes);
|
||||||
},
|
},
|
||||||
onerror() {
|
onerror() {
|
||||||
reject(new TypeError('Network request errored.'));
|
reject(new Error("Network request errored."));
|
||||||
},
|
},
|
||||||
ontimeout() {
|
ontimeout() {
|
||||||
reject(new TypeError('Network request timed out.'));
|
reject(new Error("Network request timed out."));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
export * from './fetch';
|
|
||||||
export * from './router';
|
|
||||||
|
|
||||||
export function readCookies(): Record<string, string> {
|
|
||||||
return Object.fromEntries(document.cookie.split("; ").map(cookie => cookie.split("=", 2)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sleep(delay: number): Promise<void> {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
setTimeout(resolve, delay);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function downloadFile(data: Blob, filename: string) {
|
|
||||||
const url = URL.createObjectURL(data);
|
|
||||||
const el = document.createElement('a');
|
|
||||||
el.style.display = 'none';
|
|
||||||
el.download = filename;
|
|
||||||
el.href = url;
|
|
||||||
document.body.appendChild(el);
|
|
||||||
el.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fromHTML<T extends HTMLElement>(html: string): T {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = html;
|
|
||||||
return div.firstElementChild as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function waitFor<T extends HTMLElement>(selector: string, waitSelector?: string, timeout: number = 5000, refEl: HTMLElement | ShadowRoot | Document = document): Promise<T | null> {
|
|
||||||
return new Promise<T | null>((resolve) => {
|
|
||||||
let disposeTimeout = 0;
|
|
||||||
let disposeInterval = 0;
|
|
||||||
|
|
||||||
disposeTimeout = setTimeout(() => {
|
|
||||||
clearInterval(disposeInterval);
|
|
||||||
resolve(null);
|
|
||||||
}, timeout) as any;
|
|
||||||
|
|
||||||
disposeInterval = setInterval(() => {
|
|
||||||
let el = refEl.querySelector<T>(waitSelector ?? selector);
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
if (waitSelector) {
|
|
||||||
el = refEl.querySelector<T>(selector);
|
|
||||||
if (!el) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(el);
|
|
||||||
clearTimeout(disposeTimeout);
|
|
||||||
clearInterval(disposeInterval);
|
|
||||||
}, 10) as any;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function observe<T extends HTMLElement>(selector: string, cb: (el: T) => any): void {
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
for (const node of Array.from(mutation.addedNodes)) {
|
|
||||||
if (!(node instanceof Element)) continue;
|
|
||||||
if (!node.matches(selector)) continue;
|
|
||||||
|
|
||||||
cb(node as T);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
38
src/common/menu.ts
Normal file
38
src/common/menu.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
interface MenuElement extends HTMLElement {
|
||||||
|
addMenuItem<TElement extends HTMLElement>(el: TElement, index: number): TElement
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureMenu(selector: string, cb: () => void): MenuElement {
|
||||||
|
let refEl = document.querySelector<MenuElement>(selector);
|
||||||
|
if (refEl) return createMenu(refEl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch {
|
||||||
|
console.error(`Could not create menu reference element for selector "${selector}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
refEl = document.querySelector<MenuElement>(selector);
|
||||||
|
if (refEl) return createMenu(refEl);
|
||||||
|
|
||||||
|
throw new Error(`Could not find menu reference element by selector "${selector}".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu(menuEl: HTMLElement & MenuElement): MenuElement {
|
||||||
|
menuEl.addMenuItem = (el, index) => {
|
||||||
|
let refEl: HTMLElement = menuEl;
|
||||||
|
|
||||||
|
for (let limit = 0; limit < 100; limit++) {
|
||||||
|
const nextRefEl = refEl.nextElementSibling as HTMLElement;
|
||||||
|
if (!nextRefEl) break;
|
||||||
|
if (Number(nextRefEl.getAttribute("data-index")) > index) break;
|
||||||
|
refEl = nextRefEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.setAttribute("data-index", index.toString());
|
||||||
|
refEl.after(el);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
return menuEl;
|
||||||
|
}
|
||||||
31
src/common/misc.ts
Normal file
31
src/common/misc.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export function readCookies(): Record<string, string> {
|
||||||
|
return Object.fromEntries(document.cookie.split("; ").map(cookie => cookie.split("=", 2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(delay: number): Promise<void> {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
setTimeout(resolve, delay);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadFile(data: Blob, filename: string): void {
|
||||||
|
const url = URL.createObjectURL(data);
|
||||||
|
const el = document.createElement("a");
|
||||||
|
el.style.display = "none";
|
||||||
|
el.download = filename;
|
||||||
|
el.href = url;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parallelMap<TInput, TOutput>(values: TInput[], workers: number, fn: (value: TInput) => Promise<TOutput>): Promise<TOutput[]> {
|
||||||
|
const results: TOutput[] = new Array(values.length);
|
||||||
|
const entries = values.entries();
|
||||||
|
|
||||||
|
await Promise.all(Array.from({ length: Math.min(values.length, workers) }, async () => {
|
||||||
|
for (const [ index, value ] of entries) results[index] = await fn(value);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
@@ -1,48 +1,32 @@
|
|||||||
interface CloneIntoOptions {
|
declare global {
|
||||||
|
|
||||||
cloneFunctions?: boolean;
|
interface Window {
|
||||||
|
wrappedJSObject: Window | undefined;
|
||||||
|
Promise: typeof Promise;
|
||||||
|
}
|
||||||
|
|
||||||
wrapReflectors?: boolean;
|
const cloneInto: CloneIntoFunction<object> | undefined;
|
||||||
|
const exportFunction: ExportFunctionFunction<Function> | undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CloneIntoOptions {
|
||||||
|
cloneFunctions?: boolean;
|
||||||
|
wrapReflectors?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface ExportFunctionOptions {
|
interface ExportFunctionOptions {
|
||||||
|
|
||||||
defineAs?: string;
|
defineAs?: string;
|
||||||
|
|
||||||
allowCrossOriginArguments?: boolean;
|
allowCrossOriginArguments?: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CloneIntoFunction<T extends object> = (obj: T, targetScope: object, options?: CloneIntoOptions) => T;
|
type CloneIntoFunction<T extends object> = (obj: T, targetScope: object, options?: CloneIntoOptions) => T;
|
||||||
type ExportFunctionFunction<T extends Function> = (fn: T, targetScope: object, options?: ExportFunctionOptions) => T;
|
type ExportFunctionFunction<T extends Function> = (fn: T, targetScope: object, options?: ExportFunctionOptions) => T;
|
||||||
|
|
||||||
declare global {
|
export const imposedWindow = unsafeWindow?.wrappedJSObject ?? unsafeWindow;
|
||||||
|
|
||||||
interface Window {
|
const cloneIntoImpl: CloneIntoFunction<any> = typeof cloneInto === "function" ? cloneInto : (obj: object) => obj;
|
||||||
|
const exportFunctionImpl: ExportFunctionFunction<any> = typeof exportFunction === "function" ? exportFunction : (fn: Function) => fn;
|
||||||
wrappedJSObject: Window | undefined;
|
|
||||||
|
|
||||||
Promise: typeof Promise;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const cloneInto: CloneIntoFunction<object> | undefined;
|
|
||||||
|
|
||||||
const exportFunction: ExportFunctionFunction<Function> | undefined;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export let imposedWindow = unsafeWindow;
|
|
||||||
if (imposedWindow.wrappedJSObject) imposedWindow = imposedWindow.wrappedJSObject;
|
|
||||||
|
|
||||||
const cloneIntoImpl: CloneIntoFunction<any> = typeof cloneInto === 'function'
|
|
||||||
? cloneInto
|
|
||||||
: (obj: object) => obj;
|
|
||||||
|
|
||||||
const exportFunctionImpl: ExportFunctionFunction<any> = typeof exportFunction === 'function'
|
|
||||||
? exportFunction
|
|
||||||
: (fn: Function) => fn;
|
|
||||||
|
|
||||||
export function exposeObject<T extends object>(obj: T): T {
|
export function exposeObject<T extends object>(obj: T): T {
|
||||||
return cloneIntoImpl(obj, imposedWindow, {
|
return cloneIntoImpl(obj, imposedWindow, {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { match, MatchFunction } from "path-to-regexp";
|
|
||||||
import { exposeFunction, imposedWindow, imposeFunction } from "./patch";
|
import { exposeFunction, imposedWindow, imposeFunction } from "./patch";
|
||||||
|
|
||||||
export type RouteCallback = () => any;
|
export type RouteCallback = () => void;
|
||||||
|
|
||||||
interface Route {
|
interface Route {
|
||||||
pattern: string;
|
pattern: RegExp;
|
||||||
matcher: MatchFunction;
|
|
||||||
onCallbacks: RouteCallback[];
|
onCallbacks: RouteCallback[];
|
||||||
offCallbacks: RouteCallback[];
|
offCallbacks: RouteCallback[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const registeredRoutes: Route[] = [];
|
const registeredRoutes: Record<string, Route> = {};
|
||||||
|
|
||||||
const imposedPushState = imposeFunction(imposedWindow.history.pushState, imposedWindow.history);
|
const imposedPushState = imposeFunction(imposedWindow.history.pushState, imposedWindow.history);
|
||||||
imposedWindow.history.pushState = exposeFunction(proxyPushState);
|
imposedWindow.history.pushState = exposeFunction(proxyPushState);
|
||||||
@@ -25,39 +23,38 @@ addEventListener("popstate", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function onNavigate() {
|
function onNavigate() {
|
||||||
for (const route of registeredRoutes) {
|
for (const route of Object.values(registeredRoutes)) {
|
||||||
const cbs = route.matcher(location.pathname) ? route.onCallbacks : route.offCallbacks;
|
const cbs = route.pattern.test(location.pathname) ? route.onCallbacks : route.offCallbacks;
|
||||||
for (const cb of cbs) cb();
|
for (const cb of cbs) cb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureRoute(pattern: string): Route {
|
function ensureRoute(pattern: string): Route {
|
||||||
let route = registeredRoutes.find(route => route.pattern === pattern);
|
const compiledPattern = new RegExp(`^/${pattern.replaceAll("/", "\\/")}$`);
|
||||||
|
|
||||||
|
let route = registeredRoutes[pattern];
|
||||||
if (route) return route;
|
if (route) return route;
|
||||||
|
|
||||||
route = {
|
route = {
|
||||||
pattern,
|
pattern: compiledPattern,
|
||||||
matcher: match(pattern),
|
|
||||||
onCallbacks: [],
|
onCallbacks: [],
|
||||||
offCallbacks: []
|
offCallbacks: []
|
||||||
};
|
};
|
||||||
|
|
||||||
registeredRoutes.push(route);
|
registeredRoutes[pattern] = route;
|
||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onRoute(pattern: string, cb: RouteCallback, once = false): void {
|
export function onRoute(pattern: string, cb: RouteCallback): void {
|
||||||
const route = ensureRoute(pattern);
|
const route = ensureRoute(pattern);
|
||||||
const match = route.matcher(location.pathname);
|
const match = route.pattern.test(location.pathname);
|
||||||
|
|
||||||
route.onCallbacks.push(cb);
|
route.onCallbacks.push(cb);
|
||||||
if (match) cb();
|
if (match) cb();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function offRoute(pattern: string, cb: RouteCallback, once = false): void {
|
export function offRoute(pattern: string, cb: RouteCallback): void {
|
||||||
const route = ensureRoute(pattern);
|
const route = ensureRoute(pattern);
|
||||||
const match = route.matcher(location.pathname);
|
const match = route.pattern.test(location.pathname);
|
||||||
|
|
||||||
route.offCallbacks.push(cb);
|
route.offCallbacks.push(cb);
|
||||||
if (!match) cb();
|
if (!match) cb();
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/musicbrainz/assets/icons/mhcovers.svg
Normal file
4
src/musicbrainz/assets/icons/mhcovers.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
|
<!--! Font Awesome Pro 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. -->
|
||||||
|
<path fill="#dedede" d="M512 255.1c0 1.8-.9 2.7-.9 3.6.5 36.5-32.7 60.4-69.2 60.4H344c-26.5 0-48 22.4-48 48.9 0 3.4.4 6.7 1 9.9 2.2 10.2 6.5 19.2 10.9 29.9 6 13.8 12.1 27.5 12.1 42 0 31.9-21.6 60.7-53.4 62-3.5.1-7.1.2-10.6.2C114.6 512 0 397.4 0 256S114.6 0 256 0s256 114.6 256 256v-.9zm-416 0c-17.67 0-32 15.2-32 32 0 18.6 14.33 32 32 32 17.7 0 32-13.4 32-32 0-16.8-14.3-32-32-32zm32-64c17.7 0 32-13.4 32-32 0-16.8-14.3-32-32-32s-32 15.2-32 32c0 18.6 14.3 32 32 32zm128-128c-17.7 0-32 15.23-32 32 0 18.6 14.3 32 32 32s32-13.4 32-32c0-16.77-14.3-32-32-32zm128 128c17.7 0 32-13.4 32-32 0-16.8-14.3-32-32-32s-32 15.2-32 32c0 18.6 14.3 32 32 32z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 887 B |
BIN
src/musicbrainz/assets/icons/ongakunomori.ico
Normal file
BIN
src/musicbrainz/assets/icons/ongakunomori.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
17
src/musicbrainz/glue/router.ts
Normal file
17
src/musicbrainz/glue/router.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { RouteCallback, onRoute } from "../../common/router";
|
||||||
|
|
||||||
|
const RELEASE_PATTERN = "release/[0-9a-f-]+";
|
||||||
|
const RELEASE_VIEW_COVER_PATTERN = "release/[0-9a-f-]+/cover-art";
|
||||||
|
const RELEASE_ADD_COVER_PATTERN = "release/[0-9a-f-]+/add-cover-art";
|
||||||
|
|
||||||
|
export function onReleaseRoute(cb: RouteCallback) {
|
||||||
|
onRoute(RELEASE_PATTERN, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onReleaseViewCoverRoute(cb: RouteCallback) {
|
||||||
|
onRoute(RELEASE_VIEW_COVER_PATTERN, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onReleaseAddCoverRoute(cb: RouteCallback) {
|
||||||
|
onRoute(RELEASE_ADD_COVER_PATTERN, cb);
|
||||||
|
}
|
||||||
29
src/musicbrainz/glue/sidebar.ts
Normal file
29
src/musicbrainz/glue/sidebar.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
import { ensureMenu } from "../../common/menu";
|
||||||
|
|
||||||
|
export function addReleaseSidebarButton(index: number, icon: string, title: string, link: string): HTMLElement {
|
||||||
|
return addSidebarButton(".release-information", index, icon, title, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSidebarButton(refSelector: string, index: number, icon: string, title: string, link: string): HTMLElement {
|
||||||
|
const menuEl = ensureMenu("#ame-sidebar", () => {
|
||||||
|
const refEl = document.querySelector<HTMLElement>(refSelector)!;
|
||||||
|
const sectionTitleEl = fromHTML(`<h2>Ame</h2>`);
|
||||||
|
const sectionEl = fromHTML(`
|
||||||
|
<ul class="external_links">
|
||||||
|
<li id="ame-sidebar" style="display: none;"></li>
|
||||||
|
</ul>
|
||||||
|
`);
|
||||||
|
|
||||||
|
refEl.before(sectionTitleEl);
|
||||||
|
refEl.before(sectionEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemEl = fromHTML(`
|
||||||
|
<li data-index="${index}" style="background: transparent url('${icon}') center left no-repeat; background-size: 16px 16px;">
|
||||||
|
<a href="${link}">${title}</a>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
|
||||||
|
return menuEl.addMenuItem(itemEl, 100);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @namespace ame-musicbrainz
|
// @namespace ame-musicbrainz
|
||||||
// @name Ame (MusicBrainz)
|
// @name Ame (MusicBrainz)
|
||||||
// @version 1.3.0
|
// @version 1.4.0
|
||||||
// @author SuperSaltyGamer
|
// @author SuperSaltyGamer
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// @match https://musicbrainz.org/*
|
// @match https://musicbrainz.org/*
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
|
|
||||||
import "./modules/search";
|
import "./modules/search";
|
||||||
import "./modules/covers";
|
import "./modules/covers";
|
||||||
|
import "./modules/scans";
|
||||||
|
import "./modules/related";
|
||||||
import styles from "./style.css?inline";
|
import styles from "./style.css?inline";
|
||||||
|
|
||||||
GM.addStyle(styles);
|
GM.addStyle(styles);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { html } from "lighterhtml";
|
import { fetchCors } from "../../common/fetch";
|
||||||
import { fetchCors } from "../../common";
|
import { fromHTML } from "../../common/dom";
|
||||||
|
import { onReleaseAddCoverRoute } from "../glue/router";
|
||||||
|
import { getPageReleaseInfo } from "../services/release";
|
||||||
|
|
||||||
interface CoverData {
|
interface CoverData {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -11,55 +13,35 @@ interface CoverData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const COVERS_URL = "https://covers.musichoarders.xyz";
|
onReleaseAddCoverRoute(() => {
|
||||||
|
|
||||||
const isReleasePage = location.pathname.startsWith("/release/");
|
|
||||||
const isCoverPage = location.pathname.endsWith("/add-cover-art");
|
|
||||||
|
|
||||||
if (isReleasePage && isCoverPage) { // When editing covers of a release.
|
|
||||||
function handleClick(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
openCovers(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const refEl = document.querySelector(".fileinput-button.buttons");
|
const refEl = document.querySelector(".fileinput-button.buttons");
|
||||||
refEl?.appendChild(html.node`
|
if (!refEl) return;
|
||||||
<button type="button" onclick="${handleClick}">Pick from MH Covers...</button>
|
|
||||||
`);
|
|
||||||
} else if (isReleasePage) { // When viewing a release.
|
|
||||||
function handleClick(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
openCovers(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const refEl = document.querySelector("#medium-toolbox");
|
const buttonEl = fromHTML(`<button type="button">Pick from MH Covers...</button>`);
|
||||||
if (refEl) {
|
buttonEl.onclick = openPicker;
|
||||||
setTimeout(() => {
|
|
||||||
refEl.insertAdjacentElement("afterbegin", html.node`
|
refEl.appendChild(buttonEl);
|
||||||
<button class="btn-link" type="button" onclick="${handleClick}">Search MH Covers</button>
|
});
|
||||||
`);
|
|
||||||
}, 100);
|
function openPicker(e: MouseEvent) {
|
||||||
}
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
const release = getPageReleaseInfo();
|
||||||
|
if (!release) return;
|
||||||
|
|
||||||
function openCovers(isEditing: boolean) {
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
const artist = document.querySelector<HTMLElement>(".subheader bdi")?.innerText ?? "";
|
params.set("artist", release.artist);
|
||||||
const album = document.querySelector<HTMLElement>("h1")?.innerText ?? "";
|
params.set("album", release.title);
|
||||||
params.set("artist", artist);
|
params.set("remote.port", "browser");
|
||||||
params.set("album", album);
|
params.set("remote.agent", "Ame - MusicBrainz");
|
||||||
if (isEditing) {
|
params.set("remote.text", "Pick cover for MusicBrainz release.");
|
||||||
params.set("remote.port", "browser");
|
|
||||||
params.set("remote.agent", "Ame - MusicBrainz");
|
|
||||||
params.set("remote.text", "Pick cover for MusicBrainz release.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const win = open(`${COVERS_URL}?${params}`, "_blank");
|
const win = open(`https://covers.musichoarders.xyz?${params}`, "_blank");
|
||||||
if (!win || !isEditing) return;
|
if (!win) return;
|
||||||
|
|
||||||
// Close covers window when the release page is closed.
|
// Close covers window when the release page is closed.
|
||||||
addEventListener("beforeunload", () => {
|
addEventListener("beforeunload", () => {
|
||||||
win?.close();
|
win.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
addEventListener("message", async e => {
|
addEventListener("message", async e => {
|
||||||
|
|||||||
26
src/musicbrainz/modules/related.ts
Normal file
26
src/musicbrainz/modules/related.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { onReleaseRoute } from "../glue/router";
|
||||||
|
import { ReleaseInfo, getPageReleaseInfo } from "../services/release";
|
||||||
|
import { addReleaseSidebarButton } from "../glue/sidebar";
|
||||||
|
import mhCoversIcon from "../assets/icons/mhcovers.svg";
|
||||||
|
import ongakuNoMoriIcon from "../assets/icons/ongakunomori.ico";
|
||||||
|
|
||||||
|
onReleaseRoute(async () => {
|
||||||
|
const release = getPageReleaseInfo();
|
||||||
|
if (!release) return;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
addOngakuNoMori(release),
|
||||||
|
addMhCovers(release)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function addOngakuNoMori(release: ReleaseInfo) {
|
||||||
|
const dn = release.barcode ?? release.catalogs[0];
|
||||||
|
if (!dn) return;
|
||||||
|
|
||||||
|
addReleaseSidebarButton(200, ongakuNoMoriIcon, "音楽の森 <small>(Search)</small>", `https://search.minc.or.jp/product/list/?type=search-form-diskno&dn=${dn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMhCovers(release: ReleaseInfo) {
|
||||||
|
addReleaseSidebarButton(300, mhCoversIcon, "MH Covers <small>(Search)</small>", `https://covers.musichoarders.xyz?artist=${encodeURIComponent(release.artist)}&album=${encodeURIComponent(release.title)}`);
|
||||||
|
}
|
||||||
78
src/musicbrainz/modules/scans.ts
Normal file
78
src/musicbrainz/modules/scans.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import JSZip from "jszip";
|
||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
import { downloadFile, parallelMap, sleep } from "../../common/misc";
|
||||||
|
import { onReleaseViewCoverRoute } from "../glue/router";
|
||||||
|
import { getPageReleaseInfo } from "../services/release";
|
||||||
|
import { formatPath } from "../../common/format";
|
||||||
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
|
||||||
|
onReleaseViewCoverRoute(() => {
|
||||||
|
const refEl = document.querySelector<HTMLElement>(".buttons.ui-helper-clearfix")!;
|
||||||
|
const downloadButtonEl = fromHTML(`<a class="ame-download-scans"><bdi>Download all scans</bdi></a>`);
|
||||||
|
refEl.appendChild(downloadButtonEl);
|
||||||
|
|
||||||
|
let downloading = false;
|
||||||
|
downloadButtonEl.addEventListener("click", async () => {
|
||||||
|
if (downloading) return;
|
||||||
|
downloading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadScans(downloadButtonEl);
|
||||||
|
} catch (err) {
|
||||||
|
downloadButtonEl.innerHTML = `Download all scans (Retry)`;
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function downloadScans(progressEl: HTMLElement) {
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
const release = getPageReleaseInfo();
|
||||||
|
if (!release) return;
|
||||||
|
|
||||||
|
const types: Record<string, number> = {};
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
const artworkEls = Array.from(document.querySelectorAll(`.artwork-cont`));
|
||||||
|
|
||||||
|
progressEl.innerHTML = `Download all scans (0/${artworkEls.length})`;
|
||||||
|
|
||||||
|
for (const artworkEl of artworkEls) {
|
||||||
|
index++;
|
||||||
|
|
||||||
|
const downloadEl = artworkEl.querySelector<HTMLLinkElement>("a:last-child");
|
||||||
|
if (!downloadEl) continue;
|
||||||
|
|
||||||
|
const type = artworkEl.querySelector("p")?.innerText.replace("Types:", "").trim();
|
||||||
|
if (!type) continue;
|
||||||
|
|
||||||
|
types[type] = Number(types[type]) + 1 || 1;
|
||||||
|
const typeCount = types[type];
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
try {
|
||||||
|
const filename = `${type} ${typeCount}.${downloadEl.href.split(".").at(-1)}`;
|
||||||
|
const data = await fetchCors(downloadEl.href).then(res => res.blob());
|
||||||
|
zip.file(formatPath(filename), data);
|
||||||
|
|
||||||
|
progressEl.innerHTML = `Download all scans (${index}/${artworkEls.length})`;
|
||||||
|
await sleep(100);
|
||||||
|
break;
|
||||||
|
} catch (err) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressEl.innerHTML = `Download all scans (Zipping 0%)`;
|
||||||
|
|
||||||
|
const foldername = `Scans {${release.catalogs[0] || release.barcode || release.id}}`;
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" }, (e) => {
|
||||||
|
progressEl.innerHTML = `Download all scans (Zipping ${e.percent.toFixed(0)}%)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
downloadFile(zipBlob, formatPath(`${foldername}.zip`));
|
||||||
|
|
||||||
|
progressEl.innerHTML = `Download all scans (Done)`;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getPageReleaseId } from "../services/release";
|
||||||
|
|
||||||
enum QueryType {
|
enum QueryType {
|
||||||
Other = "other",
|
Other = "other",
|
||||||
Catalog = "catalog",
|
Catalog = "catalog",
|
||||||
@@ -42,8 +44,7 @@ formEl.addEventListener("submit", e => {
|
|||||||
|
|
||||||
switch (queryType) {
|
switch (queryType) {
|
||||||
case QueryType.Catalog:
|
case QueryType.Catalog:
|
||||||
const catalog = query.split('~')[0];
|
location.href = `https://musicbrainz.org/search?type=release&method=advanced&query=catno:${encodeURIComponent(formatCatalog(query))}`;
|
||||||
location.href = `https://musicbrainz.org/search?type=release&method=advanced&query=catno:${encodeURIComponent(catalog)}`;
|
|
||||||
break;
|
break;
|
||||||
case QueryType.Barcode:
|
case QueryType.Barcode:
|
||||||
location.href = `https://musicbrainz.org/search?type=release&method=advanced&query=barcode:${encodeURIComponent(query)}`;
|
location.href = `https://musicbrainz.org/search?type=release&method=advanced&query=barcode:${encodeURIComponent(query)}`;
|
||||||
@@ -54,7 +55,7 @@ formEl.addEventListener("submit", e => {
|
|||||||
case QueryType.LogEac:
|
case QueryType.LogEac:
|
||||||
case QueryType.LogXld:
|
case QueryType.LogXld:
|
||||||
let params = `?toc=${parseTocFromLog(query)}`;
|
let params = `?toc=${parseTocFromLog(query)}`;
|
||||||
if (location.pathname.startsWith("/release/")) params += `&filter-release.query=${location.pathname.split("/")[2]}`;
|
if (location.pathname.startsWith("/release/")) params += `&filter-release.query=${getPageReleaseId()}`;
|
||||||
location.href = `https://musicbrainz.org/cdtoc/attach${params}`;
|
location.href = `https://musicbrainz.org/cdtoc/attach${params}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -69,6 +70,10 @@ export function identifyQuery(value: string): QueryType {
|
|||||||
return QueryType.Other;
|
return QueryType.Other;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCatalog(value: string): string {
|
||||||
|
return /^(.+)([1-9][0-9]*)~([0-9]+)$/.test(value) ? value.split('~')[0] : value;
|
||||||
|
}
|
||||||
|
|
||||||
function parseTocFromLog(value: string): string {
|
function parseTocFromLog(value: string): string {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
let lastNo = 0;
|
let lastNo = 0;
|
||||||
|
|||||||
24
src/musicbrainz/services/release.ts
Normal file
24
src/musicbrainz/services/release.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface ReleaseInfo {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
catalogs: string[];
|
||||||
|
barcode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageReleaseId(): string {
|
||||||
|
return location.pathname.split('/')[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPageReleaseInfo(): ReleaseInfo | null {
|
||||||
|
let barcode = document.querySelector<HTMLElement>(".barcode")?.innerText;
|
||||||
|
if (barcode === "[none]") barcode = undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: getPageReleaseId(),
|
||||||
|
title: document.querySelector<HTMLElement>("h1 > a")!.innerText,
|
||||||
|
artist: document.querySelector<HTMLElement>(".subheader bdi")!.innerText,
|
||||||
|
catalogs: Array.from(document.querySelectorAll<HTMLElement>(".catalog-number")).map(el => el.innerText),
|
||||||
|
barcode
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,3 +4,10 @@ span.fileinput-button.buttons {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Minimize sidebar layout shift on release page */
|
||||||
|
.cover-art-image img {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|||||||
8
src/vgmdb/glue/router.ts
Normal file
8
src/vgmdb/glue/router.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { onRoute } from "../../common/router";
|
||||||
|
import { RouteCallback } from "../../common/router";
|
||||||
|
|
||||||
|
const ALBUM_PATTERN = "album/.+";
|
||||||
|
|
||||||
|
export function onAlbumRoute(cb: RouteCallback) {
|
||||||
|
onRoute(ALBUM_PATTERN, cb);
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { onRoute } from "../../common";
|
|
||||||
import { RouteCallback } from "../../common/router";
|
|
||||||
|
|
||||||
export function onAlbumRoute(cb: RouteCallback) {
|
|
||||||
onRoute("/album/:id", cb);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export function addColumnLink(index: number, icon: string, title: string, link: string): HTMLElement {
|
export function addAlbumSidebarButton(index: number, icon: string, title: string, link: string): HTMLElement {
|
||||||
let refEl = document.querySelector("#ame-section");
|
let refEl = document.querySelector("#ame-section");
|
||||||
if (!refEl) {
|
if (!refEl) {
|
||||||
const sectionEls = document.querySelectorAll("#rightcolumn > br");
|
const sectionEls = document.querySelectorAll("#rightcolumn > br");
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @namespace ame-vgmdb
|
// @namespace ame-vgmdb
|
||||||
// @name Ame (VGMdb)
|
// @name Ame (VGMdb)
|
||||||
// @version 1.1.4
|
// @version 1.1.5
|
||||||
// @author SuperSaltyGamer
|
// @author SuperSaltyGamer
|
||||||
// @run-at document-end
|
// @run-at document-end
|
||||||
// @match https://vgmdb.net/*
|
// @match https://vgmdb.net/*
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
import "./modules/scans";
|
import "./modules/scans";
|
||||||
import "./modules/album";
|
import "./modules/related";
|
||||||
import styles from "./style.css?inline";
|
import styles from "./style.css?inline";
|
||||||
|
|
||||||
GM.addStyle(styles);
|
GM.addStyle(styles);
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { fetchCors } from "../../common";
|
|
||||||
import { addColumnLink } from "../glue/column";
|
|
||||||
import { onAlbumRoute } from "../glue/routing";
|
|
||||||
import { AlbumInfo, getAlbumInfo, splitCatalog } from "../utils";
|
|
||||||
|
|
||||||
import musicbrainzIcon from "../assets/icons/musicbrainz.ico";
|
|
||||||
import ongakuNoMoriIcon from "../assets/icons/ongakunomori.ico";
|
|
||||||
import mhCoversIcon from "../assets/icons/mhcovers.svg";
|
|
||||||
import { createMusicBrainzReleaseSeeder } from "./musicbrainz";
|
|
||||||
|
|
||||||
onAlbumRoute(async () => {
|
|
||||||
const albumInfo = getAlbumInfo();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
addMusicBrainz(albumInfo),
|
|
||||||
addOngakuNoMori(albumInfo),
|
|
||||||
addMhCovers(albumInfo)
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function addMusicBrainz(albumInfo: AlbumInfo) {
|
|
||||||
let query: string[] = [];
|
|
||||||
if (albumInfo.catalog) for (const catalog of splitCatalog(albumInfo.catalog)) query.push(`catno:${catalog}`);
|
|
||||||
if (albumInfo.barcode) query.push(`barcode:${albumInfo.barcode}`);
|
|
||||||
|
|
||||||
function addSeedLink() {
|
|
||||||
function seed(e: MouseEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
createMusicBrainzReleaseSeeder(albumInfo).submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkEl = addColumnLink(100, musicbrainzIcon, "MusicBrainz <small>(Seed)</small>", "#");
|
|
||||||
linkEl.addEventListener("click", seed);
|
|
||||||
linkEl.addEventListener("auxclick", seed);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.length === 0) {
|
|
||||||
addSeedLink();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetchCors(`http://musicbrainz.org/ws/2/release/?fmt=json&query=${encodeURIComponent(query.join(" "))}`);
|
|
||||||
const body = await res.json<{ releases: { id: string }[] }>();
|
|
||||||
|
|
||||||
if (body.releases.length === 0) {
|
|
||||||
addSeedLink();
|
|
||||||
} else if (body.releases.length === 1) {
|
|
||||||
addColumnLink(100, musicbrainzIcon, "MusicBrainz", `https://musicbrainz.org/release/${body.releases[0].id}`);
|
|
||||||
} else {
|
|
||||||
addColumnLink(100, musicbrainzIcon, "MusicBrainz <small>(Search)</small>", `https://musicbrainz.org/search?type=release&method=advanced&query=${encodeURIComponent(query.join(" "))}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addOngakuNoMori(albumInfo: AlbumInfo) {
|
|
||||||
const dn = albumInfo.catalog ? splitCatalog(albumInfo.catalog)[0] : albumInfo.barcode;
|
|
||||||
if (!dn) return;
|
|
||||||
|
|
||||||
addColumnLink(200, ongakuNoMoriIcon, "音楽の森 <small>(Search)</small>", `https://search.minc.or.jp/product/list/?type=search-form-diskno&dn=${dn}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addMhCovers(albumInfo: AlbumInfo) {
|
|
||||||
addColumnLink(300, mhCoversIcon, "MH Covers <small>(Search)</small>", `https://covers.musichoarders.xyz?artist=${encodeURIComponent(albumInfo.artist)}&album=${encodeURIComponent(albumInfo.album)}`);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fromHTML } from "../../common";
|
import { AlbumInfo } from "../services/album";
|
||||||
import { AlbumInfo, splitCatalog } from "../utils";
|
import { fromHTML } from "../../common/dom";
|
||||||
|
|
||||||
export function createMusicBrainzReleaseSeeder(albumInfo: AlbumInfo): HTMLFormElement {
|
export function createMusicBrainzReleaseSeeder(album: AlbumInfo): HTMLFormElement {
|
||||||
const formEl = fromHTML<HTMLFormElement>(`
|
const formEl = fromHTML<HTMLFormElement>(`
|
||||||
<form method="POST" target="_blank" action="https://musicbrainz.org/release/add" style="display: none;">
|
<form method="POST" target="_blank" action="https://musicbrainz.org/release/add" style="display: none;">
|
||||||
<button type="submit">
|
<button type="submit">
|
||||||
@@ -20,51 +20,51 @@ export function createMusicBrainzReleaseSeeder(albumInfo: AlbumInfo): HTMLFormEl
|
|||||||
|
|
||||||
const url = `${location.origin}${location.pathname}`;
|
const url = `${location.origin}${location.pathname}`;
|
||||||
|
|
||||||
set("name", albumInfo.album);
|
set("name", album.album);
|
||||||
if (albumInfo.artist) {
|
if (album.artist) {
|
||||||
set("artist_credit.names.0.name", albumInfo.artist);
|
set("artist_credit.names.0.name", album.artist);
|
||||||
}
|
}
|
||||||
if (albumInfo.album.match(/[ㄱ-ㅎ가-힣]/)) {
|
if (album.album.match(/[ㄱ-ㅎ가-힣]/)) {
|
||||||
set("language", "kor");
|
set("language", "kor");
|
||||||
set("script", "Kore");
|
set("script", "Kore");
|
||||||
} else if (albumInfo.album.match(/[一-龯]/)) {
|
} else if (album.album.match(/[一-龯]/)) {
|
||||||
set("language", "jpn");
|
set("language", "jpn");
|
||||||
set("script", "Jpan");
|
set("script", "Jpan");
|
||||||
} else {
|
} else {
|
||||||
set("language", "eng");
|
set("language", "eng");
|
||||||
set("script", "Latn");
|
set("script", "Latn");
|
||||||
}
|
}
|
||||||
if (albumInfo.publish) {
|
if (album.publish) {
|
||||||
if (albumInfo.publish.includes("promo")) set("status", "promotion");
|
if (album.publish.includes("promo")) set("status", "promotion");
|
||||||
else if (albumInfo.publish.includes("bootleg")) set("status", "bootleg");
|
else if (album.publish.includes("bootleg")) set("status", "bootleg");
|
||||||
else set("status", "official");
|
else set("status", "official");
|
||||||
}
|
}
|
||||||
if (albumInfo.tracks.length <= 6) {
|
if (album.tracks.length <= 6) {
|
||||||
set("type", "single");
|
set("type", "single");
|
||||||
} else {
|
} else {
|
||||||
set("type", "album");
|
set("type", "album");
|
||||||
}
|
}
|
||||||
if (!albumInfo.classification) {
|
if (!album.classification) {
|
||||||
} else if (albumInfo.classification.includes("soundtrack")) {
|
} else if (album.classification.includes("soundtrack")) {
|
||||||
set("type", "soundtrack");
|
set("type", "soundtrack");
|
||||||
} else if (albumInfo.classification.includes("drama")) {
|
} else if (album.classification.includes("drama")) {
|
||||||
set("type", "audio drama");
|
set("type", "audio drama");
|
||||||
} else if (albumInfo.classification.includes("remix")) {
|
} else if (album.classification.includes("remix")) {
|
||||||
set("type", "remix");
|
set("type", "remix");
|
||||||
} else if (albumInfo.classification.includes("talk")) {
|
} else if (album.classification.includes("talk")) {
|
||||||
set("type", "spokenword");
|
set("type", "spokenword");
|
||||||
}
|
}
|
||||||
if (albumInfo.date) {
|
if (album.date) {
|
||||||
switch (albumInfo.date.length) {
|
switch (album.date.length) {
|
||||||
case 3:
|
case 3:
|
||||||
set("events.0.date.day", albumInfo.date[2].toString());
|
set("events.0.date.day", album.date[2].toString());
|
||||||
case 2:
|
case 2:
|
||||||
set("events.0.date.month", albumInfo.date[1].toString());
|
set("events.0.date.month", album.date[1].toString());
|
||||||
case 1:
|
case 1:
|
||||||
set("events.0.date.year", albumInfo.date[0].toString());
|
set("events.0.date.year", album.date[0].toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch (albumInfo.currency) {
|
switch (album.currency) {
|
||||||
case "jpy":
|
case "jpy":
|
||||||
set("events.0.country", "JP");
|
set("events.0.country", "JP");
|
||||||
break;
|
break;
|
||||||
@@ -75,24 +75,23 @@ export function createMusicBrainzReleaseSeeder(albumInfo: AlbumInfo): HTMLFormEl
|
|||||||
set("events.0.country", "CN");
|
set("events.0.country", "CN");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (albumInfo.barcode) set("barcode", albumInfo.barcode);
|
if (album.barcode) set("barcode", album.barcode);
|
||||||
if (albumInfo.catalog) {
|
if (album.catalog) {
|
||||||
const catalogs = splitCatalog(albumInfo.catalog);
|
album.catalogs.forEach((catalog, i) => {
|
||||||
catalogs.forEach((catalog, i) => {
|
|
||||||
set(`labels.${i}.catalog_number`, catalog);
|
set(`labels.${i}.catalog_number`, catalog);
|
||||||
if (albumInfo.label && i !== 0) set(`labels.${i}.name`, albumInfo.label);
|
if (album.label && i !== 0) set(`labels.${i}.name`, album.label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (albumInfo.label) set("labels.0.name", albumInfo.label);
|
if (album.label) set("labels.0.name", album.label);
|
||||||
|
|
||||||
albumInfo.mediums.forEach((medium, i) => {
|
album.mediums.forEach((medium, i) => {
|
||||||
if (medium) set(`mediums.${i}.format`, medium);
|
if (medium) set(`mediums.${i}.format`, medium);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mediumNumber = 0;
|
let mediumNumber = 0;
|
||||||
let trackNumber = 0;
|
let trackNumber = 0;
|
||||||
|
|
||||||
for (const track of albumInfo.tracks) {
|
for (const track of album.tracks) {
|
||||||
if (track.number <= trackNumber) mediumNumber++;
|
if (track.number <= trackNumber) mediumNumber++;
|
||||||
trackNumber = track.number;
|
trackNumber = track.number;
|
||||||
|
|
||||||
@@ -103,7 +102,7 @@ export function createMusicBrainzReleaseSeeder(albumInfo: AlbumInfo): HTMLFormEl
|
|||||||
|
|
||||||
set("urls.0.url", url);
|
set("urls.0.url", url);
|
||||||
set("urls.0.link_type", "86"); // vgmdb
|
set("urls.0.link_type", "86"); // vgmdb
|
||||||
albumInfo.urls.forEach((url, i) => {
|
album.urls.forEach((url, i) => {
|
||||||
let type: string | null = null;
|
let type: string | null = null;
|
||||||
if (url.includes("cdjapan.co.jp/") || url.includes("yesasia.com/") || url.includes("play-asia.com/")) {
|
if (url.includes("cdjapan.co.jp/") || url.includes("yesasia.com/") || url.includes("play-asia.com/")) {
|
||||||
type = "79"; // purchase for mail-order
|
type = "79"; // purchase for mail-order
|
||||||
|
|||||||
62
src/vgmdb/modules/related.ts
Normal file
62
src/vgmdb/modules/related.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
import { addAlbumSidebarButton } from "../glue/sidebar";
|
||||||
|
import { onAlbumRoute } from "../glue/router";
|
||||||
|
import musicbrainzIcon from "../assets/icons/musicbrainz.ico";
|
||||||
|
import ongakuNoMoriIcon from "../assets/icons/ongakunomori.ico";
|
||||||
|
import mhCoversIcon from "../assets/icons/mhcovers.svg";
|
||||||
|
import { createMusicBrainzReleaseSeeder } from "./musicbrainz";
|
||||||
|
import { AlbumInfo, getPageAlbumInfo } from "../services/album";
|
||||||
|
|
||||||
|
onAlbumRoute(async () => {
|
||||||
|
const album = getPageAlbumInfo();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
addMusicBrainz(album),
|
||||||
|
addOngakuNoMori(album),
|
||||||
|
addMhCovers(album)
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function addMusicBrainz(album: AlbumInfo) {
|
||||||
|
let query: string[] = [];
|
||||||
|
if (album.catalog) for (const catalog of album.catalogs) query.push(`catno:${catalog}`);
|
||||||
|
if (album.barcode) query.push(`barcode:${album.barcode}`);
|
||||||
|
|
||||||
|
function addSeedLink() {
|
||||||
|
function seed(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
createMusicBrainzReleaseSeeder(album).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkEl = addAlbumSidebarButton(100, musicbrainzIcon, "MusicBrainz <small>(Seed)</small>", "#");
|
||||||
|
linkEl.addEventListener("click", seed);
|
||||||
|
linkEl.addEventListener("auxclick", seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.length === 0) {
|
||||||
|
addSeedLink();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetchCors(`http://musicbrainz.org/ws/2/release/?fmt=json&query=${encodeURIComponent(query.join(" "))}`);
|
||||||
|
const body = await res.json<{ releases: { id: string }[] }>();
|
||||||
|
|
||||||
|
if (body.releases.length === 0) {
|
||||||
|
addSeedLink();
|
||||||
|
} else if (body.releases.length === 1) {
|
||||||
|
addAlbumSidebarButton(100, musicbrainzIcon, "MusicBrainz", `https://musicbrainz.org/release/${body.releases[0].id}`);
|
||||||
|
} else {
|
||||||
|
addAlbumSidebarButton(100, musicbrainzIcon, "MusicBrainz <small>(Search)</small>", `https://musicbrainz.org/search?type=release&method=advanced&query=${encodeURIComponent(query.join(" "))}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addOngakuNoMori(album: AlbumInfo) {
|
||||||
|
const dn = album.catalogs[0] ?? album.barcode;
|
||||||
|
if (!dn) return;
|
||||||
|
|
||||||
|
addAlbumSidebarButton(200, ongakuNoMoriIcon, "音楽の森 <small>(Search)</small>", `https://search.minc.or.jp/product/list/?type=search-form-diskno&dn=${dn}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addMhCovers(album: AlbumInfo) {
|
||||||
|
addAlbumSidebarButton(300, mhCoversIcon, "MH Covers <small>(Search)</small>", `https://covers.musichoarders.xyz?artist=${encodeURIComponent(album.artist)}&album=${encodeURIComponent(album.album)}`);
|
||||||
|
}
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { onAlbumRoute } from "../glue/routing";
|
import { onAlbumRoute } from "../glue/router";
|
||||||
import { downloadFile, fetchCors, sleep } from "../../common";
|
import { downloadFile, sleep } from "../../common/misc";
|
||||||
import { getAlbumInfo } from "../utils";
|
import { fetchCors } from "../../common/fetch";
|
||||||
|
import { getPageAlbumInfo } from "../services/album";
|
||||||
import { formatPath } from "../../common/format";
|
import { formatPath } from "../../common/format";
|
||||||
|
import { fromHTML } from "../../common/dom";
|
||||||
|
|
||||||
onAlbumRoute(() => {
|
onAlbumRoute(() => {
|
||||||
// Select scan gallery tab by default.
|
// Select scan gallery tab by default.
|
||||||
document.querySelector<HTMLElement>("a[rel='cover_gallery']")?.click();
|
document.querySelector<HTMLElement>("a[rel='cover_gallery']")?.click();
|
||||||
|
|
||||||
const coverGalleryEl = document.querySelector<HTMLElement>("#cover_gallery")!;
|
const coverGalleryEl = document.querySelector<HTMLElement>("#cover_gallery")!;
|
||||||
const downloadEl = document.createElement("a");
|
const downloadButtonEl = fromHTML(`<a class="ame-download-scans">Download all scans</a>`);
|
||||||
downloadEl.classList.add("ame-download_button");
|
coverGalleryEl.insertAdjacentElement("afterbegin", downloadButtonEl);
|
||||||
downloadEl.innerText = "Download all scans";
|
|
||||||
coverGalleryEl.insertAdjacentElement("afterbegin", downloadEl);
|
|
||||||
|
|
||||||
let downloading = false;
|
let downloading = false;
|
||||||
downloadEl.addEventListener("click", async () => {
|
downloadButtonEl.addEventListener("click", async () => {
|
||||||
if (downloading) return;
|
if (downloading) return;
|
||||||
downloading = true;
|
downloading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
downloadEl.dataset["status"] = "loading";
|
downloadButtonEl.dataset["status"] = "loading";
|
||||||
await downloadScans();
|
await downloadScans();
|
||||||
downloadEl.dataset["status"] = "success";
|
downloadButtonEl.dataset["status"] = "success";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
downloadEl.dataset["status"] = "error";
|
downloadButtonEl.dataset["status"] = "error";
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,16 +35,17 @@ onAlbumRoute(() => {
|
|||||||
async function downloadScans() {
|
async function downloadScans() {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
const albumInfo = getAlbumInfo()!;
|
const album = getPageAlbumInfo();
|
||||||
const foldername = formatPath(`Scans {${albumInfo.catalog || albumInfo.barcode || albumInfo.id}}`);
|
if (!album) return;
|
||||||
|
|
||||||
const els = Array.from(document.querySelectorAll<HTMLLinkElement>(`#cover_gallery a[href^="https://media.vgm.io"]`));
|
const scanEls = Array.from(document.querySelectorAll<HTMLLinkElement>(`#cover_gallery a[href^="https://media.vgm.io"]`));
|
||||||
for (const el of els) {
|
for (const scanEl of scanEls) {
|
||||||
const data = await fetchCors(el.href).then(res => res.blob());
|
const data = await fetchCors(scanEl.href).then(res => res.blob());
|
||||||
const filename = formatPath(el.querySelector('h4')!.innerText.trim());
|
const filename = scanEl.querySelector('h4')!.innerText.trim();
|
||||||
zip.file(`${filename}.jpg`, data);
|
zip.file(formatPath(`${filename}.jpg`), data);
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const foldername = formatPath(`Scans {${album.catalog || album.barcode || album.id}}`);
|
||||||
downloadFile(await zip.generateAsync({ type: "blob" }), `${foldername}.zip`);
|
downloadFile(await zip.generateAsync({ type: "blob" }), `${foldername}.zip`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AlbumInfo {
|
|||||||
currency?: string;
|
currency?: string;
|
||||||
date?: number[];
|
date?: number[];
|
||||||
catalog?: string;
|
catalog?: string;
|
||||||
|
catalogs: string[];
|
||||||
barcode?: string;
|
barcode?: string;
|
||||||
mediums: (string | null)[];
|
mediums: (string | null)[];
|
||||||
urls: string[];
|
urls: string[];
|
||||||
@@ -18,7 +19,7 @@ export interface AlbumInfo {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAlbumInfo(): AlbumInfo {
|
export function getPageAlbumInfo(): AlbumInfo {
|
||||||
const artistTitle = (
|
const artistTitle = (
|
||||||
document.querySelector<HTMLElement>(".albumtitle[lang='ja']") ??
|
document.querySelector<HTMLElement>(".albumtitle[lang='ja']") ??
|
||||||
document.querySelector<HTMLElement>(".albumtitle[lang='en']") ??
|
document.querySelector<HTMLElement>(".albumtitle[lang='en']") ??
|
||||||
@@ -33,6 +34,7 @@ export function getAlbumInfo(): AlbumInfo {
|
|||||||
id: location.pathname.split("/")[2],
|
id: location.pathname.split("/")[2],
|
||||||
artist,
|
artist,
|
||||||
album: title,
|
album: title,
|
||||||
|
catalogs: [],
|
||||||
mediums: [],
|
mediums: [],
|
||||||
urls: [],
|
urls: [],
|
||||||
tracks: []
|
tracks: []
|
||||||
@@ -51,16 +53,17 @@ export function getAlbumInfo(): AlbumInfo {
|
|||||||
info.label = value;
|
info.label = value;
|
||||||
break;
|
break;
|
||||||
case "Publish Format":
|
case "Publish Format":
|
||||||
info.publish = value?.toLowerCase()?.replace(/[^a-z]/g, "");
|
info.publish = formatSlug(value);
|
||||||
break;
|
break;
|
||||||
case "Classification":
|
case "Classification":
|
||||||
info.classification = value?.toLowerCase()?.replace(/[^a-z]/g, "");
|
info.classification = formatSlug(value);
|
||||||
break;
|
break;
|
||||||
case "Media Format":
|
case "Media Format":
|
||||||
info.mediums = parseMediums(value);
|
info.mediums = parseMediums(value);
|
||||||
case "Release Price":
|
case "Release Price":
|
||||||
if (value.includes("USD")) info.currency = "usd";
|
if (value.includes("USD")) info.currency = "usd";
|
||||||
else if (value.includes("JPY")) info.currency = "jpy";
|
else if (value.includes("JPY")) info.currency = "jpy";
|
||||||
|
else if (value.includes("Japan")) info.currency = "jpy";
|
||||||
else if (value.includes("KRW")) info.currency = "krw";
|
else if (value.includes("KRW")) info.currency = "krw";
|
||||||
else if (value.includes("RMB") || value.includes("CNY")) info.currency = "cny";
|
else if (value.includes("RMB") || value.includes("CNY")) info.currency = "cny";
|
||||||
break;
|
break;
|
||||||
@@ -68,7 +71,10 @@ export function getAlbumInfo(): AlbumInfo {
|
|||||||
info.date = parseDate(valueEl.innerText);
|
info.date = parseDate(valueEl.innerText);
|
||||||
break;
|
break;
|
||||||
case "Catalog Number":
|
case "Catalog Number":
|
||||||
if (value !== "N/A") info.catalog = value;
|
if (value !== "N/A") {
|
||||||
|
info.catalog = value;
|
||||||
|
info.catalogs = splitCatalog(value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case "Barcode":
|
case "Barcode":
|
||||||
if (value !== "N/A") info.barcode = value;
|
if (value !== "N/A") info.barcode = value;
|
||||||
@@ -106,8 +112,13 @@ export function getAlbumInfo(): AlbumInfo {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function splitCatalog(value: string): string[] {
|
function formatSlug(value: string): string
|
||||||
const match = value.match(/^(.+?)(\d+)~(\d+)$/);
|
function formatSlug(value: string | null): string | null {
|
||||||
|
return value?.toLowerCase().replace(/[^a-z]/g, "") ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitCatalog(value: string): string[] {
|
||||||
|
const match = value.match(/^(.+)([1-9][0-9]*)~([0-9]+)$/);
|
||||||
if (!match) return [ value ];
|
if (!match) return [ value ];
|
||||||
const start = Number(match[2]);
|
const start = Number(match[2]);
|
||||||
const end = Number(match[2].slice(0, -match[3].length) + match[3]);
|
const end = Number(match[2].slice(0, -match[3].length) + match[3]);
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
/* Scans download button. */
|
/* Scans download button. */
|
||||||
|
|
||||||
.ame-download_button {
|
.ame-download-scans {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ame-download_button[data-status="loading"] {
|
.ame-download-scans[data-status="loading"] {
|
||||||
color: #006ad4;
|
color: #006ad4;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ame-download_button[data-status="success"] {
|
.ame-download-scans[data-status="success"] {
|
||||||
color: #00d46a;
|
color: #00d46a;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ame-download_button[data-status="error"] {
|
.ame-download-scans[data-status="error"] {
|
||||||
color: #d40000;
|
color: #d40000;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user