UNPKG

lowfi

Version:

🎵 Play Lofi music through your terminal.

393 lines (369 loc) • 11.1 kB
#!/usr/bin/env node // src/index.ts import { Command } from "commander"; // src/commands/play.ts import inquirer from "inquirer"; import chalk2 from "chalk"; // src/data/playlists.ts var playlists = [ { title: "Lofi Girl \u2014 Dark Ambient Playlist", url: "https://soundcloud.com/lofi_girl/sets/dark-ambient-music-to-escape" }, { title: "Lofi Girl \u2014 Peaceful Piano Playlist", url: "https://soundcloud.com/lofi_girl/sets/peaceful-piano-music-to-focus" }, { title: "Lofi Girl \u2014 Synthwave Ambient Playlist", url: "https://soundcloud.com/lofi_girl/sets/synthwave-ambient-chill-music" }, { title: "Lofi Girl \u2014 Lofi Reading Playlist", url: "https://soundcloud.com/lofi_girl/sets/lofi-reading-2024" }, { title: "Chillhop Music \u2014 Night Time Cruise Playlist", url: "https://soundcloud.com/chillhopdotcom/sets/night-time-cruise" }, { title: "Chillhop Music \u2014 Golden Hour Playlist", url: "https://soundcloud.com/chillhopdotcom/sets/golden-hour" }, { title: "Studio Ghibli \u2014 Lofi (Study, Sleep, Relax)", url: "https://soundcloud.com/luvemenot/sets/studio-ghibli-lofi-study-sleep-relax" } ]; // src/lib/stream.ts import ora2 from "ora"; import { Soundcloud } from "soundcloud.ts"; // src/lib/logger.ts import logSymbols from "log-symbols"; function info(message) { console.log(logSymbols.info, message); } function error(message) { console.error(logSymbols.error, message); } // src/lib/play.ts import ora from "ora"; import chalk from "chalk"; // src/helpers/number.ts function padNumber(num) { return num.toString().padStart(2, "0"); } // src/helpers/time.ts function formatTime(seconds) { const mins = padNumber(Math.floor(seconds / 60)); const secs = padNumber(seconds % 60); return `${mins}:${secs}`; } // src/lib/middleware.ts import { Transform } from "stream"; var MiddlewareStream = class extends Transform { constructor(onFirstData, options) { super(options); this.firstChunkLogged = false; this.onFirstData = onFirstData; } _transform(chunk, encoding, callback) { if (!this.firstChunkLogged) { this.onFirstData(); this.firstChunkLogged = true; } this.push(chunk); callback(); } }; // src/lib/ffmpeg.ts import ffmpeg from "fluent-ffmpeg"; function createFfmpegStream(stream2, onEnd, onError) { const ffmpegStream = ffmpeg(stream2).toFormat("s16le").audioFrequency(44100).audioChannels(2).on("error", onError).on("end", onEnd); return ffmpegStream; } // src/lib/speaker.ts import Speaker from "speaker"; function createSpeaker() { const speaker = new Speaker({ bitDepth: 16, channels: 2, sampleRate: 44100 }); return speaker; } // src/lib/volume.ts import Volume from "pcm-volume"; function createVolume(amount) { const volume = new Volume(); volume.setVolume(amount); return volume; } // src/lib/play.ts function play(title, volumeAmount, stream2) { return new Promise((resolve, reject) => { let timer = null; const spinner = ora(`Now playing "${chalk.bold.white(title)}" (00:00)`); let startTime; try { const speaker = createSpeaker(); const volume = createVolume(volumeAmount); const middleware = new MiddlewareStream(() => { startTime = Date.now(); spinner.start(); timer = setInterval(() => { const elapsedTime = Math.floor((Date.now() - startTime) / 1e3); spinner.text = `Now playing "${chalk.bold.white(title)}" (${formatTime(elapsedTime)})`; }, 1e3); }); const ffmpegStream = createFfmpegStream( stream2, () => { if (timer) clearInterval(timer); const elapsedTime = Math.floor((Date.now() - startTime) / 1e3); spinner.succeed( `Finished "${chalk.bold.white(title)}" (${formatTime(elapsedTime)})` ); resolve(true); }, (error2) => { if (timer) clearInterval(timer); reject(error2); } ); ffmpegStream.pipe(middleware).pipe(volume).pipe(speaker); } catch (error2) { reject(error2); } }); } // src/helpers/random.ts function random(min, max) { return Math.floor(Math.random() * (max - min) + min); } function pick(array) { const randomIndex = random(0, array.length); return array[randomIndex]; } function shuffle(array) { return array.map((value) => ({ sort: Math.random(), value })).sort((a, b) => a.sort - b.sort).map(({ value }) => value); } // src/lib/key.ts import sckey from "soundcloud-key-fetch"; async function fetchKey() { return sckey.fetchKey(); } // src/lib/stream.ts async function stream(volume, url) { let spinner; try { spinner = ora2("Fetching the playlist from SoundCloud").start(); const clientId = await fetchKey(); const soundcloud = new Soundcloud(clientId); const playlist = await soundcloud.playlists.get(url); if (playlist?.tracks) { spinner.succeed( `Fetched the playlist from SoundCloud (${playlist.track_count} Tracks)` ); console.log(""); const { tracks } = playlist; const shuffled = shuffle(tracks); let index = 0; while (true) { const track = shuffled[index]; const stream2 = await soundcloud.util.streamTrack(track.permalink_url); await play(track.title, volume, stream2); index = (index + 1) % shuffled.length; } } } catch (err) { spinner?.stop(); if (err instanceof Error) { error(`Error: ${err.message}`); } else { error(`Something went wrong.`); } } } // src/lib/banner.ts import figlet from "figlet"; import gradient from "gradient-string"; async function printBanner() { return new Promise((resolve, reject) => { figlet("Lowfi", { font: "O8" }, (err, data) => { if (err) return reject(err); console.log(gradient(["blue", "purple"]).multiline(data)); resolve(true); }); }); } // src/commands/play.ts async function play2({ random: random2, volume }) { await printBanner(); if (volume) { const volumeNumber = Number(volume); if (volumeNumber < 0 || volumeNumber > 1) { return error("Volume should be between 0 and 1"); } } if (random2) { const { title, url } = pick(playlists); info(`Selected playlist: ${chalk2.bold.white(title)} `); return stream(Number(volume), url); } inquirer.prompt([ { choices: playlists.map((playlist) => playlist.title), message: "Select a lofi playlist to play:", name: "playlist", type: "list" } ]).then((answers) => { const playlist = playlists.filter( (playlist2) => playlist2.title === answers.playlist )[0]; stream(Number(volume), playlist.url); }); } // src/commands/donate.ts import open from "open"; import inquirer2 from "inquirer"; import chalk3 from "chalk"; function donate() { console.log(`${chalk3.green("[\u2022\u1D17\u2022]")} Hello There!`); console.log("Enjoyed Lowfi? Please consider buying me a coffee!"); console.log("Link: https://buymeacoffee.com/remvze"); inquirer2.prompt([ { message: "Open the link in your browser?", name: "open", type: "confirm" } ]).then((answers) => { if (answers.open) open("https://buymeacoffee.com/remvze"); }); } // src/commands/open.ts import inquirer3 from "inquirer"; import open2 from "open"; import ora3 from "ora"; async function openCommand() { await printBanner(); inquirer3.prompt([ { choices: playlists.map((playlist) => playlist.title), message: "Select a lofi playlist to open:", name: "playlist", type: "list" } ]).then((answers) => { const playlist = playlists.filter( (playlist2) => playlist2.title === answers.playlist )[0]; const spinner = ora3("Opening the playlist in your browser.").start(); open2(playlist.url).then( () => spinner.succeed("Opened the playlist in your browser.") ); }); } // package.json var package_default = { name: "lowfi", version: "0.9.1", description: "\u{1F3B5} Play Lofi music through your terminal.", main: "dist/bin/lowfi.js", type: "module", bin: { lowfi: "dist/bin/lowfi.js" }, files: [ "dist/bin/lowfi.js" ], scripts: { test: 'echo "Error: no test specified" && exit 1', lint: "eslint . --ext .js,.ts", "lint:fix": "npm run lint -- --fix", format: "prettier . --write", prepare: "husky", commit: "git-cz", release: "standard-version --no-verify", "release:major": "npm run release -- --release-as major", "release:minor": "npm run release -- --release-as minor", "release:patch": "npm run release -- --release-as patch", build: "tsup-node", dev: "tsup-node --watch", prepublishOnly: "npm run build" }, keywords: [ "lofi", "cli" ], author: "MAZE", repository: { type: "git", url: "git+https://github.com/remvze/lowfi.git" }, bugs: { url: "https://github.com/remvze/lowfi/issues" }, homepage: "https://github.com/remvze/lowfi#readme", license: "MIT", devDependencies: { "@commitlint/cli": "19.3.0", "@commitlint/config-conventional": "19.2.2", "@tsconfig/node16": "16.1.3", "@types/figlet": "1.5.8", "@types/fluent-ffmpeg": "2.1.24", "@types/gradient-string": "1.1.6", "@types/inquirer": "9.0.7", "@types/node": "20.12.12", "@types/pcm-volume": "1.0.2", "@typescript-eslint/eslint-plugin": "7.9.0", "@typescript-eslint/parser": "7.9.0", commitizen: "4.3.0", "cz-conventional-changelog": "3.3.0", eslint: "8.57.0", "eslint-config-prettier": "9.1.0", "eslint-import-resolver-alias": "1.1.2", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "2.29.1", "eslint-plugin-node": "11.1.0", "eslint-plugin-prettier": "5.1.3", "eslint-plugin-sort-destructure-keys": "2.0.0", "eslint-plugin-sort-keys-fix": "1.1.2", "eslint-plugin-typescript-sort-keys": "3.2.0", husky: "9.0.11", "lint-staged": "15.2.2", prettier: "3.2.5", "standard-version": "9.5.0", tsup: "8.0.2" }, dependencies: { chalk: "5.3.0", commander: "12.0.0", figlet: "1.7.0", "fluent-ffmpeg": "2.1.3", "gradient-string": "2.0.2", inquirer: "9.2.21", "log-symbols": "6.0.0", open: "10.1.0", ora: "8.0.1", "pcm-volume": "1.0.0", "soundcloud-key-fetch": "1.0.13", "soundcloud.ts": "0.6.5", speaker: "0.5.5" } }; // src/index.ts var program = new Command(); program.name("lowfi").description("A CLI tool to play lofi music").version(package_default.version); program.command("play").description("Play a lofi playlist").option("-r, --random", "Select a playlist randomly").option("-v, --volume <number>", "Set the volume", "0.5").action(play2); program.command("donate").description("Donate to the creator of Lowfi").action(donate); program.command("open").description("Open a lofi playlist in your browser").action(openCommand); // bin/lowfi.ts program.parse(process.argv);