From ff40a1b82d0988f21a50f646dadaf6c334af01d6 Mon Sep 17 00:00:00 2001 From: devsavsis Date: Sun, 30 Aug 2026 03:30:10 +0500 Subject: [PATCH] BurmaldaMusic CLI v0.1.1 --- .gitignore | 3 + README.md | 26 +++ bm.js | 482 +++++++++++++++++++++++++++++++++++++++++++++++++++ icon.ico | Bin 0 -> 6893 bytes package.json | 20 +++ 5 files changed, 531 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 bm.js create mode 100644 icon.ico create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e03b3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +*.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6ed1d3 --- /dev/null +++ b/README.md @@ -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 — скачать (FLAC по умолчанию) +bm --video — видео mp4 +``` + +## интерфейс + +- **↑↓** — переключаться между вводом и форматом +- **←→** — выбрать формат (FLAC WAV MP3 OPUS MP4) +- **↵** — скачать +- **ctrl+c** — выход + +made by [im.savsis.xyz](https://im.savsis.xyz) ♥ diff --git a/bm.js b/bm.js new file mode 100644 index 0000000..f55da61 --- /dev/null +++ b/bm.js @@ -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}${c.r}`, `скачать FLAC`], + [` ${c.pu}bm${c.r} ${c.ma}${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) +}) diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..08116fa82992e43ca03713c15557c6f3f7d87028 GIT binary patch literal 6893 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 LFT^eOk0Ady`=?21 literal 0 HcmV?d00001 diff --git a/package.json b/package.json new file mode 100644 index 0000000..6eebcc3 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "bm-downloader", + "version": "0.1.1", + "description": "BurmaldaMusic CLI — youtube/spotify/soundcloud/apple → FLAC", + "author": "ImSavsis (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" + } +}