UNPKG

termice

Version:

Simple terminal icecast player

356 lines (352 loc) 11.4 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const minimist_1 = __importDefault(require("minimist")); const Util = __importStar(require("./lib/util")); const Icecast = __importStar(require("./lib/icecast")); const Shoutcast = __importStar(require("./lib/shoutcast")); const Radio = __importStar(require("./lib/radio")); const mplayer_1 = __importDefault(require("./lib/mplayer")); async function force_quit(s, smth) { await mplayer_1.default.quit(); s.scr.destroy(); console.log(smth); throw Error('Force exit'); process.exit(); } function print_usage_and_quit() { console.log(` Usage: termice [ARGS] Options -h: Show this help -q: Query -s: Source [Icecast | Shoutcast | Radio] Commands [:command | query] q : Quit Sources Icecast : https://dir.xiph.org Shoutcast : https://directory.shoutcast.com Radio : http://www.radio-browser.info This source also allows to filter results by name, tag, country or language, which can be achieved by querying for filter:query `); process.exit(); } async function quit(s, line = '') { await mplayer_1.default.quit(); s.scr.destroy(); if (line) throw Error(line); process.exit(); } function set_flags(s, flags) { const _flags = Object.assign(Object.assign({}, s.flags), flags); return Object.freeze({ scr: s.scr, comp: s.comp, config: s.config, stream_list: s.stream_list, flags: Object.freeze(_flags) }); } function set_stream_list(s, list) { return Object.freeze({ scr: s.scr, comp: s.comp, config: s.config, stream_list: list.slice(), flags: s.flags }); } async function stop(s) { const s2 = set_flags(s, { is_paused: false }); await mplayer_1.default.stop(); set_header(s2); return s2; } async function pause(s) { if (mplayer_1.default.is_init) { const s2 = set_flags(s, { is_paused: !s.flags.is_paused }); await mplayer_1.default.pause(); set_header(s2); return s2; } else { return s; } } async function play_url(s, entry) { try { const s2 = set_flags(s, { is_paused: false }); await mplayer_1.default.play(entry.url, entry.is_playlist); set_header(s2); render(s2); return s2; } catch (err) { force_quit(s, err); return s; } } function set_header(s) { const line = Util.format_header(s.flags.last_tab, s.config.header, s.config.pause_key, s.flags.is_paused); s.comp.header.setContent(line); } function set_rows(s, rows) { s.comp.stream_table.setData(rows); } function query_streams(s, search) { switch (s.flags.source) { case 'Icecast': { return Icecast.search_xiph(search); } case 'Shoutcast': { return Shoutcast.search_shoutcast(search); } case 'Radio': { const has_mode = search.includes(':'); if (has_mode) { const [mode, subsearch] = search.split(':'); switch (mode) { case 'name': case 'tag': case 'country': case 'language': { return Radio.search_radio(subsearch, mode); } default: { return new Promise((resolve) => { resolve([Util.error_entry('Not a valid mode: ' + mode, s.flags.source)]); }); } } } else { return Radio.search_radio(search, 'name'); } } default: { force_quit(s, 'Not a valid source: ' + s.flags.source); return query_streams(s, s.flags.last_search); } } } async function search_streams(s, search) { s.comp.loading.load('Searching: ' + search); const s2 = set_flags(s, { last_search: search, last_tab: s.flags.source }); const list = await query_streams(s2, search); const s3 = set_stream_list(s2, list); const rows = Util.format_stream_list(s3, list, s3.config.table_headers[s3.flags.source], search); if (rows !== false) { set_rows(s3, rows); s3.comp.loading.stop(); set_header(s3); render(s3); } return s3; } async function refresh_table(s) { return await search_streams(s, s.flags.last_search); } function show_input(s) { const s2 = set_flags(s, { last_tab: 'Search' }); s2.comp.input.show(); s2.comp.input.focus(); set_header(s2); return s2; } function hide_input(s) { const s2 = set_flags(s, { last_tab: s.flags.source }); s2.comp.input.hide(); s2.comp.stream_table.focus(); set_header(s2); render(s2); return s2; } async function input_handler(s, line) { if (line === ':q' || line === ":Q" || line === ":quit" || line === ":exit") { await quit(s); return s; } else { s.comp.input.clearValue(); const s2 = hide_input(s); if (line !== '') { return await search_streams(s2, line); } else { return s; } } } function render(s) { s.scr.render(); } function remove_events(s) { s.scr.unkey(s.config.keys.screen.quit); s.scr.unkey(s.config.keys.screen.pause); s.scr.unkey(s.config.keys.screen.stop); s.scr.unkey(s.config.keys.screen.vol_up); s.scr.unkey(s.config.keys.screen.vol_down); s.scr.unkey(s.config.keys.screen.input); s.scr.unkey(s.config.keys.screen.icecast); s.scr.unkey(s.config.keys.screen.shoutcast); s.scr.unkey(s.config.keys.screen.radio); s.scr.unkey(s.config.keys.screen.refresh); s.comp.stream_table.unkey(s.config.keys.screen.input); s.comp.stream_table.unkey(s.config.keys.stream_table.debug); s.comp.stream_table.unkey(s.config.keys.stream_table.scroll_down); s.comp.stream_table.unkey(s.config.keys.stream_table.scroll_up); s.comp.input.unkey(s.config.keys.screen.input); s.comp.input.unkey(s.config.keys.input.submit); delete s.comp.stream_table._events.select; } function set_events(s) { remove_events(s); s.scr.onceKey(s.config.keys.screen.quit, () => quit(s)); s.scr.onceKey(s.config.keys.screen.pause, async () => { s.comp.loading.load('Pausing'); const s2 = await pause(s); s.comp.loading.stop(); render(s); set_events(s2); }); s.scr.onceKey(s.config.keys.screen.stop, async () => { s.comp.loading.load('Stopping'); const s2 = await stop(s); s.comp.loading.stop(); render(s2); set_events(s2); }); s.scr.key(s.config.keys.screen.vol_up, () => mplayer_1.default.volume('+1')); s.scr.key(s.config.keys.screen.vol_down, () => mplayer_1.default.volume('-1')); s.scr.onceKey(s.config.keys.screen.icecast, async () => { const s2 = await refresh_table(set_flags(s, { source: 'Icecast' })); set_events(s2); }); s.scr.onceKey(s.config.keys.screen.shoutcast, async () => { const s2 = await refresh_table(set_flags(s, { source: 'Shoutcast' })); set_events(s2); }); s.scr.onceKey(s.config.keys.screen.radio, async () => { const s2 = await refresh_table(set_flags(s, { source: 'Radio' })); set_events(s2); }); s.scr.onceKey(s.config.keys.screen.refresh, async () => { const s2 = await refresh_table(s); set_events(s2); }); s.comp.stream_table.on('select', async (_, i) => { const entry = s.stream_list[i - 1]; const s2 = await play_url(s, entry); set_events(s2); }); s.comp.stream_table.onceKey(s.config.keys.screen.input, async () => { const s2 = show_input(s); render(s2); set_events(s2); }); s.comp.stream_table.key(s.config.keys.stream_table.debug, () => { const offset = s.comp.stream_table.childOffset; s.scr.debug(s.stream_list[offset - 1].entry); }); s.comp.stream_table.key(s.config.keys.stream_table.scroll_down, () => { s.comp.stream_table.down(s.config.scroll_up_dist); render(s); }); s.comp.stream_table.key(s.config.keys.stream_table.scroll_up, () => { s.comp.stream_table.up(s.config.scroll_down_dist); render(s); }); s.comp.input.onceKey(s.config.keys.input.submit, async () => { const line = s.comp.input.getText().trim(); const s2 = await input_handler(s, line); set_events(s2); }); s.comp.input.onceKey(s.config.keys.screen.input, async () => { const s2 = hide_input(s); set_events(s2); }); s.comp.loading.onceKey('q', () => quit(s)); } async function init(s) { set_header(s); render(s); const s2 = await search_streams(s, s.flags.last_search); set_events(s2); } function init_state(config, argv, styles) { const Blessed = require('blessed'); const scr = Blessed.screen({ autoPadding: true, debug: true, fullUnicode: true, smartCSR: true, }); styles.header.content = Util.format_init_header(config.header); const comp = { header: Blessed.listbar(styles.header), stream_table: Blessed.listtable(styles.stream_table), input: Blessed.textarea(styles.input), loading: Blessed.loading(styles.loading) }; scr.append(comp.header); scr.append(comp.stream_table); scr.append(comp.input); scr.append(comp.loading); comp.stream_table.focus(); return Object.freeze({ scr, comp, config, stream_list: [], flags: Object.freeze({ last_search: argv.q || config.default_search, last_tab: argv.s || config.default_source, source: argv.s || config.default_source, is_paused: false }) }); } function main() { const available_opts = ['h', 'q', 'Q', 's', '_']; const argv = (0, minimist_1.default)(process.argv); if (argv.h || !!Object.keys(argv).find((opt) => !available_opts.includes(opt))) print_usage_and_quit(); else { const config = Util.read_config(); const styles = Util.read_styles(); const s = init_state(config, argv, styles); init(s); } } main();