#!/usr/bin/env python # PyRadio: Curses based Internet Radio Player # http://www.coderholic.com/pyradio # Ben Dowling - 2008 import os import sys import curses import thread import subprocess stations = [ ("Afterhours.FM (Trance/Progressive)", "http://at2.ah.fm:80"), ("BBC Radio 1", "mms://wmlive-acl.bbc.co.uk/wms/radio1/radio1_nb_e1s1"), ("Digitally Imported: Chillout", "http://di.fm/mp3/chillout.pls"), ("Digitally Imported: Trance", "http://di.fm/mp3/trance.pls"), ("Digitally Imported: Classic Techno", "http://di.fm/mp3/classictechno.pls"), ("Electronic Culture", "http://www.shouted.fm/tunein/electro-dsl.m3u"), ("Frequence 3 (Pop)", "http://streams.frequence3.net/hd-mp3.m3u"), ("Mostly Classical", "http://www.sky.fm/mp3/classical.pls"), ("Proton Radio (House/Dance)", "http://www.protonradio.com:8000"), ("Ragga Kings", "http://www.raggakings.net/listen.m3u"), ("Secret Agent (Downtempo)", "http://somafm.com/secretagent.pls"), ("Slay Radio (C64 Remix)", "http://sc.slayradio.org:8000/listen.pls"), ("Smooth FM (Jazz)", "http://66.55.143.195:9028"), ("SomaFM: Groove Salad", "http://somafm.com/startstream=groovesalad.pls"), ("SomaFM: Beat Blender", "http://somafm.com/startstream=beatblender.pls"), ("SomaFM: Cliq Hop", "http://somafm.com/startstream=cliqhop.pls"), ("SomaFM: Sonic Universe", "http://somafm.com/startstream=sonicuniverse.pls"), ("SomaFM: Tags Trance Trip", "http://somafm.com/tagstrance.pls"), ("Virgin Radio (Pop)", "http://www.smgradio.com/core/audio/mp3/live.pls?service=vrbb"), ] class Log(object): """ Log class that outputs text to a curses screen """ def __init__(self, cursesScreen): self.cursesScreen = cursesScreen self.width = cursesScreen.getmaxyx()[1] - 5 def write(self, str): self.cursesScreen.erase() self.cursesScreen.addstr(0, 1, str[0: self.width].replace("\r", "").replace("\n", "")) self.cursesScreen.refresh() def readline(self): pass class Player(object): """ Media player class. Playing is handled by mplayer """ process = None def __init__(self, outputStream): self.outputStream = outputStream def __del__(self): self.close() def updateStatus(self): try: input = self.process.stdout.readline() while(input != ''): self.outputStream.write(input) input = self.process.stdout.readline() except: pass def play(self, url): self.close() self.process = subprocess.Popen(["mplayer", "-quiet", url], shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) thread.start_new_thread(self.updateStatus, ()) def sendCommand(self, command): if(self.process != None): try: self.process.stdin.write(command) except: pass def mute(self): self.sendCommand("m") def pause(self): self.sendCommand("p") def close(self): self.sendCommand("q") if self.process != None: os.kill(self.process.pid, 9) self.process.wait() self.process = None class PyRadio(object): startPos = 0 selection = 0 def setup(self, stdscr): self.stdscr = stdscr try: curses.curs_set(0) except: pass curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK) curses.init_pair(2, curses.COLOR_BLUE, curses.COLOR_BLACK) curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK) curses.init_pair(4, curses.COLOR_GREEN, curses.COLOR_BLACK) curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_BLACK) curses.init_pair(6, curses.COLOR_BLACK, curses.COLOR_MAGENTA) curses.init_pair(7, curses.COLOR_BLACK, curses.COLOR_GREEN) self.maxY, self.maxX = stdscr.getmaxyx() self.headWin = curses.newwin(1, self.maxX, 0, 0) self.bodyWin = curses.newwin(self.maxY-2, self.maxX, 1, 0) self.footerWin = curses.newwin(1, self.maxX, self.maxY-1, 0) self.initHead() self.initBody() self.initFooter() self.log = Log(self.footerWin) self.player = Player(self.log) curses.doupdate() self.run() def initHead(self): info = " PyRadio 0.1 " self.headWin.addstr(0, 0, info, curses.color_pair(4)) rightStr = "www.coderholic.com/pyradio" self.headWin.addstr(0, self.maxX - len(rightStr) - 1, rightStr, curses.color_pair(2)) self.headWin.bkgd(' ', curses.color_pair(7)) self.headWin.noutrefresh() def initBody(self): """ Initializes the body/story window """ self.bodyWin.timeout(100) self.bodyWin.keypad(1) self.bodyMaxY, self.bodyMaxX = self.bodyWin.getmaxyx() self.bodyWin.noutrefresh() self.refreshBody() def initFooter(self): """ Initializes the body/story window """ self.footerWin.bkgd(' ', curses.color_pair(7)) self.footerWin.noutrefresh() def refreshBody(self): self.bodyWin.erase() self.bodyWin.box() self.bodyWin.move(1, 1) maxDisplay = self.bodyMaxY - 1 for idx in range(maxDisplay - 1): if(idx > maxDisplay): break try: station = stations[idx + self.startPos] col = curses.color_pair(5) if idx + self.startPos == self.selection: col = curses.color_pair(6) self.bodyWin.hline(idx + 1, 1, ' ', self.bodyMaxX - 2, col) self.bodyWin.addstr(idx + 1, 1, station[0], col) except IndexError: break def run(self): while True: try: c = self.bodyWin.getch() ret = self.keypress(c) if (ret == -1): return except KeyboardInterrupt: break def keypress(self, char): if char == curses.KEY_EXIT or char == ord('q'): self.player.close() return -1 elif char in (curses.KEY_ENTER, ord('\n'), ord('\r')): url = stations[self.selection][1] self.player.play(url) return elif char == curses.KEY_DOWN or char== ord('j'): if self.selection < len(stations) - 1: if self.selection - self.startPos > (self.bodyMaxY - 4): self.startPos += 1 self.selection += 1 self.refreshBody() return elif char == curses.KEY_UP or char == ord('k'): # Scroll stories up by one if self.selection > 0: self.selection -= 1 if self.selection < self.startPos: self.startPos -= 1 self.refreshBody() return elif char == ord('m'): self.player.mute() return if __name__ == "__main__": pyRadio = PyRadio() curses.wrapper(pyRadio.setup)