#!/usr/bin/env python # -*- python -*- __version__ = "comms" # Comms will try to start this only if there's no player running default_player = "audacious" # Where to attempt to start the player. Usually :0 is the X server # you're running. Set to empty "" to force headless operation. default_x_display = ":0" """ Forked from Ulf Betlehem's cplay 1.44 by TeknoHog()iki.fi on 27.01.2002 Homepage: http://www.iki.fi/teknohog/hacks/comms/ 30.11.2003: Added command_play_album contributed by Kim Poulsen 30.12.2003: Changed backend into pyxmms (http://www.via.ecp.fr/~flo/2002/PyXMMS/xmms.html) 26.08.2005: Added command_add_cd and command_play_cd 28.9.2005: Improved xmms launching. If an X server is running, it is utilized. 8.12.2005: Disabed move during playback (see function command_move for reasons) Re-enabled mark_all Added mplayer-style 9/0 keybindings for volume down/up Moved audio CD functions from playlist to root window 20060106: Disabled periodic display updates while paused (to facilitate the following) Added display_fileinfo. It has some problems still: * when path is too long -- use larger terminal window if possible * when song is playing -- pause the song to see the path 20060329: Added the rather experimental command_add_via_cache. It copies a file to a cache directory, and adds that copy into playlist. I wrote it to help with DJing, so that I can make a playlist of files that are burned on different CDs. It could use a feature to remove files from cache after they have been played, but it's not essential for me at the moment. A single night's cache should not take too many gigs. 20060615: Kind of bugfix in command_add_cd and command_play_cd 20060829: command_mark_all: removed dupe and changed behaviour to toggle. In fact I wanted a kind of unmark_all since I sometimes mark_all by accident. Then again marking/unmarking of single entries is a toggle with a single key, so this may be more sensible. 20061102: Version for Audacious with xmmsalike.py + ctypes instead of pyxmms. Aim for a general xmms/audacious/bpm version, as the interface allows this pretty easily. However, I had to hardcode /mnt/cdrom as the drive mountpoint, since the functions for reading the config are not so universal. I don't consider this a big issue, but hardcoding is such a loss of elegance ;) 20061103: Generalized xmmsalike.py usage to cover xmms and bmp in addition to audacious. (BMP does need testing though, I don't have it at the moment.) Also rebuilt the X detection and headless stuff, it may need some more refining. Since the cdrom directory is no longer detected, and there are some other variables you need to specify, I put those at the top of this script for easier configuration. 20061103b: New config file reading function; now we can again get CDDA directory from the player :) TODO: playlist indexing? do we need that? cache song lengths into playlist-buffer.entries? very minimal benefit. fix quirks with updating display.. should be enough now. more xmms'ish keybindings? start xmms with files from args; works, but may need smoothing const TODO update helptext; add_url (btw, this works by adding an m3u file that contains the url) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """ # ------------------------------------------ from types import * import os import sys import time import getopt import signal import string import select import re #import xmms.control #import xmms.config import shutil import xmmsalike import ConfigParser try: from ncurses import curses except ImportError: import curses try: import tty except ImportError: tty = None try: import locale; locale.setlocale(locale.LC_ALL, "") except: pass # ------------------------------------------ _locale_domain = "comms" _locale_dir = "/usr/local/share/locale" try: import gettext # python 2.0 gettext.install(_locale_domain, _locale_dir) except ImportError: try: import fintl fintl.bindtextdomain(_locale_domain, _locale_dir) fintl.textdomain(_locale_domain) _ = fintl.gettext except ImportError: def _(s): return s except: def _(s): return s # ------------------------------------------ XTERM = re.search("rxvt|xterm", os.environ["TERM"]) and 1 or 0 RETRY = 2.0 # ------------------------------------------ ## def log(msg): ## f = open("log", "a"); f.write(msg); f.close() # ------------------------------------------ def which(program): for path in string.split(os.environ["PATH"], ":"): if os.path.exists(os.path.join(path, program)): return os.path.join(path, program) # ------------------------------------------ class Stack: def __init__(self): self.items = () def push(self, item): self.items = (item,) + self.items def pop(self): self.items, item = self.items[1:], self.items[0] return item # ------------------------------------------ class KeymapStack(Stack): def process(self, code): for keymap in self.items: if keymap and keymap.process(code): break # ------------------------------------------ class Keymap: def __init__(self): self.methods = [None] * curses.KEY_MAX def bind(self, key, method, args=None): if type(key) in (TupleType, ListType): for i in key: self.bind(i, method, args) return if type(key) is StringType: key = ord(key) self.methods[key] = (method, args) def process(self, key): if self.methods[key] is None: return 0 method, args = self.methods[key] if args is None: apply(method, (key,)) else: apply(method, args) return 1 # ------------------------------------------ class Window: t = ['?'] * 256 for i in range(0x20, 0x7f): t[i] = chr(i) for c in string.letters: t[ord(c)] = c translationTable = string.join(t, "") def __init__(self, parent): self.parent = parent self.children = [] self.name = None self.keymap = None self.visible = 1 self.resize() if parent: parent.children.append(self) def __getattr__(self, name): return getattr(self.w, name) def getmaxyx(self): y, x = self.w.getmaxyx() try: curses.version # tested with '1.2' and '1.6' except AttributeError: # pyncurses - emulate traditional (silly) behavior y, x = y+1, x+1 return y, x def touchwin(self): try: self.w.touchwin() except AttributeError: self.touchln(0, self.getmaxyx()[0]) def attron(self, attr): try: self.w.attron(attr) except AttributeError: self.w.attr_on(attr) def attroff(self, attr): try: self.w.attroff(attr) except AttributeError: self.w.attr_off(attr) def newwin(self): return curses.newwin(0, 0, 0, 0) def resize(self): ## todo - delwin? self.w = self.newwin() self.ypos, self.xpos = self.getbegyx() self.rows, self.cols = self.getmaxyx() self.keypad(1) self.leaveok(1) self.scrollok(1) for child in self.children: child.resize() def update(self): self.clear() self.refresh() for child in self.children: child.update() # ------------------------------------------ class HelpWindow(Window): text = _("""\ z, x, c, v, b = prev, play, pause, stop, next (as in XMMS) +,=,0 / -,9 = Increase / decrease volume Tab = Goto playlist/filelist s = Toggle shuffle (see below if it's on[S] or off[s]) r = Toggle repeat (ditto with [R]/[r]) Left/Right = Skip -/+ 5 seconds within song C-l = Refresh screen q = Quit Esc = Abort prompted operation (w, o) C = Add audio CD to playlist P = Clear playlist and play audio CD f = Show path of file being played (buggy, see source) Movement: Up, C-p, k, Down, C-n, j, PgUp, K, PgDown, J, Home, g, End, G Filelist only: . or Backspace = Parent directory Space = Add file to playlist Enter = Goto directory; Play file o = Prompt for directory to go to a = Add directory recursively to playlist p = Clear playlist and play directory contents t = Add file via harddrive cache Playlist only: Space = Mark or unmark song a = Mark all d / D = Remove marked songs / remove all m = Move marked songs to pointer position Enter = Start playing from file w = Write playlist to a file """) def __init__(self, parent): Window.__init__(self, parent) self.name = _("Help") self.keymap = Keymap() self.keymap.bind('q', self.parent.help, ()) def newwin(self): return curses.newwin(self.parent.rows-2, self.parent.cols, self.parent.ypos+2, self.parent.xpos) def update(self): self.move(0, 0) self.addstr(self.text) self.touchwin() self.refresh() # ------------------------------------------ class ProgressWindow(Window): def __init__(self, parent): Window.__init__(self, parent) self.value = 0 def newwin(self): return curses.newwin(1, self.parent.cols, self.parent.rows-2, 0) def update(self): self.move(0, 0) self.hline(ord('-'), self.cols) if self.value > 0: self.move(0, 0) x = int(self.value * self.cols) # 0 to cols-1 self.hline(ord('='), x+1) self.move(0, x) self.addstr('|') self.touchwin() self.refresh() def progress(self): denom = float(xmmsalike.get_playlist_time(xmmsalike.get_playlist_pos())) if denom > 0: value = float(xmmsalike.get_output_time()) / denom else: value = 0 self.value = min(value, 0.99) self.update() # ------------------------------------------ class StatusWindow(Window): def __init__(self, parent): Window.__init__(self, parent) self.default_message = '' self.current_message = '' self.timeout_tag = None def newwin(self): return curses.newwin(1, self.parent.cols-16, self.parent.rows-1, 0) # watch the width.. related to CounterWindow! def update(self): msg = string.translate(self.current_message, Window.translationTable) if len(msg) > self.cols: msg = "%s>" % msg[:self.cols-1] self.move(0, 0) self.addstr(msg) self.clrtoeol() self.touchwin() self.refresh() def status(self, message, duration = 0): self.current_message = message if duration > 0: if self.timeout_tag: app.timeout.remove(self.timeout_tag) self.timeout_tag = app.timeout.add(duration, self.timeout) self.update() def timeout(self): self.timeout_tag = None self.restore_default_status() def set_default_status(self, message): self.default_message = message self.status(message) XTERM and sys.stderr.write("\033]0;%s\a" % (message or "comms")) def restore_default_status(self): self.status(self.default_message) # ------------------------------------------ class CounterWindow(Window): def __init__(self, parent): Window.__init__(self, parent) self.values = [0, 0] self.mode = 1 def newwin(self): return curses.newwin(1, 15, self.parent.rows-1, self.parent.cols-15) def update(self): if xmmsalike.is_repeat(): rep_status = "R" else: rep_status = "r" if xmmsalike.is_shuffle(): shu_status = "S" else: shu_status = "s" seconds = xmmsalike.get_output_time() / 1000 m, s = divmod(seconds, 60) statusline = "[" + rep_status + "] [" + shu_status + "]" self.move(0, 0) self.attron(curses.A_BOLD) self.insstr(statusline + " %02dm %02ds" % (m, s)) # Dog knows why addstr fails.. fix borrowed from cplay 1.46 self.attroff(curses.A_BOLD) self.touchwin() self.refresh() def counter(self): time.sleep(0.05) self.update() def toggle_mode(self): self.mode = not self.mode self.update() # ------------------------------------------ class RootWindow(Window): def __init__(self, parent): Window.__init__(self, parent) keymap = Keymap() keymap.bind(12, self.update, ()) # C-l keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1,)) # Left, C-b keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1,)) # Right, C-f # keymap.bind(range(48,58), app.key_volume) # 1234567890 keymap.bind(['+', "=", "0"], app.inc_volume, ()) keymap.bind(['-', "9"], app.dec_volume, ()) keymap.bind('b', app.next, ()) keymap.bind('z', app.prev, ()) keymap.bind('c', app.pause, ()) keymap.bind('v', app.stop, ()) keymap.bind('x', app.play, ()) keymap.bind('r', app.toggle_repeat, ()) keymap.bind('s', app.toggle_shuffle, ()) # keymap.bind('t', app.toggle_counter_mode, ()) keymap.bind('q', app.quit, ()) keymap.bind('C', self.command_add_cd, ()) keymap.bind('P', self.command_play_cd, ()) keymap.bind('f', app.display_fileinfo, ()) app.keymapstack.push(keymap) self.win_progress = ProgressWindow(self) self.win_status = StatusWindow(self) self.win_counter = CounterWindow(self) self.win_tab = TabWindow(self) def command_add_cd(self): cdda_directory = xmmsalike.config_get("CDDA", "directory") xmmsalike.playlist_add([cdda_directory]) self.update() def command_play_cd(self): cdda_directory = xmmsalike.config_get("CDDA", "directory") xmmsalike.playlist([cdda_directory], 0) # ------------------------------------------ class TabWindow(Window): def __init__(self, parent): Window.__init__(self, parent) self.active_child = 0 self.win_filelist = self.add(FilelistWindow) self.win_playlist = self.add(PlaylistWindow) self.win_help = self.add(HelpWindow) self.keymap = Keymap() self.keymap.bind('\t', self.change_window, ()) # Tab self.keymap.bind('h', self.help, ()) app.keymapstack.push(self.keymap) app.keymapstack.push(self.children[self.active_child].keymap) def newwin(self): return curses.newwin(self.parent.rows-2, self.parent.cols, 0, 0) def update(self): self.update_title() self.move(1, 0) self.hline(ord('-'), self.cols) self.move(2, 0) self.clrtobot() self.refresh() child = self.children[self.active_child] child.visible = 1 child.update() def update_title(self, refresh = 1): self.move(0, 0) self.clrtoeol() self.attron(curses.A_BOLD) self.addstr(str(self.children[self.active_child].name)) self.attroff(curses.A_BOLD) if refresh: self.refresh() def add(self, Class): win = Class(self) win.visible = 0 return win def change_window(self, window = None): app.keymapstack.pop() self.children[self.active_child].visible = 0 if window: self.active_child = self.children.index(window) else: # toggle windows 0 and 1 self.active_child = not self.active_child app.keymapstack.push(self.children[self.active_child].keymap) self.update() def help(self): if self.children[self.active_child] == self.win_help: self.change_window(self.win_last) else: self.win_last = self.children[self.active_child] self.change_window(self.win_help) app.status(__version__, 2) # ------------------------------------------ class ListWindow(Window): def __init__(self, parent): Window.__init__(self, parent) self.buffer = [] self.bufptr = self.scrptr = 0 self.search_direction = 0 self.input_mode = 0 self.input_prompt = "" self.input_string = "" self.do_input_hook = None self.stop_input_hook = None self.keymap = Keymap() self.keymap.bind(['k', curses.KEY_UP, 16], self.cursor_move, (-1,)) self.keymap.bind(['j', curses.KEY_DOWN, 14], self.cursor_move, (1,)) self.keymap.bind(['K', curses.KEY_PPAGE], self.cursor_ppage, ()) self.keymap.bind(['J', curses.KEY_NPAGE], self.cursor_npage, ()) self.keymap.bind(['g', curses.KEY_HOME], self.cursor_home, ()) self.keymap.bind(['G', curses.KEY_END], self.cursor_end, ()) self.keymap.bind(['?', 18], self.start_search, (_("backward-isearch"), -1)) self.keymap.bind(['/', 19], self.start_search, (_("forward-isearch"), 1)) self.input_keymap = Keymap() self.input_keymap.bind(range(32, 128), self.do_input) self.input_keymap.bind('\t', self.do_input) self.input_keymap.bind(curses.KEY_BACKSPACE, self.do_input, (8,)) self.input_keymap.bind(['\a', 27], self.stop_input, (_("cancel"),)) self.input_keymap.bind('\n', self.stop_input, (_("ok"),)) def newwin(self): return curses.newwin(self.parent.rows-2, self.parent.cols, self.parent.ypos+2, self.parent.xpos) def update(self, force = 1): self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1)) scrptr = (self.bufptr / self.rows) * self.rows if force or self.scrptr != scrptr: self.scrptr = scrptr self.move(0, 0) for entry in self.buffer[self.scrptr:]: if self.getyx()[0] == self.rows - 1: break if self.getyx()[1] > 0: self.addstr("\n") self.putstr(entry) self.clrtobot() if self.visible: self.refresh() self.update_line(curses.A_REVERSE) def update_line(self, attr = None, refresh = 1): if not self.buffer: return ypos = self.bufptr - self.scrptr if attr: self.attron(attr) self.move(ypos, 0) self.hline(ord(' '), self.cols) entry = self.current() self.putstr(entry) if attr: self.attroff(attr) if self.visible and refresh: self.refresh() def start_input(self, prompt="", data=""): self.input_mode = 1 app.keymapstack.push(self.input_keymap) self.input_prompt = prompt self.input_string = data def do_input(self, *args): if self.do_input_hook: return apply(self.do_input_hook, args) ch = args and args[0] or None if ch in [8, 127]: # backspace self.input_string = self.input_string[:-1] elif ch: self.input_string = "%s%c" % (self.input_string, ch) app.status("%s: %s" % (self.input_prompt, self.input_string)) ## We have the result in self.input_string def stop_input(self, *args): self.input_mode = 0 app.keymapstack.pop() if self.stop_input_hook: return apply(self.stop_input_hook, args) def putstr(self, entry, *pos): s = string.translate(str(entry), Window.translationTable) s = "%s%s" % ((len(s) > self.cols) and (s[:self.cols - 1], ">") or (s, "")) pos and apply(self.move, pos) self.addstr(s) def current(self): if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1 return self.buffer[self.bufptr] def cursor_move(self, ydiff): if self.input_mode: self.stop_input(_("cancel")) if not self.buffer: return self.update_line(refresh = 0) self.bufptr = (self.bufptr + ydiff) % len(self.buffer) self.update(force = 0) def cursor_ppage(self): if self.rows > len(self.buffer): return tmp = self.bufptr % self.rows if tmp == self.bufptr: self.cursor_move(-(tmp + (len(self.buffer) % self.rows) or self.rows)) else: self.cursor_move(-(tmp + self.rows)) def cursor_npage(self): if self.rows > len(self.buffer): return tmp = self.rows - self.bufptr % self.rows if self.bufptr + tmp > len(self.buffer): self.cursor_move(len(self.buffer) - self.bufptr) else: self.cursor_move(tmp) def cursor_home(self): self.cursor_move(-self.bufptr) def cursor_end(self): self.cursor_move(-self.bufptr - 1) def is_searching(self): return abs(self.search_direction) def start_search(self, type, direction): if not self.is_searching(): self.start_input() self.do_input_hook = self.do_search self.stop_input_hook = self.stop_search if self.search_direction != direction: self.search_direction = direction self.input_prompt = type self.do_search() else: self.do_search(advance = direction) def stop_search(self, reason = ""): self.search_direction = 0 app.status(reason, 1) def do_search(self, ch = None, advance = 0): direction = self.search_direction if ch in [8, 127]: # backspace direction = -direction self.input_string = self.input_string[:-1] elif ch: self.input_string = "%s%c" % (self.input_string, ch) index = self.bufptr + advance while 1: if index >= len(self.buffer) or index < 0: app.status(_("Not found: %s") % self.input_string) break line = "%s" % self.buffer[index] if string.find(string.lower(line), string.lower(self.input_string)) != -1: app.status("%s: %s" % (self.input_prompt, self.input_string)) self.update_line(refresh = 0) self.bufptr = index self.update(force = 0) break index = index + direction # ------------------------------------------ class FilelistWindow(ListWindow): def __init__(self, parent): ListWindow.__init__(self, parent) self.oldposition = {} try: self.chdir(os.getcwd()) except OSError: self.chdir(os.environ['HOME']) self.mtime_when = 0 self.mtime = None self.keymap.bind('\n', self.command_chdir_or_play, ()) self.keymap.bind(['.', curses.KEY_BACKSPACE], self.command_chparentdir, ()) self.keymap.bind(' ', self.command_add, ()) self.keymap.bind('a', self.command_add_recursively, ()) self.keymap.bind('o', self.command_goto, ()) self.keymap.bind('p', self.command_play_album, ()) self.keymap.bind('t', self.command_add_via_cache, ()) self.cachedir = os.path.join(os.environ['HOME'], "tmp/comms-cache") + "/" def listdir_maybe(self, now=0): if now < self.mtime_when+2: return self.mtime_when = now try: mtime = os.stat(self.cwd)[8] self.mtime == mtime or self.listdir() self.mtime = mtime except os.error: pass def listdir(self): app.status(_("Reading directory...")) self.dirs = [] self.files = [] try: self.mtime = os.stat(self.cwd)[8] self.mtime_when = time.time() for entry in os.listdir(self.cwd): if entry[0] == ".": continue if os.path.isdir(self.cwd + entry): self.dirs.append("%s/" % entry) else: self.files.append("%s" % entry) except os.error: pass self.dirs.sort() self.files.sort() self.buffer = self.dirs + self.files self.cwd != "/" and self.buffer.insert(0, "../") if self.oldposition.has_key(self.cwd): self.bufptr = self.oldposition[self.cwd] else: self.bufptr = 0 self.parent.update_title() self.update(force = 1) app.restore_default_status() def normpath(self, dir): dir = dir and dir + '/' match = 1 while match: dir, match = re.subn("/+(\.|[^/]*/*\.\.)/+", "/", dir, 1) match = 1 while match: dir, match = re.subn("//+", "/", dir, 1) return dir def chdir(self, dir): if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr self.cwd = self.normpath(dir) self.name = _("Filelist: ") diff = len(self.name) + len(self.cwd) - self.cols if diff > 0: self.name = "%s<%s" % (self.name, self.cwd[diff+1:]) else: self.name = "%s%s" % (self.name, self.cwd) def command_chdir_or_play(self): if os.path.isdir(self.cwd + self.current()): self.chdir(self.cwd + self.current()) self.listdir() else: # it's a file # this is pyxmms specific macro... #xmmsalike.enqueue_and_play([self.cwd + self.current()]) # ..so I rewrite it similarly to pyxmms. pl = xmmsalike.get_playlist_length() xmmsalike.playlist_add([self.cwd + self.current()]) xmmsalike.set_playlist_pos(pl) xmmsalike.play() def command_chparentdir(self): self.chdir(self.cwd + "..") self.listdir() def command_goto(self): self.start_input(_("goto")) self.do_input_hook = None self.stop_input_hook = self.stop_goto self.do_input() def stop_goto(self, reason): if reason == _("cancel") or not self.input_string: app.status(_("cancel"), 1) return dir = self.input_string if dir[0] != '/': dir = "%s%s" % (self.cwd, dir) if not os.path.isdir(dir): app.status(_("Not a directory!"), 1) return self.chdir(dir) self.listdir() def command_add(self): if (os.path.isfile(self.cwd + self.current())): xmmsalike.playlist_add([self.cwd + self.current()]) self.cursor_move(+1) def command_add_recursively(self): xmmsalike.playlist_add([self.cwd + self.current()]) self.cursor_move(+1) def command_add_via_cache(self): # teknohog's experimental DJing feature sourcefile = self.cwd + self.current() destfile = self.cachedir + self.current() if (os.path.isfile(sourcefile)): # copy file to cachedir if not os.path.isdir(self.cachedir): os.makedirs(self.cachedir) shutil.copyfile(sourcefile, destfile) # chmod 644 to enable deletion; leading 0 is required for octal os.chmod(destfile, 0644) # add cached file to playlist xmmsalike.playlist_add([destfile]) self.cursor_move(+1) def command_play_album(self): xmmsalike.playlist([self.cwd + self.current()], 0) # ------------------------------------------ class PlaylistEntry: def __init__(self, title): self.title = title self.marked = 0 self.active = 0 self.attrib = curses.A_BOLD def set_marked(self, value): self.marked = value def toggle_marked(self): self.marked = not self.marked def is_marked(self): return self.marked == 1 def set_active(self, value): self.active = value def is_active(self): return self.active == 1 def __str__(self): return "%s %s" % (self.is_marked() and "#" or " ", self.title) # ------------------------------------------ class PlaylistWindow(ListWindow): def __init__(self, parent): ListWindow.__init__(self, parent) self.name = _("Playlist") self.repeat = 0 self.random = 0 self.random_buffer = [] self.keymap.bind('\n', self.command_play, ()) self.keymap.bind(' ', self.command_mark, ()) self.keymap.bind('d', self.command_delete, ()) self.keymap.bind('m', self.command_move, ()) self.keymap.bind('w', self.command_save_playlist, ()) self.keymap.bind('D', self.command_delete_all, ()) # self.keymap.bind('u', self.command_add_url, ()) self.keymap.bind('a', self.command_mark_all, ()) # self.keymap.bind('c', self.command_clear_all, ()) # self.keymap.bind('A', self.command_mark_regexp, ()) # self.keymap.bind('C', self.command_clear_regexp, ()) def update(self, force = 1): # If the list doesn't change, let's not lose any marking info! xlength = xmmsalike.get_playlist_length() if xlength != len(self.buffer): # List has changed. self.buffer = [] if xlength > 0: for i in range(xlength): entry = PlaylistEntry(xmmsalike.get_playlist_title(i)) self.buffer.append(entry) # old xmmsctrl impl. # for item in xmms.get_playlist(): # entry = PlaylistEntry(item) # self.buffer.append(entry) self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1)) scrptr = (self.bufptr / self.rows) * self.rows if force or self.scrptr != scrptr: self.scrptr = scrptr self.move(0, 0) for entry in self.buffer[self.scrptr:]: if self.getyx()[0] == self.rows - 1: break if self.getyx()[1] > 0: self.addstr("\n") self.putstr(entry) self.clrtobot() if self.visible: self.refresh() self.update_line(curses.A_REVERSE) def change_name(self): self.name = _("Playlist %s %s") % (self.repeat and _("[repeat]") or " " * len(_("[repeat]")), self.random and _("[random]") or " " * len(_("[random]"))) def clear(self): xmmsalike.playlist_clear() def putstr(self, entry, *pos): playpos = xmmsalike.get_playlist_pos() if self.buffer.index(entry) == playpos: self.attron(curses.A_BOLD) apply(ListWindow.putstr, (self, entry) + pos) if self.buffer.index(entry) == playpos: self.attroff(curses.A_BOLD) def get_remaining_entries(self): l = [] for i in self.buffer: if not i in self.random_buffer: l.append(i) return l def get_active_entry(self): return self.buffer[xmmsalike.get_playlist_pos()] def command_play(self): if not self.buffer: return xmmsalike.set_playlist_pos(self.bufptr) if not xmmsalike.is_playing(): app.play() else: app.display_title() self.update() def command_mark(self): if not self.buffer: return self.buffer[self.bufptr].toggle_marked() self.cursor_move(1) def command_mark_all(self): for entry in self.buffer: entry.toggle_marked() app.status(_("Almost there..."), 1) self.update(force = 1) def command_delete_all(self): xmmsalike.playlist_clear() app.status(_("Cleared playlist"), 1) self.update(force = 1) app.progress() app.counter() def command_delete(self): if not self.buffer: return current_entry = self.current() # must delete in reverse order.. otherwise # the indices of to-be-deleted items will change for i in range(len(self.buffer)-1, -1, -1): if self.buffer[i].is_marked(): xmmsalike.playlist_delete(i) try: self.bufptr = self.buffer.index(current_entry) except ValueError: self.bufptr = 0 self.update(force = 1) def command_move(self): # this isn't very easily xmms'izable, as there isn't a # relevant function in the API. # no, wait.. just take the filenames and remove & add back. # we have to rebuild the whole list and then clear & add all of them. # 20051205 DJing experience -> this is not the way while doing # live playback.. disable moving during playback for now, and # put a warning message. if xmmsalike.is_playing(): app.status(_("comms move disabled during playback :("), 5) else: if not self.buffer: return current_entry = self.current() if current_entry.is_marked(): return # sanity check filelist = [] l = [] for i in range(0, len(self.buffer)): if self.buffer[i].is_marked(): filelist.append("") # to maintain the correct indexing l.append(xmmsalike.get_playlist_file(i)) else: filelist.append(xmmsalike.get_playlist_file(i)) self.bufptr = self.buffer.index(current_entry) filelist[self.bufptr:self.bufptr] = l xmmsalike.playlist_clear() self.update(force = 1) # update now so the buffer gets changed! for file in filelist: if file != "": xmmsalike.playlist_add([file]) self.update(force = 1) def command_mark_regexp(self): self.mark_value = 1 self.start_input(_("Mark regexp")) self.do_input_hook = None self.stop_input_hook = self.stop_mark_regexp self.do_input() def command_clear_regexp(self): self.mark_value = 0 self.start_input(_("Clear regexp")) self.do_input_hook = None self.stop_input_hook = self.stop_mark_regexp self.do_input() def stop_mark_regexp(self, reason): if reason == _("cancel") or not self.input_string: app.status(_("cancel"), 1) return try: r = re.compile(self.input_string) for entry in self.buffer: if r.search(entry.filename): entry.set_marked(self.mark_value) self.update(force = 1) app.status(_("ok"), 1) except re.error, e: app.status(str(e), 2) def command_save_playlist(self): self.start_input(_("Save playlist"), app.win_filelist.cwd) self.do_input_hook = None self.stop_input_hook = self.stop_save_playlist self.do_input() def stop_save_playlist(self, reason): if reason == _("cancel") or not self.input_string: app.status(_("cancel"), 1) return filename = self.input_string if filename[0] != '/': filename = "%s%s" % (app.win_filelist.cwd, filename) if not VALID_PLAYLIST(filename): filename = "%s%s" % (filename, ".m3u") try: file = open(filename, "w") for i in range(0, len(self.buffer)-1): file.write("%s\n" % xmmsalike.get_playlist_file(i)) file.close() app.status(_("ok"), 1) except IOError: app.status(_("Cannot write playlist!"), 1) def command_add_url(self): self.start_input(_("Add URL")) self.do_input_hook = None self.stop_input_hook = self.stop_add_url self.do_input() def stop_add_url(self, reason): if reason == _("cancel") or not self.input_string: app.status(_("cancel"), 1) return xmmsalike.playlist_add_url_string(self.input_string) # ------------------------------------------ class Timeout: def __init__(self): self.next = 0 self.dict = {} def add(self, timeout, func, args=()): tag = self.next = self.next + 1 self.dict[tag] = (func, args, time.time() + timeout) return tag def remove(self, tag): del self.dict[tag] def check(self, now): for tag, (func, args, timeout) in self.dict.items(): if now >= timeout: self.remove(tag) apply(func, args) return len(self.dict) and 0.2 or None # ------------------------------------------ class Application: def __init__(self): self.keymapstack = KeymapStack() def setup(self): if tty: self.tcattr = tty.tcgetattr(sys.stdin.fileno()) tcattr = tty.tcgetattr(sys.stdin.fileno()) tcattr[0] = tcattr[0] & ~(tty.IXON) tty.tcsetattr(sys.stdin.fileno(), tty.TCSANOW, tcattr) self.w = curses.initscr() curses.cbreak() curses.noecho() try: curses.meta(1) except: pass try: curses.curs_set(0) except: pass signal.signal(signal.SIGCHLD, signal.SIG_IGN) signal.signal(signal.SIGHUP, self.handler_quit) signal.signal(signal.SIGINT, self.handler_quit) signal.signal(signal.SIGTERM, self.handler_quit) signal.signal(signal.SIGWINCH, self.handler_resize) self.jump_const = 5000 # milliseconds per keypress when seeking self.win_root = RootWindow(None) self.win_root.update() self.win_tab = self.win_root.win_tab self.win_filelist = self.win_root.win_tab.win_filelist self.win_playlist = self.win_root.win_tab.win_playlist self.status = self.win_root.win_status.status self.set_default_status = self.win_root.win_status.set_default_status self.restore_default_status = self.win_root.win_status.restore_default_status self.counter = self.win_root.win_counter.counter self.progress = self.win_root.win_progress.progress self.timeout = Timeout() self.win_filelist.listdir_maybe(time.time()) self.set_default_status("") self.seek_tag = None self.start_tag = None def cleanup(self): curses.endwin() XTERM and sys.stderr.write("\033]0;%s\a" % "xterm") tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr) def display_title(self): time.sleep(0.05) # if we use this for next/prev/pause etc. songname = xmmsalike.get_playlist_title(xmmsalike.get_playlist_pos()) if xmmsalike.is_paused(): songname += " [PAUSED]" self.status(_(songname), 0) def display_fileinfo(self): fileinfo = xmmsalike.get_playlist_file(xmmsalike.get_playlist_pos()) self.status(_(fileinfo), 0) def play(self): xmmsalike.play() self.display_title() def stop(self): xmmsalike.stop() self.counter() self.progress() def next(self): xmmsalike.playlist_next() self.display_title() def prev(self): xmmsalike.playlist_prev() self.display_title() def pause(self): xmmsalike.pause() self.display_title() def toggle_repeat(self): xmmsalike.toggle_repeat() self.counter() def toggle_shuffle(self): xmmsalike.toggle_shuffle() self.counter() def run(self): self.status(_("Starting player..."), 0) while not xmmsalike.is_running(): # wait for xmms to load time.sleep(0.2) self.status(_(""), 0) if xmmsalike.get_playlist_length(): app.win_tab.change_window() while 1: now = time.time() timeout = self.timeout.check(now) self.win_filelist.listdir_maybe(now) # apparently, a paused song is_playing technically, but # it's useful to disable these updates while is_paused if xmmsalike.is_playing() and not xmmsalike.is_paused(): timeout = 1 self.counter() # progress bar is hard, we don't have total times self.progress() self.display_title() # basically needed for automatic song changes R = [sys.stdin] try: r, w, e = select.select(R, [], [], timeout) except select.error: continue ## user input if sys.stdin in r: c = self.win_root.getch() self.keymapstack.process(c) def toggle_counter_mode(self): self.win_root.win_counter.toggle_mode() def seek(self, direction): xmmsalike.jump_to_time(xmmsalike.get_output_time() + direction * self.jump_const) def change_volume(self, dv): # single number self.set_volume(self.get_volume() + dv) def inc_volume(self): self.change_volume(+2) def dec_volume(self): self.change_volume(-2) def key_volume(self, ch): self.set_volume((ch & 0x0f) * 10) def get_volume(self): # single number self.volume = xmmsalike.get_main_volume() return self.volume def set_volume(self, v): # single number xmmsalike.set_main_volume(v) def quit(self): if self.daemon_pid: xmmsalike.stop() # Let it play if xmms is running elsewhere. xmmsalike.quit() os.kill(self.daemon_pid, signal.SIGKILL) sys.exit(0) def handler_resize(self, sig, frame): ## curses trickery curses.endwin() self.w.refresh() self.win_root.resize() self.win_root.update() def handler_quit(self, sig, frame): self.quit() # ------------------------------------------ def main(): try: opts, args = getopt.getopt(sys.argv[1:], "rR") except: usage = _("Usage: %s [-rR] [ file | dir | playlist.m3u ] ...\n") sys.stderr.write(usage % sys.argv[0]) sys.exit(1) global app app = Application() # initialize xmmsalike. # If there is already a player running, it will be found here. player = xmmsalike.init() # No running player -> start and init default_player. if player == "": player = default_player environ = os.environ #if not environ.has_key("DISPLAY"): # It's a matter of preference whether you want to change an # existing DISPLAY variable. For my usage it is essential to # do so. This probably needs some kind of setting... environ.update({"DISPLAY": default_x_display}) # see if we have X.. there has to be a better way. If there's # no X this takes some time when the program tries to connect # to the X server. #test_args = ["xdpyinfo"] #x_test = os.spawnvpe(os.P_WAIT, test_args[0], test_args, environ) x_test = os.system("DISPLAY=" + default_x_display + " xdpyinfo >/dev/null") if x_test == 0: player_args = [player] os.spawnvpe(os.P_NOWAIT, player_args[0], player_args, environ) # leave xmms running app.daemon_pid = 0 else: if player == "audacious": # we can use the headless mode that doesn't need X. To # keep track of the player, we quit it when quitting # comms in this case. Since daemon_pid is used to kill # Xvfb later, we can use the same marker here. player_args = ["audacious", "--headless"] app.daemon_pid = os.spawnvp(os.P_NOWAIT, player_args[0], player_args) else: # start virtual X server and player. I have no # interest in maintaining this section though, so I # may remove it in the future. Xvfb_display = ":2" fontpath = "unix/:-1" Xvfb_args = ["Xvfb", Xvfb_display, "-fp", fontpath] app.daemon_pid = os.spawnvp(os.P_NOWAIT, Xvfb_args[0], Xvfb_args) os.system("sleep 1") player_args = [player] environ.update({"DISPLAY": Xvfb_display}) os.spawnvpe(os.P_NOWAIT, player_args[0], player_args, environ) # the argument is essential, since the player takes some # time to get running and recognized. xmmsalike.init(player) else: app.daemon_pid = 0 playlist = [] # if not sys.stdin.isatty(): # playlist = map(string.strip, sys.stdin.readlines()) # os.close(0) # os.open("/dev/tty", 0) try: app.setup() # for opt, optarg in opts: # if opt == '-r': app.win_playlist.command_toggle_repeat() # if opt == '-R': app.win_playlist.command_toggle_random() # if args or playlist: # for item in args or playlist: # app.win_playlist.append(item) app.run() except SystemExit: app.cleanup() except Exception: app.cleanup() import traceback traceback.print_exc() # ------------------------------------------ RE_PLAYLIST = re.compile(".*\.m3u$", re.I) def VALID_PLAYLIST(name): if RE_PLAYLIST.match(name): return 1 # ------------------------------------------ if __name__ == "__main__": main()