musescore-downloader/src/cli.ts

207 lines
6.1 KiB
TypeScript
Raw Normal View History

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