diff --git a/package.json b/package.json index 8c80289..a20adf6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "homepage": "https://github.com/Xmader/musescore-downloader#readme", "dependencies": { + "md5": "^2.3.0", "pdfkit": "git+https://github.com/Xmader/pdfkit.git", "svg-to-pdfkit": "^0.1.8", "webmscore": "^0.10.4" @@ -21,6 +22,7 @@ "devDependencies": { "@rollup/plugin-json": "^4.0.0", "@types/file-saver": "^2.0.1", + "@types/md5": "^2.2.0", "@types/pdfkit": "^0.10.4", "rollup": "^1.26.3", "rollup-plugin-commonjs": "^10.1.0", diff --git a/src/btn.ts b/src/btn.ts index 5b82ff7..1b42c0c 100644 --- a/src/btn.ts +++ b/src/btn.ts @@ -1,5 +1,6 @@ import { loadMscore, WebMscore } from './mscore' +import { saveAs } from './utils' type BtnElement = HTMLButtonElement @@ -86,8 +87,27 @@ export namespace BtnAction { export const PROCESSING_TEXT = 'Processing…' export const ERROR_TEXT = '❌Download Failed!' - export const openUrl = (url: string): BtnAction => { - return (): any => window.open(url) + type Promisable = T | Promise + type UrlInput = Promisable | (() => Promisable) + + const normalizeUrlInput = (url: UrlInput) => { + if (typeof url === 'function') return url() + else return url + } + + export const openUrl = (url: UrlInput): BtnAction => { + return process(async (): Promise => { + window.open(await normalizeUrlInput(url)) + }) + } + + export const download = (url: UrlInput, filename?: string): BtnAction => { + return process(async (): Promise => { + const _url = await normalizeUrlInput(url) + const r = await fetch(_url) + const blob = await r.blob() + saveAs(blob, filename) + }) } export const mscoreWindow = (fn: (w: Window, score: WebMscore, processingTextEl: ChildNode) => any): BtnAction => { diff --git a/src/file.ts b/src/file.ts new file mode 100644 index 0000000..355d36a --- /dev/null +++ b/src/file.ts @@ -0,0 +1,32 @@ + +import scoreinfo from './scoreinfo' +import md5 from 'md5' + +const MAGIC = String(4 * 11 * 1607 * 2767) // '195649036' // I don't know what this is +const AUTH_LEN = 4 + +type FileType = 'img' | 'mp3' | 'midi' + +const getApiUrl = (type: FileType, index: number): string => { + // proxy + return `https://musescore.now.sh/api/jmuse?id=${scoreinfo.id}&type=${type}&index=${index}` +} + +const getApiAuth = (type: FileType, index: number): string => { + const str = String(scoreinfo.id) + type + String(index) + MAGIC + return md5(str).slice(0, AUTH_LEN) +} + +export const getFileUrl = async (type: FileType, index = 0): Promise => { + const url = getApiUrl(type, index) + const auth = getApiAuth(type, index) + + const r = await fetch(url, { + headers: { + Authorization: auth, + }, + }) + + const { info } = await r.json() + return info.url as string +} diff --git a/src/main.ts b/src/main.ts index 8919125..af3676b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import './meta' import { waitForDocumentLoaded, saveAs } from './utils' import { downloadPDF } from './pdf' import { downloadMscz } from './mscz' +import { getFileUrl } from './file' import { getDownloadBtn, BtnList, BtnAction } from './btn' import * as recaptcha from './recaptcha' import scoreinfo from './scoreinfo' @@ -16,6 +17,7 @@ const main = (): void => { recaptcha.init() const btnList = new BtnList(getDownloadBtn()) + const filename = scoreinfo.fileName btnList.add({ name: 'Download MSCZ', @@ -32,19 +34,19 @@ const main = (): void => { action: BtnAction.mscoreWindow(async (w, score) => { const mxl = await score.saveMxl() const data = new Blob([mxl]) - saveAs(data, `${scoreinfo.fileName}.mxl`) + saveAs(data, `${filename}.mxl`) w.close() }), }) btnList.add({ name: 'Download MIDI', - action: BtnAction.openUrl(scoreinfo.midiUrl), + action: BtnAction.download(() => getFileUrl('midi'), `${filename}.mid`), }) btnList.add({ name: 'Download MP3', - action: BtnAction.openUrl(scoreinfo.mp3Url), + action: BtnAction.download(() => getFileUrl('mp3'), `${filename}.mp3`), }) btnList.add({ @@ -95,7 +97,6 @@ const main = (): void => { await score.setExcerptId(+id) - const filename = scoreinfo.fileName const data = new Blob([await score.savePdf()]) saveAs(data, `${filename} - ${partName}.pdf`) } diff --git a/src/pdf.ts b/src/pdf.ts index c739ceb..0ff0984 100644 --- a/src/pdf.ts +++ b/src/pdf.ts @@ -1,5 +1,6 @@ import { PDFWorkerHelper } from './worker-helper' +import { getFileUrl } from './file' import { saveAs } from './utils' import scoreinfo from './scoreinfo' @@ -26,9 +27,14 @@ export const downloadPDF = async (): Promise => { const imgType = scoreinfo.sheetImgType const pageCount = scoreinfo.pageCount - const sheetImgURLs = Array.from({ length: pageCount }).map((_, i) => { - return scoreinfo.baseUrl + `score_${i}.${imgType}` + const rs = Array.from({ length: pageCount }).map((_, i) => { + if (i === 0) { // The url to the first page is static. We don't need to use API to obtain it. + return scoreinfo.baseUrl + `score_${i}.${imgType}` + } else { // obtain image urls using the API + return getFileUrl('img', i) + } }) + const sheetImgURLs = await Promise.all(rs) return _downloadPDF(sheetImgURLs, imgType, scoreinfo.fileName) } diff --git a/src/scoreinfo.ts b/src/scoreinfo.ts index 143dd57..bbf97d9 100644 --- a/src/scoreinfo.ts +++ b/src/scoreinfo.ts @@ -38,14 +38,6 @@ const scoreinfo = { return origin + pathname.split('/').slice(0, -1).join('/') + '/' }, - get midiUrl (this: typeof scoreinfo): string { - return this.baseUrl + 'score.mid' - }, - - get mp3Url (this: typeof scoreinfo): string { - return this.baseUrl + 'score.mp3' - }, - get msczUrl (this: typeof scoreinfo): string { // https://github.com/Xmader/cloudflare-worker-musescore-mscz return `https://musescore.now.sh/api/mscz?id=${this.id}&token=`