BurmaldaMusic CLI v0.1.1
This commit is contained in:
commit
ff40a1b82d
5 changed files with 531 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
*.exe
|
||||
26
README.md
Normal file
26
README.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# bm — BurmaldaMusic Downloader
|
||||
|
||||
youtube / spotify / soundcloud / apple music → FLAC WAV MP3 OPUS MP4
|
||||
|
||||
## установка
|
||||
|
||||
скачай `bm.exe` из [releases](https://github.com/devsavsis/bm-downloader/releases) и кинь куда хочешь
|
||||
|
||||
зависимости (yt-dlp, ffmpeg) скачаются сами при первом запуске
|
||||
|
||||
## использование
|
||||
|
||||
```
|
||||
bm — интерактивный интерфейс
|
||||
bm <url> — скачать (FLAC по умолчанию)
|
||||
bm <url> --video — видео mp4
|
||||
```
|
||||
|
||||
## интерфейс
|
||||
|
||||
- **↑↓** — переключаться между вводом и форматом
|
||||
- **←→** — выбрать формат (FLAC WAV MP3 OPUS MP4)
|
||||
- **↵** — скачать
|
||||
- **ctrl+c** — выход
|
||||
|
||||
made by [im.savsis.xyz](https://im.savsis.xyz) ♥
|
||||
482
bm.js
Normal file
482
bm.js
Normal file
|
|
@ -0,0 +1,482 @@
|
|||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
|
||||
const { spawn } = require('child_process')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const https = require('https')
|
||||
const http = require('http')
|
||||
const os = require('os')
|
||||
|
||||
const BIN_DIR = path.join(os.homedir(), 'AppData', 'Roaming', 'bm-cli')
|
||||
const YTDLP = path.join(BIN_DIR, 'yt-dlp.exe')
|
||||
const FFMPEG = path.join(BIN_DIR, 'ffmpeg.exe')
|
||||
const OUT_DIR = path.join(os.homedir(), 'Music', 'BurmaldaMusic')
|
||||
const VERSION = '0.1.1'
|
||||
|
||||
const FORMATS = ['FLAC', 'WAV', 'MP3', 'OPUS', 'MP4 видео']
|
||||
const SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||||
|
||||
const c = {
|
||||
r: '\x1b[0m', b: '\x1b[1m', d: '\x1b[2m',
|
||||
pu: '\x1b[35m', ma: '\x1b[95m', cy: '\x1b[36m',
|
||||
wh: '\x1b[37m', gr: '\x1b[90m', gn: '\x1b[32m',
|
||||
ye: '\x1b[33m', re: '\x1b[31m',
|
||||
}
|
||||
|
||||
const st = {
|
||||
query: '', formatIdx: 0, focus: 'input',
|
||||
mode: 'idle', progress: 0, speed: '', eta: '',
|
||||
file: '', msg: '', setupMsg: '', spinI: 0,
|
||||
results: [], resultIdx: 0,
|
||||
}
|
||||
|
||||
let spinTimer = null
|
||||
let dlProc = null
|
||||
|
||||
function w(s) { process.stdout.write(String(s)) }
|
||||
|
||||
function render() {
|
||||
w('\x1b[2J\x1b[H\x1b[?25l')
|
||||
const L = []
|
||||
const p = s => L.push(s)
|
||||
|
||||
p('')
|
||||
p(` ${c.pu}${c.b} ██████╗ ███╗ ███╗${c.r}`)
|
||||
p(` ${c.pu}${c.b} ██╔══██╗████╗ ████║${c.r} ${c.wh}${c.b}BurmaldaMusic${c.r} ${c.gr}v${VERSION}${c.r}`)
|
||||
p(` ${c.pu}${c.b} ██████╔╝██╔████╔██║${c.r}`)
|
||||
p(` ${c.pu}${c.b} ██╔══██╗██║╚██╔╝██║${c.r} ${c.gr}youtube · spotify · soundcloud · apple${c.r}`)
|
||||
p(` ${c.pu}${c.b} ██████╔╝██║ ╚═╝ ██║${c.r}`)
|
||||
p(` ${c.pu}${c.b} ╚═════╝ ╚═╝ ╚═╝${c.r}`)
|
||||
p('')
|
||||
|
||||
if (st.mode === 'setup') {
|
||||
p(` ${c.cy}${SPIN[st.spinI % 10]}${c.r} ${c.gr}${st.setupMsg}${c.r}`)
|
||||
p('')
|
||||
p(` ${c.gr}первый запуск — скачиваем зависимости...${c.r}`)
|
||||
p('')
|
||||
|
||||
} else if (st.mode === 'searching') {
|
||||
p(` ${c.cy}${SPIN[st.spinI % 10]}${c.r} ${c.gr}ищем: ${st.query}${c.r}`)
|
||||
p('')
|
||||
|
||||
} else if (st.mode === 'results') {
|
||||
p(` ${c.gr}выбери трек ↑↓ ↵ скачать esc назад${c.r}`)
|
||||
p('')
|
||||
st.results.forEach((r, i) => {
|
||||
const sel = i === st.resultIdx
|
||||
const num = `${c.gr}${i + 1}.${c.r}`
|
||||
const title = sel ? `${c.pu}${c.b}▶ ${r.title}${c.r}` : ` ${r.title}`
|
||||
const dur = r.duration ? ` ${c.gr}${r.duration}${c.r}` : ''
|
||||
const ch = r.channel ? ` ${c.gr}${r.channel}${c.r}` : ''
|
||||
p(` ${num} ${title}${dur}${ch}`)
|
||||
})
|
||||
p('')
|
||||
p(` ${c.d}${'─'.repeat(52)}${c.r}`)
|
||||
p(` ${c.gr}↑↓ выбор ↵ скачать esc назад ctrl+c выход${c.r}`)
|
||||
p('')
|
||||
|
||||
} else if (st.mode === 'downloading') {
|
||||
p(` ${c.pu}${c.b}▸ скачиваем...${c.r}`)
|
||||
p('')
|
||||
const f = Math.min(20, Math.floor(st.progress / 5))
|
||||
const bar = `${c.pu}${'█'.repeat(f)}${c.r}${c.gr}${'░'.repeat(20-f)}${c.r}`
|
||||
const pct = `${c.b}${st.progress.toFixed(1)}%${c.r}`
|
||||
const sp = st.speed ? `${c.gr}${st.speed}${c.r}` : ''
|
||||
const et = st.eta ? `${c.gr}ETA ${st.eta}${c.r}` : ''
|
||||
p(` ${bar} ${pct} ${sp} ${et}`)
|
||||
p('')
|
||||
if (st.msg) p(` ${c.gr}${st.msg}${c.r}`)
|
||||
p('')
|
||||
p(` ${c.d}${'─'.repeat(52)}${c.r}`)
|
||||
p(` ${c.gr}ctrl+c — отмена загрузки${c.r}`)
|
||||
p('')
|
||||
|
||||
} else {
|
||||
const iF = st.focus === 'input'
|
||||
const fF = st.focus === 'format'
|
||||
const iB = iF ? c.ma : c.gr
|
||||
const fB = fF ? c.ma : c.gr
|
||||
|
||||
const qDisp = st.query.length > 44 ? '…' + st.query.slice(-43) : st.query
|
||||
const cursor = iF ? `${c.ma}█${c.r}` : ' '
|
||||
|
||||
p(` ${iB}┌─ url или название трека ${'─'.repeat(22)}┐${c.r}`)
|
||||
p(` ${iB}│${c.r} ${iF ? c.wh : c.gr}▸ ${qDisp}${c.r}${cursor}${' '.repeat(Math.max(0, 44 - qDisp.length))}${iB}│${c.r}`)
|
||||
p(` ${iB}└${'─'.repeat(50)}┘${c.r}`)
|
||||
p('')
|
||||
|
||||
const fmtRow = FORMATS.map((f, i) =>
|
||||
i === st.formatIdx
|
||||
? `${c.pu}${c.b}[ ${f} ]${c.r}`
|
||||
: `${c.gr}${f}${c.r}`
|
||||
).join(` `)
|
||||
p(` ${fB}формат${c.r} ${c.gr}←${c.r} ${fmtRow} ${c.gr}→${c.r}`)
|
||||
p('')
|
||||
|
||||
if (st.mode === 'done') {
|
||||
p(` ${c.gn}${c.b}✓ готово!${c.r} ${c.gr}${st.file}${c.r}`)
|
||||
p('')
|
||||
} else if (st.mode === 'error') {
|
||||
p(` ${c.re}✗ ошибка${c.r} ${c.gr}${st.msg}${c.r}`)
|
||||
p('')
|
||||
} else {
|
||||
p('')
|
||||
}
|
||||
|
||||
p(` ${c.d}${'─'.repeat(52)}${c.r}`)
|
||||
p(` ${c.gr}↑↓ навигация ←→ формат ↵ скачать ctrl+c выход${c.r}`)
|
||||
p(` ${c.gr}made by ${c.r}${c.pu}${c.b}im.savsis.xyz${c.r} ${c.re}♥${c.r}`)
|
||||
p('')
|
||||
}
|
||||
|
||||
w(L.join('\n'))
|
||||
}
|
||||
|
||||
function startSpinner(msg) {
|
||||
st.setupMsg = msg
|
||||
if (spinTimer) return
|
||||
spinTimer = setInterval(() => { st.spinI++; render() }, 100)
|
||||
}
|
||||
function stopSpinner() {
|
||||
if (spinTimer) { clearInterval(spinTimer); spinTimer = null }
|
||||
}
|
||||
|
||||
function dlFile(url, dest) {
|
||||
return new Promise((res, rej) => {
|
||||
const follow = u => {
|
||||
const mod = u.startsWith('https') ? https : http
|
||||
mod.get(u, r => {
|
||||
if ([301,302,303,307,308].includes(r.statusCode)) return follow(r.headers.location)
|
||||
const tmp = dest + '.tmp'
|
||||
const out = fs.createWriteStream(tmp)
|
||||
r.pipe(out)
|
||||
out.on('finish', () => { fs.renameSync(tmp, dest); res() })
|
||||
out.on('error', rej)
|
||||
r.on('error', rej)
|
||||
}).on('error', rej)
|
||||
}
|
||||
follow(url)
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureBins() {
|
||||
if (!fs.existsSync(BIN_DIR)) fs.mkdirSync(BIN_DIR, { recursive: true })
|
||||
|
||||
if (!fs.existsSync(YTDLP)) {
|
||||
startSpinner('скачиваем yt-dlp.exe ...')
|
||||
await dlFile('https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe', YTDLP)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(FFMPEG)) {
|
||||
startSpinner('скачиваем ffmpeg.exe ...')
|
||||
const zipPath = path.join(BIN_DIR, 'ffmpeg.zip')
|
||||
const extractDir = path.join(BIN_DIR, 'ffmpeg-extract')
|
||||
await dlFile('https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl.zip', zipPath)
|
||||
startSpinner('распаковываем ffmpeg ...')
|
||||
await new Promise((res, rej) => {
|
||||
const ps = spawn('powershell', ['-Command', `Expand-Archive -Path '${zipPath}' -DestinationPath '${extractDir}' -Force`])
|
||||
ps.on('close', res)
|
||||
ps.on('error', rej)
|
||||
})
|
||||
const found = fs.readdirSync(extractDir, { recursive: true }).find(f => String(f).endsWith('ffmpeg.exe'))
|
||||
if (found) fs.copyFileSync(path.join(extractDir, String(found)), FFMPEG)
|
||||
try { fs.rmSync(zipPath); fs.rmSync(extractDir, { recursive: true }) } catch {}
|
||||
}
|
||||
|
||||
stopSpinner()
|
||||
}
|
||||
|
||||
function parseProgress(line) {
|
||||
const pm = line.match(/(\d+\.?\d*)%/)
|
||||
if (!pm) return false
|
||||
st.progress = parseFloat(pm[1])
|
||||
st.speed = line.match(/at\s+([\d.]+\s*\S*\/s)/)?.[1] || st.speed
|
||||
st.eta = line.match(/ETA\s+(\d+:\d+)/)?.[1] || st.eta
|
||||
return true
|
||||
}
|
||||
|
||||
function doDownload(url, fmtIdx, retry403 = false) {
|
||||
if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true })
|
||||
|
||||
const fmt = FORMATS[fmtIdx]
|
||||
const isVideo = fmt === 'MP4 видео'
|
||||
const isApple = url.includes('music.apple.com')
|
||||
const isSpotify = url.includes('spotify.com')
|
||||
const ffmpegDir = path.dirname(FFMPEG)
|
||||
|
||||
let args = [url, '--no-playlist', '--continue', '--ffmpeg-location', ffmpegDir, '--newline', '--no-colors']
|
||||
|
||||
if (isVideo) {
|
||||
args.push('-f', 'bestvideo[height<=1080]+bestaudio/best', '-o', `${OUT_DIR}\\%(title)s.%(ext)s`, '--merge-output-format', 'mp4')
|
||||
} else {
|
||||
const audioFmt = { FLAC:'flac', WAV:'wav', MP3:'mp3', OPUS:'opus' }[fmt] || 'flac'
|
||||
args.push('-x', '--audio-format', audioFmt, '--audio-quality', '0',
|
||||
'-o', `${OUT_DIR}\\%(uploader)s - %(title)s.%(ext)s`,
|
||||
'--embed-thumbnail', '--embed-metadata', '--add-metadata')
|
||||
if (!isApple && !isSpotify) {
|
||||
args.push('--extractor-args', 'youtube:player_client=web_creator,web',
|
||||
'--user-agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')
|
||||
}
|
||||
}
|
||||
|
||||
if (isApple || retry403) args.push('--cookies-from-browser', 'chrome')
|
||||
|
||||
st.mode = 'downloading'
|
||||
st.progress = 0; st.speed = ''; st.eta = ''; st.msg = ''
|
||||
render()
|
||||
|
||||
let got403 = false
|
||||
dlProc = spawn(YTDLP, args)
|
||||
|
||||
const onData = d => {
|
||||
const line = d.toString()
|
||||
parseProgress(line)
|
||||
const dm = line.match(/(?:Destination|Merging formats into):\s*(.+)/)
|
||||
if (dm) st.file = path.basename(dm[1].trim())
|
||||
if (line.includes('ExtractAudio')) st.msg = 'конвертируем...'
|
||||
if (line.includes('Merger')) st.msg = 'мержим...'
|
||||
render()
|
||||
}
|
||||
|
||||
dlProc.stdout.on('data', onData)
|
||||
dlProc.stderr.on('data', d => {
|
||||
const line = d.toString()
|
||||
if (parseProgress(line)) { render(); return }
|
||||
if (!got403 && (line.includes('HTTP Error 403') || line.includes('Sign in'))) {
|
||||
got403 = true
|
||||
dlProc.kill()
|
||||
st.msg = '403 — retry с куками Chrome...'
|
||||
render()
|
||||
doDownload(url, fmtIdx, true)
|
||||
}
|
||||
})
|
||||
|
||||
dlProc.on('close', code => {
|
||||
if (got403) return
|
||||
dlProc = null
|
||||
if (code === 0) {
|
||||
st.mode = 'done'; st.progress = 100
|
||||
} else if (code !== null) {
|
||||
st.mode = 'error'
|
||||
st.msg = `ошибка (код ${code})`
|
||||
}
|
||||
render()
|
||||
})
|
||||
}
|
||||
|
||||
async function searchYT(query) {
|
||||
return new Promise(resolve => {
|
||||
const args = [`ytsearch5:${query}`, '--flat-playlist', '-J', '--no-playlist', '--no-colors']
|
||||
const proc = spawn(YTDLP, args)
|
||||
let out = ''
|
||||
proc.stdout.on('data', d => { out += d })
|
||||
proc.on('close', () => {
|
||||
try {
|
||||
const data = JSON.parse(out)
|
||||
const entries = data.entries || []
|
||||
resolve(entries.map(e => ({
|
||||
url: `https://www.youtube.com/watch?v=${e.id}`,
|
||||
title: e.title || e.id,
|
||||
duration: e.duration ? `${Math.floor(e.duration/60)}:${String(e.duration%60).padStart(2,'0')}` : '',
|
||||
channel: e.uploader || e.channel || ''
|
||||
})))
|
||||
} catch { resolve([]) }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleKey(key) {
|
||||
const k = typeof key === 'string' ? key : key.toString('utf8')
|
||||
|
||||
if (k === '\x03') {
|
||||
if (st.mode === 'downloading' && dlProc) {
|
||||
try { dlProc.kill() } catch {}
|
||||
dlProc = null
|
||||
st.mode = 'idle'; st.msg = ''; st.file = ''
|
||||
render()
|
||||
} else if (st.mode === 'searching') {
|
||||
stopSpinner()
|
||||
st.mode = 'idle'
|
||||
render()
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (k === '\x1b') {
|
||||
if (st.mode === 'results') { st.mode = 'idle'; render() }
|
||||
return
|
||||
}
|
||||
|
||||
if (st.mode === 'setup' || st.mode === 'downloading' || st.mode === 'searching') return
|
||||
|
||||
if (st.mode === 'results') {
|
||||
if (k === '\x1b[A') { st.resultIdx = Math.max(0, st.resultIdx - 1); render() }
|
||||
if (k === '\x1b[B') { st.resultIdx = Math.min(st.results.length - 1, st.resultIdx + 1); render() }
|
||||
if (k === '\r' || k === '\n') {
|
||||
const r = st.results[st.resultIdx]
|
||||
if (r) doDownload(r.url, st.formatIdx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (st.mode === 'done' || st.mode === 'error') {
|
||||
st.mode = 'idle'; st.msg = ''; st.file = ''
|
||||
render(); return
|
||||
}
|
||||
|
||||
if (k === '\x1b[A') { st.focus = 'input'; render(); return }
|
||||
if (k === '\x1b[B') { st.focus = 'format'; render(); return }
|
||||
|
||||
if (k === '\x1b[C' && st.focus === 'format') { st.formatIdx = (st.formatIdx + 1) % FORMATS.length; render(); return }
|
||||
if (k === '\x1b[D' && st.focus === 'format') { st.formatIdx = (st.formatIdx - 1 + FORMATS.length) % FORMATS.length; render(); return }
|
||||
|
||||
if (k === '\r' || k === '\n') {
|
||||
if (!st.query.trim()) return
|
||||
const q = st.query.trim()
|
||||
if (/^https?:\/\//.test(q)) {
|
||||
doDownload(q, st.formatIdx)
|
||||
} else {
|
||||
st.mode = 'searching'
|
||||
startSpinner(`ищем: ${q}`)
|
||||
searchYT(q).then(results => {
|
||||
stopSpinner()
|
||||
if (!results.length) {
|
||||
st.mode = 'error'; st.msg = 'ничего не найдено'
|
||||
} else {
|
||||
st.results = results; st.resultIdx = 0; st.mode = 'results'
|
||||
}
|
||||
render()
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (st.focus === 'input') {
|
||||
if (k === '\x7f' || k === '\x08') {
|
||||
st.query = st.query.slice(0, -1)
|
||||
} else if (k.length >= 1 && k.charCodeAt(0) >= 32) {
|
||||
st.query += k
|
||||
}
|
||||
render()
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
stopSpinner()
|
||||
if (dlProc) try { dlProc.kill() } catch {}
|
||||
w('\x1b[?25h\x1b[0m\n')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
process.on('SIGINT', cleanup)
|
||||
process.on('exit', () => w('\x1b[?25h\x1b[0m'))
|
||||
|
||||
const LOGO_LINES = [
|
||||
``,
|
||||
` ${c.pu}${c.b} ██████╗ ███╗ ███╗${c.r}`,
|
||||
` ${c.pu}${c.b} ██╔══██╗████╗ ████║${c.r}`,
|
||||
` ${c.pu}${c.b} ██████╔╝██╔████╔██║${c.r}`,
|
||||
` ${c.pu}${c.b} ██╔══██╗██║╚██╔╝██║${c.r}`,
|
||||
` ${c.pu}${c.b} ██████╔╝██║ ╚═╝ ██║${c.r}`,
|
||||
` ${c.pu}${c.b} ╚═════╝ ╚═╝ ╚═╝${c.r}`,
|
||||
``
|
||||
]
|
||||
|
||||
const HELP_LINES = [
|
||||
[` ${c.b}${c.wh}КАК ПОЛЬЗОВАТЬСЯ${c.r}`, ''],
|
||||
[``, ''],
|
||||
[` ${c.pu}bm${c.r}`, `открыть интерфейс`],
|
||||
[` ${c.pu}bm${c.r} ${c.ma}<url>${c.r}`, `скачать FLAC`],
|
||||
[` ${c.pu}bm${c.r} ${c.ma}<url>${c.r} ${c.gr}--video${c.r}`, `скачать mp4`],
|
||||
[` ${c.pu}bm${c.r} ${c.ma}<название трека>${c.r}`, `поиск → выбор → скачать`],
|
||||
[``, ''],
|
||||
[` ${c.b}${c.wh}ИСТОЧНИКИ${c.r}`, ''],
|
||||
[``, ''],
|
||||
[` ${c.gn}✓${c.r} YouTube · Spotify · SoundCloud · Apple Music`, ''],
|
||||
[``, ''],
|
||||
[` ${c.b}${c.wh}ФОРМАТЫ${c.r}`, ''],
|
||||
[``, ''],
|
||||
[` ${c.gr}FLAC WAV MP3 OPUS MP4 — выбор стрелками ← →${c.r}`, ''],
|
||||
[``, ''],
|
||||
[` ${c.d}${'─'.repeat(46)}${c.r}`, ''],
|
||||
[``, ''],
|
||||
[` ${c.gr}made by ${c.r}${c.pu}${c.b}im.savsis.xyz${c.r} ${c.re}♥${c.r}`, ''],
|
||||
[``, ''],
|
||||
]
|
||||
|
||||
async function writeAnimated(s, delay) {
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] === '\x1b') { const e = s.indexOf('m', i); w(s.slice(i, e+1)); i = e }
|
||||
else { w(s[i]); await sleep(delay) }
|
||||
}
|
||||
}
|
||||
|
||||
async function intro() {
|
||||
w('\x1b[2J\x1b[H\x1b[?25l')
|
||||
|
||||
for (const line of LOGO_LINES) {
|
||||
const raw = line.replace(/\x1b\[[0-9;]*m/g, '')
|
||||
await writeAnimated(line, raw.trim() ? 3 : 0)
|
||||
w('\n')
|
||||
await sleep(28)
|
||||
}
|
||||
|
||||
await writeAnimated(` ${c.gr}BurmaldaMusic CLI v${VERSION} — yt · spotify · sc · apple${c.r}`, 13)
|
||||
w('\n')
|
||||
await sleep(70)
|
||||
|
||||
await writeAnimated(` ${c.d}${'─'.repeat(46)}${c.r}`, 5)
|
||||
w('\n')
|
||||
await sleep(50)
|
||||
|
||||
for (const [left, right] of HELP_LINES) {
|
||||
if (!left) { w('\n'); await sleep(12); continue }
|
||||
w(left)
|
||||
if (right) w(` ${c.gr}${right}${c.r}`)
|
||||
w('\n')
|
||||
await sleep(28)
|
||||
}
|
||||
|
||||
await sleep(700)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length && args[0].startsWith('http')) {
|
||||
w('\x1b[?25l')
|
||||
await ensureBins()
|
||||
doDownload(args[0], args.includes('--video') ? 4 : 0)
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.resume()
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.on('data', handleKey)
|
||||
return
|
||||
}
|
||||
|
||||
await intro()
|
||||
|
||||
st.mode = 'setup'
|
||||
render()
|
||||
startSpinner('проверяем зависимости...')
|
||||
await ensureBins()
|
||||
stopSpinner()
|
||||
|
||||
st.mode = 'idle'
|
||||
render()
|
||||
|
||||
process.stdin.setRawMode(true)
|
||||
process.stdin.resume()
|
||||
process.stdin.setEncoding('utf8')
|
||||
process.stdin.on('data', handleKey)
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
stopSpinner()
|
||||
w(`\x1b[?25h\x1b[0m\nОшибка: ${e.message}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
BIN
icon.ico
Normal file
BIN
icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "bm-downloader",
|
||||
"version": "0.1.1",
|
||||
"description": "BurmaldaMusic CLI — youtube/spotify/soundcloud/apple → FLAC",
|
||||
"author": "ImSavsis <dev@savsis.xyz> (https://im.savsis.xyz)",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"bm": "bm.js"
|
||||
},
|
||||
"pkg": {
|
||||
"targets": [
|
||||
"node18-win-x64"
|
||||
],
|
||||
"outputPath": "dist",
|
||||
"assets": []
|
||||
},
|
||||
"devDependencies": {
|
||||
"png-to-ico": "^3.0.2"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue