musescore-downloader/src/main.ts

260 lines
8.6 KiB
TypeScript
Raw Normal View History

2019-11-03 19:13:06 +00:00
import "./meta"
import { ScorePlayerData } from "./types"
import { waitForDocumentLoaded } from "./utils"
2020-03-08 21:36:37 +00:00
import * as recaptcha from "./recaptcha"
2019-11-03 19:13:06 +00:00
import { PDFWorkerHelper } from "./worker-helper"
import FileSaver from "file-saver/dist/FileSaver.js"
const saveAs: typeof import("file-saver").saveAs = FileSaver.saveAs
2019-12-01 09:11:40 +00:00
2020-05-13 10:29:11 +00:00
const PROCESSING_TEXT = "Processing…"
const FAILED_TEXT = "❌Download Failed!"
const WEBMSCORE_URL = "https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js"
2019-12-01 09:11:40 +00:00
let pdfBlob: Blob
2020-05-13 10:29:11 +00:00
let msczBufferP: Promise<ArrayBuffer>
const generatePDF = async (imgURLs: string[], imgType: "svg" | "png", name?: string) => {
if (pdfBlob) {
return saveAs(pdfBlob, `${name}.pdf`)
}
const cachedImg = document.querySelector("img[src*=score_]") as HTMLImageElement
const { naturalWidth: width, naturalHeight: height } = cachedImg
const worker = new PDFWorkerHelper()
const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height)
worker.terminate()
pdfBlob = new Blob([pdfArrayBuffer])
2019-12-01 09:11:40 +00:00
saveAs(pdfBlob, `${name}.pdf`)
}
const getPagesNumber = (scorePlayerData: ScorePlayerData) => {
try {
return scorePlayerData.json.metadata.pages
} catch (_) {
return document.querySelectorAll("img[src*=score_]").length
}
}
const getImgType = (): "svg" | "png" => {
try {
const imgE: HTMLImageElement = document.querySelector("img[src*=score_]")
const { pathname } = new URL(imgE.src)
const imgtype = pathname.match(/\.(\w+)$/)[1]
return imgtype as "svg" | "png"
} catch (_) {
return null
}
}
const getTitle = (scorePlayerData: ScorePlayerData) => {
try {
return scorePlayerData.json.metadata.title
} catch (_) {
return ""
}
}
const getScoreFileName = (scorePlayerData: ScorePlayerData) => {
return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_")
}
2020-05-13 10:29:11 +00:00
const fetchMscz = async (url: string): Promise<ArrayBuffer> => {
if (!msczBufferP) {
msczBufferP = (async () => {
const token = await recaptcha.execute()
const r = await fetch(url + token)
const data = await r.arrayBuffer()
return data
})()
}
return msczBufferP
}
2019-11-29 23:07:43 +00:00
const main = () => {
2019-11-03 19:13:06 +00:00
2019-12-01 05:13:14 +00:00
// @ts-ignore
if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return }
2020-03-08 21:36:37 +00:00
// init recaptcha
recaptcha.init()
2019-11-29 23:07:43 +00:00
// @ts-ignore
const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player
const { id } = scorePlayer.json
2019-11-29 23:07:43 +00:00
const baseURL = scorePlayer.urls.image_path
2019-11-03 19:13:06 +00:00
2020-05-13 10:29:11 +00:00
const filename = getScoreFileName(scorePlayer)
// https://github.com/Xmader/cloudflare-worker-musescore-mscz
2020-03-08 21:36:37 +00:00
const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=`
2019-11-29 23:07:43 +00:00
const mxlURL = baseURL + "score.mxl"
const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls
2020-02-15 21:30:10 +00:00
const btnsDiv = document.querySelector(".score-right .buttons-wrapper") || document.querySelectorAll("aside section > div")[4]
2019-11-29 23:07:43 +00:00
const downloadBtn = btnsDiv.querySelector("button, .button") as HTMLElement
downloadBtn.onclick = null
2020-03-29 21:31:33 +00:00
// fix the icon of the download btn
// if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
const svgPath: SVGPathElement = downloadBtn.querySelector("svg > path")
if (svgPath) {
2020-03-29 21:31:33 +00:00
svgPath.setAttribute("d", "M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z")
}
const imgType = getImgType() || "svg"
const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => {
return baseURL + `score_${i}.${imgType}`
})
2019-11-29 23:07:43 +00:00
const downloadURLs = {
2020-05-13 10:29:11 +00:00
"MSCZ": null,
"PDF": null,
2019-11-29 23:07:43 +00:00
"MusicXML": mxlURL,
"MIDI": midiURL,
"MP3": mp3URL,
2020-05-13 10:29:11 +00:00
"Parts": null,
2019-11-03 20:21:59 +00:00
}
const createBtn = (name: string) => {
2019-11-29 23:07:43 +00:00
const btn = downloadBtn.cloneNode(true) as HTMLElement
if (btn.nodeName.toLowerCase() == "button") {
btn.setAttribute("style", "width: 205px !important")
} else {
btn.dataset.target = ""
}
const textNode = [...btn.childNodes].find((x) => {
return x.textContent.includes("Download") || x.textContent.includes("Print")
2019-11-29 23:07:43 +00:00
})
textNode.textContent = `Download ${name}`
return {
btn,
textNode,
}
}
const newDownloadBtns = Object.keys(downloadURLs).map((name) => {
const url = downloadURLs[name]
const { btn, textNode } = createBtn(name)
2020-03-08 21:36:37 +00:00
if (name == "PDF") {
btn.onclick = async () => {
const filename = getScoreFileName(scorePlayer)
2020-05-13 10:29:11 +00:00
textNode.textContent = PROCESSING_TEXT
try {
await generatePDF(sheetImgURLs, imgType, filename)
textNode.textContent = "Download PDF"
} catch (err) {
2020-05-13 10:29:11 +00:00
textNode.textContent = FAILED_TEXT
console.error(err)
}
}
2020-03-08 21:36:37 +00:00
} else if (name == "MSCZ") {
btn.onclick = async () => {
2020-05-13 10:29:11 +00:00
textNode.textContent = PROCESSING_TEXT
2020-03-08 21:44:20 +00:00
try {
2020-05-13 10:29:11 +00:00
const data = new Blob([await fetchMscz(msczURL)])
textNode.textContent = "Download MSCZ"
saveAs(data, `${filename}.mscz`)
} catch (err) {
2020-05-13 10:29:11 +00:00
textNode.textContent = FAILED_TEXT
console.error(err)
}
2020-03-08 21:36:37 +00:00
}
2020-05-13 10:29:11 +00:00
} else if (name == "Parts") { // download individual parts
btn.title = "Download individual parts (BETA)"
const cb = btn.onclick = async () => {
btn.onclick = null
textNode.textContent = PROCESSING_TEXT
const w = window.open("")
const txt = document.createTextNode(PROCESSING_TEXT)
w.document.body.append(txt)
// set page hooks
const destroy = () => {
score.destroy()
w.close()
}
window.addEventListener("unload", destroy)
w.addEventListener("beforeunload", () => {
score.destroy()
window.removeEventListener("unload", destroy)
textNode.textContent = "Download Parts"
btn.onclick = cb
})
// load webmscore (https://github.com/LibreScore/webmscore)
const script = w.document.createElement("script")
script.src = WEBMSCORE_URL
w.document.body.append(script)
await new Promise(resolve => { script.onload = resolve })
// parse mscz data
const data = new Uint8Array(
new Uint8Array(await fetchMscz(msczURL)) // copy its ArrayBuffer
)
const score = await w["WebMscore"].load("mscz", data)
await score.generateExcerpts()
const metadata = await score.metadata()
console.log("score metadata loaded by webmscore", metadata)
// render the part selection page
txt.remove()
const fieldset = w.document.createElement("fieldset")
for (const excerpt of metadata.excerpts) {
const e = w.document.createElement("input")
e.name = "score-part"
e.type = "radio"
e.value = excerpt.id
const label = w.document.createElement("label")
label.innerText = excerpt.title
const br = w.document.createElement("br")
fieldset.append(e, label, br)
}
const submitBtn = w.document.createElement("input")
submitBtn.type = "submit"
submitBtn.value = "Download PDF"
fieldset.append(submitBtn)
w.document.body.append(fieldset)
submitBtn.onclick = async () => {
const checked: HTMLInputElement = w.document.querySelector("input:checked")
const id = checked.value
await score.setExcerptId(id)
const data = new Blob([await score.savePdf()])
saveAs(data, `${filename}-part-${id}.pdf`)
}
}
2020-03-08 21:36:37 +00:00
} else {
btn.onclick = () => {
window.open(url)
}
}
2019-11-29 23:07:43 +00:00
return btn
2019-11-03 20:21:59 +00:00
})
2019-11-03 20:01:29 +00:00
2019-11-29 23:07:43 +00:00
downloadBtn.replaceWith(...newDownloadBtns)
}
2019-11-03 19:13:06 +00:00
2019-11-29 23:07:43 +00:00
waitForDocumentLoaded().then(main)