termice
Version:
Simple terminal icecast player
356 lines (352 loc) • 11.4 kB
JavaScript
;
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();