meillo@0: #!/usr/bin/env python meillo@0: # -*- python -*- meillo@0: meillo@0: __version__ = "cplay 1.49" meillo@0: meillo@0: """ meillo@0: cplay - A curses front-end for various audio players meillo@0: Copyright (C) 1998-2003 Ulf Betlehem meillo@0: meillo@0: This program is free software; you can redistribute it and/or meillo@0: modify it under the terms of the GNU General Public License meillo@0: as published by the Free Software Foundation; either version 2 meillo@0: of the License, or (at your option) any later version. meillo@0: meillo@0: This program is distributed in the hope that it will be useful, meillo@0: but WITHOUT ANY WARRANTY; without even the implied warranty of meillo@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the meillo@0: GNU General Public License for more details. meillo@0: meillo@0: You should have received a copy of the GNU General Public License meillo@0: along with this program; if not, write to the Free Software meillo@0: Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. meillo@0: """ meillo@0: meillo@0: # ------------------------------------------ meillo@0: from types import * meillo@0: meillo@0: import os meillo@0: import sys meillo@0: import time meillo@0: import getopt meillo@0: import signal meillo@0: import string meillo@0: import select meillo@0: import re meillo@0: meillo@0: try: from ncurses import curses meillo@0: except ImportError: import curses meillo@0: meillo@0: try: import tty meillo@0: except ImportError: tty = None meillo@0: meillo@0: try: import locale; locale.setlocale(locale.LC_ALL, "") meillo@0: except: pass meillo@0: meillo@0: # ------------------------------------------ meillo@0: _locale_domain = "cplay" meillo@0: _locale_dir = "/usr/local/share/locale" meillo@0: meillo@0: try: meillo@0: import gettext # python 2.0 meillo@0: gettext.install(_locale_domain, _locale_dir) meillo@0: except ImportError: meillo@0: try: meillo@0: import fintl meillo@0: fintl.bindtextdomain(_locale_domain, _locale_dir) meillo@0: fintl.textdomain(_locale_domain) meillo@0: _ = fintl.gettext meillo@0: except ImportError: meillo@0: def _(s): return s meillo@0: except: meillo@0: def _(s): return s meillo@0: meillo@0: # ------------------------------------------ meillo@0: XTERM = re.search("rxvt|xterm", os.environ["TERM"]) meillo@0: CONTROL_FIFO = "/var/tmp/cplay_control" meillo@0: meillo@0: # ------------------------------------------ meillo@0: def which(program): meillo@0: for path in string.split(os.environ["PATH"], ":"): meillo@0: if os.path.exists(os.path.join(path, program)): meillo@0: return os.path.join(path, program) meillo@0: meillo@0: # ------------------------------------------ meillo@0: def cut(s, n, left=0): meillo@0: if left: return len(s) > n and "<%s" % s[-n+1:] or s meillo@0: else: return len(s) > n and "%s>" % s[:n-1] or s meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Stack: meillo@0: def __init__(self): meillo@0: self.items = () meillo@0: meillo@0: def push(self, item): meillo@0: self.items = (item,) + self.items meillo@0: meillo@0: def pop(self): meillo@0: self.items, item = self.items[1:], self.items[0] meillo@0: return item meillo@0: meillo@0: # ------------------------------------------ meillo@0: class KeymapStack(Stack): meillo@0: def process(self, code): meillo@0: for keymap in self.items: meillo@0: if keymap and keymap.process(code): meillo@0: break meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Keymap: meillo@0: def __init__(self): meillo@0: self.methods = [None] * curses.KEY_MAX meillo@0: meillo@0: def bind(self, key, method, args=None): meillo@0: if type(key) in (TupleType, ListType): meillo@0: for i in key: self.bind(i, method, args) meillo@0: return meillo@0: if type(key) is StringType: meillo@0: key = ord(key) meillo@0: self.methods[key] = (method, args) meillo@0: meillo@0: def process(self, key): meillo@0: if self.methods[key] is None: return 0 meillo@0: method, args = self.methods[key] meillo@0: if args is None: meillo@0: apply(method, (key,)) meillo@0: else: meillo@0: apply(method, args) meillo@0: return 1 meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Window: meillo@0: chars = string.letters+string.digits+string.punctuation+string.whitespace meillo@0: meillo@0: t = ['?'] * 256 meillo@0: for c in chars: t[ord(c)] = c meillo@0: translationTable = string.join(t, ""); del t meillo@0: meillo@0: def __init__(self, parent): meillo@0: self.parent = parent meillo@0: self.children = [] meillo@0: self.name = None meillo@0: self.keymap = None meillo@0: self.visible = 1 meillo@0: self.resize() meillo@0: if parent: parent.children.append(self) meillo@0: meillo@0: def insstr(self, s): meillo@0: if not s: return meillo@0: self.w.addstr(s[:-1]) meillo@0: self.w.hline(ord(s[-1]), 1) # insch() work-around meillo@0: meillo@0: def __getattr__(self, name): meillo@0: return getattr(self.w, name) meillo@0: meillo@0: def getmaxyx(self): meillo@0: y, x = self.w.getmaxyx() meillo@0: try: curses.version # tested with 1.2 and 1.6 meillo@0: except AttributeError: meillo@0: # pyncurses - emulate traditional (silly) behavior meillo@0: y, x = y+1, x+1 meillo@0: return y, x meillo@0: meillo@0: def touchwin(self): meillo@0: try: self.w.touchwin() meillo@0: except AttributeError: self.touchln(0, self.getmaxyx()[0]) meillo@0: meillo@0: def attron(self, attr): meillo@0: try: self.w.attron(attr) meillo@0: except AttributeError: self.w.attr_on(attr) meillo@0: meillo@0: def attroff(self, attr): meillo@0: try: self.w.attroff(attr) meillo@0: except AttributeError: self.w.attr_off(attr) meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(0, 0, 0, 0) meillo@0: meillo@0: def resize(self): meillo@0: self.w = self.newwin() meillo@0: self.ypos, self.xpos = self.getbegyx() meillo@0: self.rows, self.cols = self.getmaxyx() meillo@0: self.keypad(1) meillo@0: self.leaveok(0) meillo@0: self.scrollok(0) meillo@0: for child in self.children: meillo@0: child.resize() meillo@0: meillo@0: def update(self): meillo@0: self.clear() meillo@0: self.refresh() meillo@0: for child in self.children: meillo@0: child.update() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class ProgressWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: self.value = 0 meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(1, self.parent.cols, self.parent.rows-2, 0) meillo@0: meillo@0: def update(self): meillo@0: self.move(0, 0) meillo@0: self.hline(ord('-'), self.cols) meillo@0: if self.value > 0: meillo@0: self.move(0, 0) meillo@0: x = int(self.value * self.cols) # 0 to cols-1 meillo@0: x and self.hline(ord('='), x) meillo@0: self.move(0, x) meillo@0: self.insstr('|') meillo@0: self.touchwin() meillo@0: self.refresh() meillo@0: meillo@0: def progress(self, value): meillo@0: self.value = min(value, 0.99) meillo@0: self.update() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class StatusWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: self.default_message = '' meillo@0: self.current_message = '' meillo@0: self.tid = None meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(1, self.parent.cols-12, self.parent.rows-1, 0) meillo@0: meillo@0: def update(self): meillo@0: msg = string.translate(self.current_message, Window.translationTable) meillo@0: self.move(0, 0) meillo@0: self.clrtoeol() meillo@0: self.insstr(cut(msg, self.cols)) meillo@0: self.touchwin() meillo@0: self.refresh() meillo@0: meillo@0: def status(self, message, duration = 0): meillo@0: self.current_message = str(message) meillo@0: if self.tid: app.timeout.remove(self.tid) meillo@0: if duration: self.tid = app.timeout.add(duration, self.timeout) meillo@0: else: self.tid = None meillo@0: self.update() meillo@0: meillo@0: def timeout(self): meillo@0: self.tid = None meillo@0: self.restore_default_status() meillo@0: meillo@0: def set_default_status(self, message): meillo@0: if self.current_message == self.default_message: self.status(message) meillo@0: self.default_message = message meillo@0: XTERM and sys.stderr.write("\033]0;%s\a" % (message or "cplay")) meillo@0: meillo@0: def restore_default_status(self): meillo@0: self.status(self.default_message) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class CounterWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: self.values = [0, 0] meillo@0: self.mode = 1 meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(1, 11, self.parent.rows-1, self.parent.cols-11) meillo@0: meillo@0: def update(self): meillo@0: h, s = divmod(self.values[self.mode], 3600) meillo@0: m, s = divmod(s, 60) meillo@0: self.move(0, 0) meillo@0: self.attron(curses.A_BOLD) meillo@0: self.insstr("%02dh %02dm %02ds" % (h, m, s)) meillo@0: self.attroff(curses.A_BOLD) meillo@0: self.touchwin() meillo@0: self.refresh() meillo@0: meillo@0: def counter(self, values): meillo@0: self.values = values meillo@0: self.update() meillo@0: meillo@0: def toggle_mode(self): meillo@0: self.mode = not self.mode meillo@0: tmp = [_("elapsed"), _("remaining")][self.mode] meillo@0: app.status(_("Counting %s time") % tmp, 1) meillo@0: self.update() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class RootWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: keymap = Keymap() meillo@0: app.keymapstack.push(keymap) meillo@0: self.win_progress = ProgressWindow(self) meillo@0: self.win_status = StatusWindow(self) meillo@0: self.win_counter = CounterWindow(self) meillo@0: self.win_tab = TabWindow(self) meillo@0: keymap.bind(12, self.update, ()) # C-l meillo@0: keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1, 1)) # C-b meillo@0: keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1, 1)) # C-f meillo@0: keymap.bind([1, '^'], app.seek, (0, 0)) # C-a meillo@0: keymap.bind([5, '$'], app.seek, (-1, 0)) # C-e meillo@0: keymap.bind(range(48,58), app.key_volume) # 0123456789 meillo@0: keymap.bind(['+', '='], app.inc_volume, ()) meillo@0: keymap.bind('-', app.dec_volume, ()) meillo@0: keymap.bind('n', app.next_song, ()) meillo@0: keymap.bind('p', app.prev_song, ()) meillo@0: keymap.bind('z', app.toggle_pause, ()) meillo@0: keymap.bind('x', app.toggle_stop, ()) meillo@0: keymap.bind('c', self.win_counter.toggle_mode, ()) meillo@0: keymap.bind('Q', app.quit, ()) meillo@0: keymap.bind('q', self.command_quit, ()) meillo@0: keymap.bind('v', app.mixer, ("toggle",)) meillo@0: meillo@0: def command_quit(self): meillo@0: app.do_input_hook = self.do_quit meillo@0: app.start_input(_("Quit? (y/N)")) meillo@0: meillo@0: def do_quit(self, ch): meillo@0: if chr(ch) == 'y': app.quit() meillo@0: app.stop_input() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class TabWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: self.active_child = 0 meillo@0: meillo@0: self.win_filelist = self.add(FilelistWindow) meillo@0: self.win_playlist = self.add(PlaylistWindow) meillo@0: self.win_help = self.add(HelpWindow) meillo@0: meillo@0: keymap = Keymap() meillo@0: keymap.bind('\t', self.change_window, ()) # tab meillo@0: keymap.bind('h', self.help, ()) meillo@0: app.keymapstack.push(keymap) meillo@0: app.keymapstack.push(self.children[self.active_child].keymap) meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(self.parent.rows-2, self.parent.cols, 0, 0) meillo@0: meillo@0: def update(self): meillo@0: self.update_title() meillo@0: self.move(1, 0) meillo@0: self.hline(ord('-'), self.cols) meillo@0: self.move(2, 0) meillo@0: self.clrtobot() meillo@0: self.refresh() meillo@0: child = self.children[self.active_child] meillo@0: child.visible = 1 meillo@0: child.update() meillo@0: meillo@0: def update_title(self, refresh = 1): meillo@0: child = self.children[self.active_child] meillo@0: self.move(0, 0) meillo@0: self.clrtoeol() meillo@0: self.attron(curses.A_BOLD) meillo@0: self.insstr(child.get_title()) meillo@0: self.attroff(curses.A_BOLD) meillo@0: if refresh: self.refresh() meillo@0: meillo@0: def add(self, Class): meillo@0: win = Class(self) meillo@0: win.visible = 0 meillo@0: return win meillo@0: meillo@0: def change_window(self, window = None): meillo@0: app.keymapstack.pop() meillo@0: self.children[self.active_child].visible = 0 meillo@0: if window: meillo@0: self.active_child = self.children.index(window) meillo@0: else: meillo@0: # toggle windows 0 and 1 meillo@0: self.active_child = not self.active_child meillo@0: app.keymapstack.push(self.children[self.active_child].keymap) meillo@0: self.update() meillo@0: meillo@0: def help(self): meillo@0: if self.children[self.active_child] == self.win_help: meillo@0: self.change_window(self.win_last) meillo@0: else: meillo@0: self.win_last = self.children[self.active_child] meillo@0: self.change_window(self.win_help) meillo@0: app.status(__version__, 2) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class ListWindow(Window): meillo@0: def __init__(self, parent): meillo@0: Window.__init__(self, parent) meillo@0: self.buffer = [] meillo@0: self.bufptr = self.scrptr = 0 meillo@0: self.search_direction = 0 meillo@0: self.last_search = "" meillo@0: self.hoffset = 0 meillo@0: self.keymap = Keymap() meillo@0: self.keymap.bind(['k', curses.KEY_UP, 16], self.cursor_move, (-1,)) meillo@0: self.keymap.bind(['j', curses.KEY_DOWN, 14], self.cursor_move, (1,)) meillo@0: self.keymap.bind(['K', curses.KEY_PPAGE], self.cursor_ppage, ()) meillo@0: self.keymap.bind(['J', curses.KEY_NPAGE], self.cursor_npage, ()) meillo@0: self.keymap.bind(['g', curses.KEY_HOME], self.cursor_home, ()) meillo@0: self.keymap.bind(['G', curses.KEY_END], self.cursor_end, ()) meillo@0: self.keymap.bind(['?', 18], self.start_search, meillo@0: (_("backward-isearch"), -1)) meillo@0: self.keymap.bind(['/', 19], self.start_search, meillo@0: (_("forward-isearch"), 1)) meillo@0: self.keymap.bind(['>'], self.hscroll, (8,)) meillo@0: self.keymap.bind(['<'], self.hscroll, (-8,)) meillo@0: meillo@0: def newwin(self): meillo@0: return curses.newwin(self.parent.rows-2, self.parent.cols, meillo@0: self.parent.ypos+2, self.parent.xpos) meillo@0: meillo@0: def update(self, force = 1): meillo@0: self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1)) meillo@0: scrptr = (self.bufptr / self.rows) * self.rows meillo@0: if force or self.scrptr != scrptr: meillo@0: self.scrptr = scrptr meillo@0: self.move(0, 0) meillo@0: self.clrtobot() meillo@0: i = 0 meillo@0: for entry in self.buffer[self.scrptr:]: meillo@0: self.move(i, 0) meillo@0: i = i + 1 meillo@0: self.putstr(entry) meillo@0: if self.getyx()[0] == self.rows - 1: break meillo@0: if self.visible: meillo@0: self.refresh() meillo@0: self.parent.update_title() meillo@0: self.update_line(curses.A_REVERSE) meillo@0: meillo@0: def update_line(self, attr = None, refresh = 1): meillo@0: if not self.buffer: return meillo@0: ypos = self.bufptr - self.scrptr meillo@0: if attr: self.attron(attr) meillo@0: self.move(ypos, 0) meillo@0: self.hline(ord(' '), self.cols) meillo@0: self.putstr(self.current()) meillo@0: if attr: self.attroff(attr) meillo@0: if self.visible and refresh: self.refresh() meillo@0: meillo@0: def get_title(self, data=""): meillo@0: pos = "%s-%s/%s" % (self.scrptr+min(1, len(self.buffer)), meillo@0: min(self.scrptr+self.rows, len(self.buffer)), meillo@0: len(self.buffer)) meillo@0: width = self.cols-len(pos)-2 meillo@0: data = cut(data, width-len(self.name), 1) meillo@0: return "%-*s %s" % (width, cut(self.name+data, width), pos) meillo@0: meillo@0: def putstr(self, entry, *pos): meillo@0: s = string.translate(str(entry), Window.translationTable) meillo@0: pos and apply(self.move, pos) meillo@0: if self.hoffset: s = "<%s" % s[self.hoffset+1:] meillo@0: self.insstr(cut(s, self.cols)) meillo@0: meillo@0: def current(self): meillo@0: if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1 meillo@0: return self.buffer[self.bufptr] meillo@0: meillo@0: def cursor_move(self, ydiff): meillo@0: if app.input_mode: app.cancel_input() meillo@0: if not self.buffer: return meillo@0: self.update_line(refresh = 0) meillo@0: self.bufptr = (self.bufptr + ydiff) % len(self.buffer) meillo@0: self.update(force = 0) meillo@0: meillo@0: def cursor_ppage(self): meillo@0: tmp = self.bufptr % self.rows meillo@0: if tmp == self.bufptr: meillo@0: self.cursor_move(-(tmp+(len(self.buffer) % self.rows) or self.rows)) meillo@0: else: meillo@0: self.cursor_move(-(tmp+self.rows)) meillo@0: meillo@0: def cursor_npage(self): meillo@0: tmp = self.rows - self.bufptr % self.rows meillo@0: if self.bufptr + tmp > len(self.buffer): meillo@0: self.cursor_move(len(self.buffer) - self.bufptr) meillo@0: else: meillo@0: self.cursor_move(tmp) meillo@0: meillo@0: def cursor_home(self): self.cursor_move(-self.bufptr) meillo@0: meillo@0: def cursor_end(self): self.cursor_move(-self.bufptr - 1) meillo@0: meillo@0: def start_search(self, type, direction): meillo@0: self.search_direction = direction meillo@0: self.not_found = 0 meillo@0: if app.input_mode: meillo@0: app.input_prompt = "%s: " % type meillo@0: self.do_search(advance = direction) meillo@0: else: meillo@0: app.do_input_hook = self.do_search meillo@0: app.stop_input_hook = self.stop_search meillo@0: app.start_input(type) meillo@0: meillo@0: def stop_search(self): meillo@0: self.last_search = app.input_string meillo@0: app.status(_("ok"), 1) meillo@0: meillo@0: def do_search(self, ch = None, advance = 0): meillo@0: if ch in [8, 127]: app.input_string = app.input_string[:-1] meillo@0: elif ch: app.input_string = "%s%c" % (app.input_string, ch) meillo@0: else: app.input_string = app.input_string or self.last_search meillo@0: index = self.bufptr + advance meillo@0: while 1: meillo@0: if not 0 <= index < len(self.buffer): meillo@0: app.status(_("Not found: %s ") % app.input_string) meillo@0: self.not_found = 1 meillo@0: break meillo@0: line = string.lower(str(self.buffer[index])) meillo@0: if string.find(line, string.lower(app.input_string)) != -1: meillo@0: app.show_input() meillo@0: self.update_line(refresh = 0) meillo@0: self.bufptr = index meillo@0: self.update(force = 0) meillo@0: self.not_found = 0 meillo@0: break meillo@0: if self.not_found: meillo@0: app.status(_("Not found: %s ") % app.input_string) meillo@0: break meillo@0: index = index + self.search_direction meillo@0: meillo@0: def hscroll(self, value): meillo@0: self.hoffset = max(0, self.hoffset + value) meillo@0: self.update() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class HelpWindow(ListWindow): meillo@0: def __init__(self, parent): meillo@0: ListWindow.__init__(self, parent) meillo@0: self.name = _("Help") meillo@0: self.keymap.bind('q', self.parent.help, ()) meillo@0: self.buffer = string.split(_("""\ meillo@0: Global t, T : tag current/regex meillo@0: ------ u, U : untag current/regex meillo@0: Up, Down, k, j, C-p, C-n, Sp, i : invert current/all meillo@0: PgUp, PgDn, K, J, ! : shell ($@ = tagged or current) meillo@0: Home, End, g, G : movement meillo@0: Enter : chdir or play Filelist meillo@0: Tab : filelist/playlist -------- meillo@0: n, p : next/prev track a : add (tagged) to playlist meillo@0: z, x : toggle pause/stop s : recursive search meillo@0: BS, o : goto parent/specified dir meillo@0: Left, Right, m, ' : set/get bookmark meillo@0: C-f, C-b : seek forward/backward meillo@0: C-a, C-e : restart/end track Playlist meillo@0: C-s, C-r, / : isearch -------- meillo@0: C-g, Esc : cancel d, D : delete (tagged) tracks/playlist meillo@0: 1..9, +, - : volume control m, M : move tagged tracks after/before meillo@0: c, v : counter/volume mode r, R : toggle repeat/Random mode meillo@0: <, > : horizontal scrolling s, S : shuffle/Sort playlist meillo@0: C-l, l : refresh, list mode w, @ : write playlist, jump to active meillo@0: h, q, Q : help, quit?, Quit! X : stop playlist after each track meillo@0: """), "\n") meillo@0: meillo@0: # ------------------------------------------ meillo@0: class ListEntry: meillo@0: def __init__(self, pathname, dir=0): meillo@0: self.filename = os.path.basename(pathname) meillo@0: self.pathname = pathname meillo@0: self.slash = dir and "/" or "" meillo@0: self.tagged = 0 meillo@0: meillo@0: def set_tagged(self, value): meillo@0: self.tagged = value meillo@0: meillo@0: def is_tagged(self): meillo@0: return self.tagged == 1 meillo@0: meillo@0: def __str__(self): meillo@0: mark = self.is_tagged() and "#" or " " meillo@0: return "%s %s%s" % (mark, self.vp(), self.slash) meillo@0: meillo@0: def vp(self): meillo@0: return self.vps[0][1](self) meillo@0: meillo@0: def vp_filename(self): meillo@0: return self.filename or self.pathname meillo@0: meillo@0: def vp_pathname(self): meillo@0: return self.pathname meillo@0: meillo@0: vps = [[_("filename"), vp_filename], meillo@0: [_("pathname"), vp_pathname]] meillo@0: meillo@0: # ------------------------------------------ meillo@0: class PlaylistEntry(ListEntry): meillo@0: def __init__(self, pathname): meillo@0: ListEntry.__init__(self, pathname) meillo@0: self.metadata = None meillo@0: self.active = 0 meillo@0: meillo@0: def set_active(self, value): meillo@0: self.active = value meillo@0: meillo@0: def is_active(self): meillo@0: return self.active == 1 meillo@0: meillo@0: def vp_metadata(self): meillo@0: return self.metadata or self.read_metadata() meillo@0: meillo@0: def read_metadata(self): meillo@0: self.metadata = get_tag(self.pathname) meillo@0: return self.metadata meillo@0: meillo@0: vps = ListEntry.vps[:] meillo@0: meillo@0: # ------------------------------------------ meillo@0: class TagListWindow(ListWindow): meillo@0: def __init__(self, parent): meillo@0: ListWindow.__init__(self, parent) meillo@0: self.keymap.bind(' ', self.command_tag_untag, ()) meillo@0: self.keymap.bind('i', self.command_invert_tags, ()) meillo@0: self.keymap.bind('t', self.command_tag, (1,)) meillo@0: self.keymap.bind('u', self.command_tag, (0,)) meillo@0: self.keymap.bind('T', self.command_tag_regexp, (1,)) meillo@0: self.keymap.bind('U', self.command_tag_regexp, (0,)) meillo@0: self.keymap.bind('l', self.command_change_viewpoint, ()) meillo@0: meillo@0: def command_change_viewpoint(self, klass=ListEntry): meillo@0: klass.vps.append(klass.vps.pop(0)) meillo@0: app.status(_("Listing %s") % klass.vps[0][0], 1) meillo@0: app.player.update_status() meillo@0: self.update() meillo@0: meillo@0: def command_invert_tags(self): meillo@0: for i in self.buffer: meillo@0: i.set_tagged(not i.is_tagged()) meillo@0: self.update() meillo@0: meillo@0: def command_tag_untag(self): meillo@0: if not self.buffer: return meillo@0: tmp = self.buffer[self.bufptr] meillo@0: tmp.set_tagged(not tmp.is_tagged()) meillo@0: self.cursor_move(1) meillo@0: meillo@0: def command_tag(self, value): meillo@0: if not self.buffer: return meillo@0: self.buffer[self.bufptr].set_tagged(value) meillo@0: self.cursor_move(1) meillo@0: meillo@0: def command_tag_regexp(self, value): meillo@0: self.tag_value = value meillo@0: app.stop_input_hook = self.stop_tag_regexp meillo@0: app.start_input(value and _("Tag regexp") or _("Untag regexp")) meillo@0: meillo@0: def stop_tag_regexp(self): meillo@0: try: meillo@0: r = re.compile(app.input_string, re.I) meillo@0: for entry in self.buffer: meillo@0: if r.search(str(entry)): meillo@0: entry.set_tagged(self.tag_value) meillo@0: self.update() meillo@0: app.status(_("ok"), 1) meillo@0: except re.error, e: meillo@0: app.status(e, 2) meillo@0: meillo@0: def get_tagged(self): meillo@0: return filter(lambda x: x.is_tagged(), self.buffer) meillo@0: meillo@0: def not_tagged(self, l): meillo@0: return filter(lambda x: not x.is_tagged(), l) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class FilelistWindow(TagListWindow): meillo@0: def __init__(self, parent): meillo@0: TagListWindow.__init__(self, parent) meillo@0: self.oldposition = {} meillo@0: try: self.chdir(os.getcwd()) meillo@0: except OSError: self.chdir(os.environ['HOME']) meillo@0: self.startdir = self.cwd meillo@0: self.mtime_when = 0 meillo@0: self.mtime = None meillo@0: self.keymap.bind(['\n', curses.KEY_ENTER], meillo@0: self.command_chdir_or_play, ()) meillo@0: self.keymap.bind(['.', curses.KEY_BACKSPACE], meillo@0: self.command_chparentdir, ()) meillo@0: self.keymap.bind('a', self.command_add_recursively, ()) meillo@0: self.keymap.bind('o', self.command_goto, ()) meillo@0: self.keymap.bind('s', self.command_search_recursively, ()) meillo@0: self.keymap.bind('m', self.command_set_bookmark, ()) meillo@0: self.keymap.bind("'", self.command_get_bookmark, ()) meillo@0: self.keymap.bind('!', self.command_shell, ()) meillo@0: self.bookmarks = { 39: [self.cwd, 0] } meillo@0: meillo@0: def command_shell(self): meillo@0: if app.restricted: return meillo@0: app.stop_input_hook = self.stop_shell meillo@0: app.complete_input_hook = self.complete_shell meillo@0: app.start_input(_("shell$ "), colon=0) meillo@0: meillo@0: def stop_shell(self): meillo@0: s = app.input_string meillo@0: curses.endwin() meillo@0: sys.stderr.write("\n") meillo@0: argv = map(lambda x: x.pathname, self.get_tagged() or [self.current()]) meillo@0: argv = ["/bin/sh", "-c", s, "--"] + argv meillo@0: pid = os.fork() meillo@0: if pid == 0: meillo@0: try: os.execv(argv[0], argv) meillo@0: except: os._exit(1) meillo@0: pid, r = os.waitpid(pid, 0) meillo@0: sys.stderr.write("\nshell returned %s, press return!\n" % r) meillo@0: sys.stdin.readline() meillo@0: app.win_root.update() meillo@0: app.restore_default_status() meillo@0: app.cursor(0) meillo@0: meillo@0: def complete_shell(self, line): meillo@0: return self.complete_generic(line, quote=1) meillo@0: meillo@0: def complete_generic(self, line, quote=0): meillo@0: import glob meillo@0: if quote: meillo@0: s = re.sub('.*[^\\\\][ \'"()\[\]{}$`]', '', line) meillo@0: s, part = re.sub('\\\\', '', s), line[:len(line)-len(s)] meillo@0: else: meillo@0: s, part = line, "" meillo@0: results = glob.glob(os.path.expanduser(s)+"*") meillo@0: if len(results) == 0: meillo@0: return line meillo@0: if len(results) == 1: meillo@0: lm = results[0] meillo@0: lm = lm + (os.path.isdir(lm) and "/" or "") meillo@0: else: meillo@0: lm = results[0] meillo@0: for result in results: meillo@0: for i in range(min(len(result), len(lm))): meillo@0: if result[i] != lm[i]: meillo@0: lm = lm[:i] meillo@0: break meillo@0: if quote: lm = re.sub('([ \'"()\[\]{}$`])', '\\\\\\1', lm) meillo@0: return part + lm meillo@0: meillo@0: def command_get_bookmark(self): meillo@0: app.do_input_hook = self.do_get_bookmark meillo@0: app.start_input(_("bookmark")) meillo@0: meillo@0: def do_get_bookmark(self, ch): meillo@0: app.input_string = ch meillo@0: bookmark = self.bookmarks.get(ch) meillo@0: if bookmark: meillo@0: self.bookmarks[39] = [self.cwd, self.bufptr] meillo@0: dir, pos = bookmark meillo@0: self.chdir(dir) meillo@0: self.listdir() meillo@0: self.bufptr = pos meillo@0: self.update() meillo@0: app.status(_("ok"), 1) meillo@0: else: meillo@0: app.status(_("Not found!"), 1) meillo@0: app.stop_input() meillo@0: meillo@0: def command_set_bookmark(self): meillo@0: app.do_input_hook = self.do_set_bookmark meillo@0: app.start_input(_("set bookmark")) meillo@0: meillo@0: def do_set_bookmark(self, ch): meillo@0: app.input_string = ch meillo@0: self.bookmarks[ch] = [self.cwd, self.bufptr] meillo@0: ch and app.status(_("ok"), 1) or app.stop_input() meillo@0: meillo@0: def command_search_recursively(self): meillo@0: app.stop_input_hook = self.stop_search_recursively meillo@0: app.start_input(_("search")) meillo@0: meillo@0: def stop_search_recursively(self): meillo@0: try: re_tmp = re.compile(app.input_string, re.I) meillo@0: except re.error, e: meillo@0: app.status(e, 2) meillo@0: return meillo@0: app.status(_("Searching...")) meillo@0: results = [] meillo@0: for entry in self.buffer: meillo@0: if entry.filename == "..": meillo@0: continue meillo@0: if re_tmp.search(entry.filename): meillo@0: results.append(entry) meillo@0: elif os.path.isdir(entry.pathname): meillo@0: try: self.search_recursively(re_tmp, entry.pathname, results) meillo@0: except: pass meillo@0: if not self.search_mode: meillo@0: self.chdir(os.path.join(self.cwd,_("search results"))) meillo@0: self.search_mode = 1 meillo@0: self.buffer = results meillo@0: self.bufptr = 0 meillo@0: self.parent.update_title() meillo@0: self.update() meillo@0: app.restore_default_status() meillo@0: meillo@0: def search_recursively(self, re_tmp, dir, results): meillo@0: for filename in os.listdir(dir): meillo@0: pathname = os.path.join(dir, filename) meillo@0: if re_tmp.search(filename): meillo@0: if os.path.isdir(pathname): meillo@0: results.append(ListEntry(pathname, 1)) meillo@0: elif VALID_PLAYLIST(filename) or VALID_SONG(filename): meillo@0: results.append(ListEntry(pathname)) meillo@0: elif os.path.isdir(pathname): meillo@0: self.search_recursively(re_tmp, pathname, results) meillo@0: meillo@0: def get_title(self): meillo@0: self.name = _("Filelist: ") meillo@0: return ListWindow.get_title(self, re.sub("/?$", "/", self.cwd)) meillo@0: meillo@0: def listdir_maybe(self, now=0): meillo@0: if now < self.mtime_when+2: return meillo@0: self.mtime_when = now meillo@0: self.oldposition[self.cwd] = self.bufptr meillo@0: try: self.mtime == os.stat(self.cwd)[8] or self.listdir(quiet=1) meillo@0: except os.error: pass meillo@0: meillo@0: def listdir(self, quiet=0, prevdir=None): meillo@0: quiet or app.status(_("Reading directory...")) meillo@0: self.search_mode = 0 meillo@0: dirs = [] meillo@0: files = [] meillo@0: try: meillo@0: self.mtime = os.stat(self.cwd)[8] meillo@0: self.mtime_when = time.time() meillo@0: filenames = os.listdir(self.cwd) meillo@0: filenames.sort() meillo@0: for filename in filenames: meillo@0: if filename[0] == ".": continue meillo@0: pathname = os.path.join(self.cwd, filename) meillo@0: if os.path.isdir(pathname): dirs.append(pathname) meillo@0: elif VALID_SONG(filename): files.append(pathname) meillo@0: elif VALID_PLAYLIST(filename): files.append(pathname) meillo@0: except os.error: pass meillo@0: dots = ListEntry(os.path.join(self.cwd, ".."), 1) meillo@0: self.buffer = [[dots], []][self.cwd == "/"] meillo@0: for i in dirs: self.buffer.append(ListEntry(i, 1)) meillo@0: for i in files: self.buffer.append(ListEntry(i)) meillo@0: if prevdir: meillo@0: for self.bufptr in range(len(self.buffer)): meillo@0: if self.buffer[self.bufptr].filename == prevdir: break meillo@0: else: self.bufptr = 0 meillo@0: elif self.oldposition.has_key(self.cwd): meillo@0: self.bufptr = self.oldposition[self.cwd] meillo@0: else: self.bufptr = 0 meillo@0: self.parent.update_title() meillo@0: self.update() meillo@0: quiet or app.restore_default_status() meillo@0: meillo@0: def chdir(self, dir): meillo@0: if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr meillo@0: self.cwd = os.path.normpath(dir) meillo@0: try: os.chdir(self.cwd) meillo@0: except: pass meillo@0: meillo@0: def command_chdir_or_play(self): meillo@0: if not self.buffer: return meillo@0: if self.current().filename == "..": meillo@0: self.command_chparentdir() meillo@0: elif os.path.isdir(self.current().pathname): meillo@0: self.chdir(self.current().pathname) meillo@0: self.listdir() meillo@0: elif VALID_SONG(self.current().filename): meillo@0: app.play(self.current()) meillo@0: meillo@0: def command_chparentdir(self): meillo@0: if app.restricted and self.cwd == self.startdir: return meillo@0: dir = os.path.basename(self.cwd) meillo@0: self.chdir(os.path.dirname(self.cwd)) meillo@0: self.listdir(prevdir=dir) meillo@0: meillo@0: def command_goto(self): meillo@0: if app.restricted: return meillo@0: app.stop_input_hook = self.stop_goto meillo@0: app.complete_input_hook = self.complete_generic meillo@0: app.start_input(_("goto")) meillo@0: meillo@0: def stop_goto(self): meillo@0: dir = os.path.expanduser(app.input_string) meillo@0: if dir[0] != '/': dir = os.path.join(self.cwd, dir) meillo@0: if not os.path.isdir(dir): meillo@0: app.status(_("Not a directory!"), 1) meillo@0: return meillo@0: self.chdir(dir) meillo@0: self.listdir() meillo@0: meillo@0: def command_add_recursively(self): meillo@0: l = self.get_tagged() meillo@0: if not l: meillo@0: app.win_playlist.add(self.current().pathname) meillo@0: self.cursor_move(1) meillo@0: return meillo@0: app.status(_("Adding tagged files"), 1) meillo@0: for entry in l: meillo@0: app.win_playlist.add(entry.pathname, quiet=1) meillo@0: entry.set_tagged(0) meillo@0: self.update() meillo@0: meillo@0: # ------------------------------------------ meillo@0: class PlaylistWindow(TagListWindow): meillo@0: def __init__(self, parent): meillo@0: TagListWindow.__init__(self, parent) meillo@0: self.pathname = None meillo@0: self.repeat = 0 meillo@0: self.random = 0 meillo@0: self.random_prev = [] meillo@0: self.random_next = [] meillo@0: self.random_left = [] meillo@0: self.stop = 0 meillo@0: self.keymap.bind(['\n', curses.KEY_ENTER], meillo@0: self.command_play, ()) meillo@0: self.keymap.bind('d', self.command_delete, ()) meillo@0: self.keymap.bind('D', self.command_delete_all, ()) meillo@0: self.keymap.bind('m', self.command_move, (1,)) meillo@0: self.keymap.bind('M', self.command_move, (0,)) meillo@0: self.keymap.bind('s', self.command_shuffle, ()) meillo@0: self.keymap.bind('S', self.command_sort, ()) meillo@0: self.keymap.bind('r', self.command_toggle_repeat, ()) meillo@0: self.keymap.bind('R', self.command_toggle_random, ()) meillo@0: self.keymap.bind('X', self.command_toggle_stop, ()) meillo@0: self.keymap.bind('w', self.command_save_playlist, ()) meillo@0: self.keymap.bind('@', self.command_jump_to_active, ()) meillo@0: meillo@0: def command_change_viewpoint(self, klass=PlaylistEntry): meillo@0: if not globals().get("ID3"): meillo@0: try: meillo@0: global ID3, ogg, codecs meillo@0: import ID3, ogg.vorbis, codecs meillo@0: klass.vps.append([_("metadata"), klass.vp_metadata]) meillo@0: except ImportError: pass meillo@0: TagListWindow.command_change_viewpoint(self, klass) meillo@0: meillo@0: def get_title(self): meillo@0: space_out = lambda value, s: value and s or " "*len(s) meillo@0: self.name = _("Playlist %s %s %s") % ( meillo@0: space_out(self.repeat, _("[repeat]")), meillo@0: space_out(self.random, _("[random]")), meillo@0: space_out(self.stop, _("[stop]"))) meillo@0: return ListWindow.get_title(self) meillo@0: meillo@0: def append(self, item): meillo@0: self.buffer.append(item) meillo@0: if self.random: self.random_left.append(item) meillo@0: meillo@0: def add_dir(self, dir): meillo@0: filenames = os.listdir(dir) meillo@0: filenames.sort() meillo@0: subdirs = [] meillo@0: for filename in filenames: meillo@0: pathname = os.path.join(dir, filename) meillo@0: if VALID_SONG(filename): meillo@0: self.append(PlaylistEntry(pathname)) meillo@0: if os.path.isdir(pathname): meillo@0: subdirs.append(pathname) meillo@0: map(self.add_dir, subdirs) meillo@0: meillo@0: def add_m3u(self, line): meillo@0: if re.match("^(#.*)?$", line): return meillo@0: if re.match("^(/|http://)", line): meillo@0: self.append(PlaylistEntry(self.fix_url(line))) meillo@0: else: meillo@0: dirname = os.path.dirname(self.pathname) meillo@0: self.append(PlaylistEntry(os.path.join(dirname, line))) meillo@0: meillo@0: def add_pls(self, line): meillo@0: # todo - support title & length meillo@0: m = re.match("File(\d+)=(.*)", line) meillo@0: if m: self.append(PlaylistEntry(self.fix_url(m.group(2)))) meillo@0: meillo@0: def add_playlist(self, pathname): meillo@0: self.pathname = pathname meillo@0: if re.search("\.m3u$", pathname, re.I): f = self.add_m3u meillo@0: if re.search("\.pls$", pathname, re.I): f = self.add_pls meillo@0: file = open(pathname) meillo@0: map(f, map(string.strip, file.readlines())) meillo@0: file.close() meillo@0: meillo@0: def add(self, pathname, quiet=0): meillo@0: try: meillo@0: if os.path.isdir(pathname): meillo@0: app.status(_("Working...")) meillo@0: self.add_dir(pathname) meillo@0: elif VALID_PLAYLIST(pathname): meillo@0: self.add_playlist(pathname) meillo@0: else: meillo@0: pathname = self.fix_url(pathname) meillo@0: self.append(PlaylistEntry(pathname)) meillo@0: # todo - refactor meillo@0: filename = os.path.basename(pathname) or pathname meillo@0: quiet or app.status(_("Added: %s") % filename, 1) meillo@0: except Exception, e: meillo@0: app.status(e, 2) meillo@0: meillo@0: def fix_url(self, url): meillo@0: return re.sub("(http://[^/]+)/?(.*)", "\\1/\\2", url) meillo@0: meillo@0: def putstr(self, entry, *pos): meillo@0: if entry.is_active(): self.attron(curses.A_BOLD) meillo@0: apply(ListWindow.putstr, (self, entry) + pos) meillo@0: if entry.is_active(): self.attroff(curses.A_BOLD) meillo@0: meillo@0: def change_active_entry(self, direction): meillo@0: if not self.buffer: return meillo@0: old = self.get_active_entry() meillo@0: new = None meillo@0: if self.random: meillo@0: if direction > 0: meillo@0: if self.random_next: new = self.random_next.pop() meillo@0: elif self.random_left: pass meillo@0: elif self.repeat: self.random_left = self.buffer[:] meillo@0: else: return meillo@0: if not new: meillo@0: import random meillo@0: new = random.choice(self.random_left) meillo@0: self.random_left.remove(new) meillo@0: try: self.random_prev.remove(new) meillo@0: except ValueError: pass meillo@0: self.random_prev.append(new) meillo@0: else: meillo@0: if len(self.random_prev) > 1: meillo@0: self.random_next.append(self.random_prev.pop()) meillo@0: new = self.random_prev[-1] meillo@0: else: return meillo@0: old and old.set_active(0) meillo@0: elif old: meillo@0: index = self.buffer.index(old)+direction meillo@0: if not (0 <= index < len(self.buffer) or self.repeat): return meillo@0: old.set_active(0) meillo@0: new = self.buffer[index % len(self.buffer)] meillo@0: else: meillo@0: new = self.buffer[0] meillo@0: new.set_active(1) meillo@0: self.update() meillo@0: return new meillo@0: meillo@0: def get_active_entry(self): meillo@0: for entry in self.buffer: meillo@0: if entry.is_active(): return entry meillo@0: meillo@0: def command_jump_to_active(self): meillo@0: entry = self.get_active_entry() meillo@0: if not entry: return meillo@0: self.bufptr = self.buffer.index(entry) meillo@0: self.update() meillo@0: meillo@0: def command_play(self): meillo@0: if not self.buffer: return meillo@0: entry = self.get_active_entry() meillo@0: entry and entry.set_active(0) meillo@0: entry = self.current() meillo@0: entry.set_active(1) meillo@0: self.update() meillo@0: app.play(entry) meillo@0: meillo@0: def command_delete(self): meillo@0: if not self.buffer: return meillo@0: current_entry, n = self.current(), len(self.buffer) meillo@0: self.buffer = self.not_tagged(self.buffer) meillo@0: if n > len(self.buffer): meillo@0: try: self.bufptr = self.buffer.index(current_entry) meillo@0: except ValueError: pass meillo@0: else: meillo@0: current_entry.set_tagged(1) meillo@0: del self.buffer[self.bufptr] meillo@0: if self.random: meillo@0: self.random_prev = self.not_tagged(self.random_prev) meillo@0: self.random_next = self.not_tagged(self.random_next) meillo@0: self.random_left = self.not_tagged(self.random_left) meillo@0: self.update() meillo@0: meillo@0: def command_delete_all(self): meillo@0: self.buffer = [] meillo@0: self.random_prev = [] meillo@0: self.random_next = [] meillo@0: self.random_left = [] meillo@0: app.status(_("Deleted playlist"), 1) meillo@0: self.update() meillo@0: meillo@0: def command_move(self, after): meillo@0: if not self.buffer: return meillo@0: current_entry, l = self.current(), self.get_tagged() meillo@0: if not l or current_entry.is_tagged(): return meillo@0: self.buffer = self.not_tagged(self.buffer) meillo@0: self.bufptr = self.buffer.index(current_entry)+after meillo@0: self.buffer[self.bufptr:self.bufptr] = l meillo@0: self.update() meillo@0: meillo@0: def command_shuffle(self): meillo@0: import random meillo@0: l = [] meillo@0: n = len(self.buffer) meillo@0: while n > 0: meillo@0: n = n-1 meillo@0: r = random.randint(0, n) meillo@0: l.append(self.buffer[r]) meillo@0: del self.buffer[r] meillo@0: self.buffer = l meillo@0: self.bufptr = 0 meillo@0: self.update() meillo@0: app.status(_("Shuffled playlist... Oops?"), 1) meillo@0: meillo@0: def command_sort(self): meillo@0: app.status(_("Working...")) meillo@0: self.buffer.sort(lambda x, y: x.vp() > y.vp() or -1) meillo@0: self.bufptr = 0 meillo@0: self.update() meillo@0: app.status(_("Sorted playlist"), 1) meillo@0: meillo@0: def command_toggle_repeat(self): meillo@0: self.toggle("repeat", _("Repeat: %s")) meillo@0: meillo@0: def command_toggle_random(self): meillo@0: self.toggle("random", _("Random: %s")) meillo@0: self.random_prev = [] meillo@0: self.random_next = [] meillo@0: self.random_left = self.buffer[:] meillo@0: meillo@0: def command_toggle_stop(self): meillo@0: self.toggle("stop", _("Stop playlist: %s")) meillo@0: meillo@0: def toggle(self, attr, format): meillo@0: setattr(self, attr, not getattr(self, attr)) meillo@0: app.status(format % (getattr(self, attr) and _("on") or _("off")), 1) meillo@0: self.parent.update_title() meillo@0: meillo@0: def command_save_playlist(self): meillo@0: if app.restricted: return meillo@0: default = self.pathname or "%s/" % app.win_filelist.cwd meillo@0: app.stop_input_hook = self.stop_save_playlist meillo@0: app.start_input(_("Save playlist"), default) meillo@0: meillo@0: def stop_save_playlist(self): meillo@0: pathname = app.input_string meillo@0: if pathname[0] != '/': meillo@0: pathname = os.path.join(app.win_filelist.cwd, pathname) meillo@0: if not re.search("\.m3u$", pathname, re.I): meillo@0: pathname = "%s%s" % (pathname, ".m3u") meillo@0: try: meillo@0: file = open(pathname, "w") meillo@0: for entry in self.buffer: meillo@0: file.write("%s\n" % entry.pathname) meillo@0: file.close() meillo@0: self.pathname = pathname meillo@0: app.status(_("ok"), 1) meillo@0: except IOError, e: meillo@0: app.status(e, 2) meillo@0: meillo@0: # ------------------------------------------ meillo@0: def get_tag(pathname): meillo@0: if re.compile("^http://").match(pathname) or not os.path.exists(pathname): meillo@0: return pathname meillo@0: tags = {} meillo@0: # FIXME: use magic instead of file extensions to identify OGGs and MP3s meillo@0: if re.compile(".*\.ogg$", re.I).match(pathname): meillo@0: try: meillo@0: vf = ogg.vorbis.VorbisFile(pathname) meillo@0: vc = vf.comment() meillo@0: tags = vc.as_dict() meillo@0: except NameError: pass meillo@0: except (IOError, UnicodeError): return os.path.basename(pathname) meillo@0: elif re.compile(".*\.mp3$", re.I).match(pathname): meillo@0: try: meillo@0: vc = ID3.ID3(pathname, as_tuple=1) meillo@0: tags = vc.as_dict() meillo@0: except NameError: pass meillo@0: except (IOError, ID3.InvalidTagError): return os.path.basename(pathname) meillo@0: else: meillo@0: return os.path.basename(pathname) meillo@0: meillo@0: artist = tags.get("ARTIST", [""])[0] meillo@0: title = tags.get("TITLE", [""])[0] meillo@0: tag = os.path.basename(pathname) meillo@0: try: meillo@0: if artist and title: meillo@0: tag = codecs.latin_1_encode(artist)[0] + " - " + codecs.latin_1_encode(title)[0] meillo@0: elif artist: meillo@0: tag = artist meillo@0: elif title: meillo@0: tag = title meillo@0: return codecs.latin_1_encode(tag)[0] meillo@0: except (NameError, UnicodeError): return tag meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Player: meillo@0: def __init__(self, commandline, files, fps=1): meillo@0: self.commandline = commandline meillo@0: self.re_files = re.compile(files, re.I) meillo@0: self.fps = fps meillo@0: self.stdin_r, self.stdin_w = os.pipe() meillo@0: self.stdout_r, self.stdout_w = os.pipe() meillo@0: self.stderr_r, self.stderr_w = os.pipe() meillo@0: self.entry = None meillo@0: self.stopped = 0 meillo@0: self.paused = 0 meillo@0: self.time_setup = None meillo@0: self.buf = '' meillo@0: self.tid = None meillo@0: meillo@0: def setup(self, entry, offset): meillo@0: self.argv = string.split(self.commandline) meillo@0: self.argv[0] = which(self.argv[0]) meillo@0: for i in range(len(self.argv)): meillo@0: if self.argv[i] == "%s": self.argv[i] = entry.pathname meillo@0: if self.argv[i] == "%d": self.argv[i] = str(offset*self.fps) meillo@0: self.entry = entry meillo@0: if offset == 0: meillo@0: app.progress(0) meillo@0: self.offset = 0 meillo@0: self.length = 0 meillo@0: self.values = [0, 0] meillo@0: self.time_setup = time.time() meillo@0: return self.argv[0] meillo@0: meillo@0: def play(self): meillo@0: self.pid = os.fork() meillo@0: if self.pid == 0: meillo@0: os.dup2(self.stdin_w, sys.stdin.fileno()) meillo@0: os.dup2(self.stdout_w, sys.stdout.fileno()) meillo@0: os.dup2(self.stderr_w, sys.stderr.fileno()) meillo@0: os.setpgrp() meillo@0: try: os.execv(self.argv[0], self.argv) meillo@0: except: os._exit(1) meillo@0: self.stopped = 0 meillo@0: self.paused = 0 meillo@0: self.step = 0 meillo@0: self.update_status() meillo@0: meillo@0: def stop(self, quiet=0): meillo@0: self.paused and self.toggle_pause(quiet) meillo@0: try: meillo@0: while 1: meillo@0: try: os.kill(-self.pid, signal.SIGINT) meillo@0: except os.error: pass meillo@0: os.waitpid(self.pid, os.WNOHANG) meillo@0: except Exception: pass meillo@0: self.stopped = 1 meillo@0: quiet or self.update_status() meillo@0: meillo@0: def toggle_pause(self, quiet=0): meillo@0: try: os.kill(-self.pid, [signal.SIGSTOP, signal.SIGCONT][self.paused]) meillo@0: except os.error: return meillo@0: self.paused = not self.paused meillo@0: quiet or self.update_status() meillo@0: meillo@0: def parse_progress(self): meillo@0: if self.stopped or self.step: self.tid = None meillo@0: else: meillo@0: self.parse_buf() meillo@0: self.tid = app.timeout.add(1.0, self.parse_progress) meillo@0: meillo@0: def read_fd(self, fd): meillo@0: self.buf = os.read(fd, 512) meillo@0: self.tid or self.parse_progress() meillo@0: meillo@0: def poll(self): meillo@0: try: os.waitpid(self.pid, os.WNOHANG) meillo@0: except: meillo@0: # something broken? try again meillo@0: if self.time_setup and (time.time() - self.time_setup) < 2.0: meillo@0: self.play() meillo@0: return 0 meillo@0: app.set_default_status("") meillo@0: app.counter([0,0]) meillo@0: app.progress(0) meillo@0: return 1 meillo@0: meillo@0: def seek(self, offset, relative): meillo@0: if relative: meillo@0: d = offset * self.length * 0.002 meillo@0: self.step = self.step * (self.step * d > 0) + d meillo@0: self.offset = min(self.length, max(0, self.offset+self.step)) meillo@0: else: meillo@0: self.step = 1 meillo@0: self.offset = (offset < 0) and self.length+offset or offset meillo@0: self.show_position() meillo@0: meillo@0: def set_position(self, offset, length, values): meillo@0: self.offset = offset meillo@0: self.length = length meillo@0: self.values = values meillo@0: self.show_position() meillo@0: meillo@0: def show_position(self): meillo@0: app.counter(self.values) meillo@0: app.progress(self.length and (float(self.offset) / self.length)) meillo@0: meillo@0: def update_status(self): meillo@0: if not self.entry: meillo@0: app.set_default_status("") meillo@0: elif self.stopped: meillo@0: app.set_default_status(_("Stopped: %s") % self.entry.vp()) meillo@0: elif self.paused: meillo@0: app.set_default_status(_("Paused: %s") % self.entry.vp()) meillo@0: else: meillo@0: app.set_default_status(_("Playing: %s") % self.entry.vp()) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class FrameOffsetPlayer(Player): meillo@0: re_progress = re.compile("Time.*\s(\d+):(\d+).*\[(\d+):(\d+)") meillo@0: meillo@0: def parse_buf(self): meillo@0: match = self.re_progress.search(self.buf) meillo@0: if match: meillo@0: m1, s1, m2, s2 = map(string.atoi, match.groups()) meillo@0: head, tail = m1*60+s1, m2*60+s2 meillo@0: self.set_position(head, head+tail, [head, tail]) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class TimeOffsetPlayer(Player): meillo@0: re_progress = re.compile("(\d+):(\d+):(\d+)") meillo@0: meillo@0: def parse_buf(self): meillo@0: match = self.re_progress.search(self.buf) meillo@0: if match: meillo@0: h, m, s = map(string.atoi, match.groups()) meillo@0: tail = h*3600+m*60+s meillo@0: head = max(self.length, tail) - tail meillo@0: self.set_position(head, head+tail, [head, tail]) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class NoOffsetPlayer(Player): meillo@0: meillo@0: def parse_buf(self): meillo@0: head = self.offset+1 meillo@0: self.set_position(head, 0, [head, head]) meillo@0: meillo@0: def seek(self, *dummy): meillo@0: return 1 meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Timeout: meillo@0: def __init__(self): meillo@0: self.next = 0 meillo@0: self.dict = {} meillo@0: meillo@0: def add(self, timeout, func, args=()): meillo@0: tid = self.next = self.next + 1 meillo@0: self.dict[tid] = (func, args, time.time() + timeout) meillo@0: return tid meillo@0: meillo@0: def remove(self, tid): meillo@0: del self.dict[tid] meillo@0: meillo@0: def check(self, now): meillo@0: for tid, (func, args, timeout) in self.dict.items(): meillo@0: if now >= timeout: meillo@0: self.remove(tid) meillo@0: apply(func, args) meillo@0: return len(self.dict) and 0.2 or None meillo@0: meillo@0: # ------------------------------------------ meillo@0: class FIFOControl: meillo@0: def __init__(self): meillo@0: try: self.fd = open(CONTROL_FIFO, "rb+", 0) meillo@0: except: self.fd = None meillo@0: self.commands = {"pause" : app.toggle_pause, meillo@0: "next" : app.next_song, meillo@0: "prev" : app.prev_song, meillo@0: "forward" : self.forward, meillo@0: "backward" : self.backward, meillo@0: "play" : app.toggle_stop, meillo@0: "stop" : app.toggle_stop, meillo@0: "volup" : app.inc_volume, meillo@0: "voldown" : app.dec_volume, meillo@0: "quit" : app.quit} meillo@0: meillo@0: def handle_command(self): meillo@0: command = string.strip(self.fd.readline()) meillo@0: if command in self.commands.keys(): meillo@0: self.commands[command]() meillo@0: meillo@0: def forward(self): meillo@0: app.seek(1, 1) meillo@0: meillo@0: def backward(self): meillo@0: app.seek(-1, 1) meillo@0: meillo@0: # ------------------------------------------ meillo@0: class Application: meillo@0: def __init__(self): meillo@0: self.keymapstack = KeymapStack() meillo@0: self.input_mode = 0 meillo@0: self.input_prompt = "" meillo@0: self.input_string = "" meillo@0: self.do_input_hook = None meillo@0: self.stop_input_hook = None meillo@0: self.complete_input_hook = None meillo@0: self.channels = [] meillo@0: self.restricted = 0 meillo@0: self.input_keymap = Keymap() meillo@0: self.input_keymap.bind(list(Window.chars), self.do_input) meillo@0: self.input_keymap.bind(curses.KEY_BACKSPACE, self.do_input, (8,)) meillo@0: self.input_keymap.bind([21, 23], self.do_input) meillo@0: self.input_keymap.bind(['\a', 27], self.cancel_input, ()) meillo@0: self.input_keymap.bind(['\n', curses.KEY_ENTER], meillo@0: self.stop_input, ()) meillo@0: meillo@0: def setup(self): meillo@0: if tty: meillo@0: self.tcattr = tty.tcgetattr(sys.stdin.fileno()) meillo@0: tcattr = tty.tcgetattr(sys.stdin.fileno()) meillo@0: tcattr[0] = tcattr[0] & ~(tty.IXON) meillo@0: tty.tcsetattr(sys.stdin.fileno(), tty.TCSANOW, tcattr) meillo@0: self.w = curses.initscr() meillo@0: curses.cbreak() meillo@0: curses.noecho() meillo@0: try: curses.meta(1) meillo@0: except: pass meillo@0: self.cursor(0) meillo@0: signal.signal(signal.SIGCHLD, signal.SIG_IGN) meillo@0: signal.signal(signal.SIGHUP, self.handler_quit) meillo@0: signal.signal(signal.SIGINT, self.handler_quit) meillo@0: signal.signal(signal.SIGTERM, self.handler_quit) meillo@0: signal.signal(signal.SIGWINCH, self.handler_resize) meillo@0: self.win_root = RootWindow(None) meillo@0: self.win_root.update() meillo@0: self.win_tab = self.win_root.win_tab meillo@0: self.win_filelist = self.win_root.win_tab.win_filelist meillo@0: self.win_playlist = self.win_root.win_tab.win_playlist meillo@0: self.win_status = self.win_root.win_status meillo@0: self.status = self.win_status.status meillo@0: self.set_default_status = self.win_status.set_default_status meillo@0: self.restore_default_status = self.win_status.restore_default_status meillo@0: self.counter = self.win_root.win_counter.counter meillo@0: self.progress = self.win_root.win_progress.progress meillo@0: self.player = PLAYERS[0] meillo@0: self.timeout = Timeout() meillo@0: self.play_tid = None meillo@0: self.kludge = 0 meillo@0: self.win_filelist.listdir() meillo@0: self.control = FIFOControl() meillo@0: meillo@0: def cleanup(self): meillo@0: try: curses.endwin() meillo@0: except curses.error: return meillo@0: XTERM and sys.stderr.write("\033]0;%s\a" % "xterm") meillo@0: tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr) meillo@0: print meillo@0: meillo@0: def run(self): meillo@0: while 1: meillo@0: now = time.time() meillo@0: timeout = self.timeout.check(now) meillo@0: self.win_filelist.listdir_maybe(now) meillo@0: if not self.player.stopped: meillo@0: timeout = 0.5 meillo@0: if self.kludge and self.player.poll(): meillo@0: self.player.stopped = 1 # end of playlist hack meillo@0: if not self.win_playlist.stop: meillo@0: entry = self.win_playlist.change_active_entry(1) meillo@0: entry and self.play(entry) meillo@0: R = [sys.stdin, self.player.stdout_r, self.player.stderr_r] meillo@0: self.control.fd and R.append(self.control.fd) meillo@0: try: r, w, e = select.select(R, [], [], timeout) meillo@0: except select.error: continue meillo@0: self.kludge = 1 meillo@0: # user meillo@0: if sys.stdin in r: meillo@0: c = self.win_root.getch() meillo@0: self.keymapstack.process(c) meillo@0: # player meillo@0: if self.player.stderr_r in r: meillo@0: self.player.read_fd(self.player.stderr_r) meillo@0: # player meillo@0: if self.player.stdout_r in r: meillo@0: self.player.read_fd(self.player.stdout_r) meillo@0: # remote meillo@0: if self.control.fd in r: meillo@0: self.control.handle_command() meillo@0: meillo@0: def play(self, entry, offset = 0): meillo@0: self.kludge = 0 meillo@0: self.play_tid = None meillo@0: if entry is None or offset is None: return meillo@0: self.player.stop(quiet=1) meillo@0: for self.player in PLAYERS: meillo@0: if self.player.re_files.search(entry.pathname): meillo@0: if self.player.setup(entry, offset): break meillo@0: else: meillo@0: app.status(_("Player not found!"), 1) meillo@0: self.player.stopped = 0 # keep going meillo@0: return meillo@0: self.player.play() meillo@0: meillo@0: def delayed_play(self, entry, offset): meillo@0: if self.play_tid: self.timeout.remove(self.play_tid) meillo@0: self.play_tid = self.timeout.add(0.5, self.play, (entry, offset)) meillo@0: meillo@0: def next_song(self): meillo@0: self.delayed_play(self.win_playlist.change_active_entry(1), 0) meillo@0: meillo@0: def prev_song(self): meillo@0: self.delayed_play(self.win_playlist.change_active_entry(-1), 0) meillo@0: meillo@0: def seek(self, offset, relative): meillo@0: if not self.player.entry: return meillo@0: self.player.seek(offset, relative) meillo@0: self.delayed_play(self.player.entry, self.player.offset) meillo@0: meillo@0: def toggle_pause(self): meillo@0: if not self.player.entry: return meillo@0: if not self.player.stopped: self.player.toggle_pause() meillo@0: meillo@0: def toggle_stop(self): meillo@0: if not self.player.entry: return meillo@0: if not self.player.stopped: self.player.stop() meillo@0: else: self.play(self.player.entry, self.player.offset) meillo@0: meillo@0: def inc_volume(self): meillo@0: self.mixer("cue", 1) meillo@0: meillo@0: def dec_volume(self): meillo@0: self.mixer("cue", -1) meillo@0: meillo@0: def key_volume(self, ch): meillo@0: self.mixer("set", (ch & 0x0f)*10) meillo@0: meillo@0: def mixer(self, cmd=None, arg=None): meillo@0: try: self._mixer(cmd, arg) meillo@0: except Exception, e: app.status(e, 2) meillo@0: meillo@0: def _mixer(self, cmd, arg): meillo@0: try: meillo@0: import ossaudiodev meillo@0: mixer = ossaudiodev.openmixer() meillo@0: get, set = mixer.get, mixer.set meillo@0: self.channels = self.channels or \ meillo@0: [['MASTER', ossaudiodev.SOUND_MIXER_VOLUME], meillo@0: ['PCM', ossaudiodev.SOUND_MIXER_PCM]] meillo@0: except ImportError: meillo@0: import oss meillo@0: mixer = oss.open_mixer() meillo@0: get, set = mixer.read_channel, mixer.write_channel meillo@0: self.channels = self.channels or \ meillo@0: [['MASTER', oss.SOUND_MIXER_VOLUME], meillo@0: ['PCM', oss.SOUND_MIXER_PCM]] meillo@0: if cmd is "toggle": self.channels.insert(0, self.channels.pop()) meillo@0: name, channel = self.channels[0] meillo@0: if cmd is "cue": arg = min(100, max(0, get(channel)[0] + arg)) meillo@0: if cmd in ["set", "cue"]: set(channel, (arg, arg)) meillo@0: app.status(_("%s volume %s%%") % (name, get(channel)[0]), 1) meillo@0: mixer.close() meillo@0: meillo@0: def show_input(self): meillo@0: n = len(self.input_prompt)+1 meillo@0: s = cut(self.input_string, self.win_status.cols-n, left=1) meillo@0: app.status("%s%s " % (self.input_prompt, s)) meillo@0: meillo@0: def start_input(self, prompt="", data="", colon=1): meillo@0: self.input_mode = 1 meillo@0: self.cursor(1) meillo@0: app.keymapstack.push(self.input_keymap) meillo@0: self.input_prompt = prompt + (colon and ": " or "") meillo@0: self.input_string = data meillo@0: self.show_input() meillo@0: meillo@0: def do_input(self, *args): meillo@0: if self.do_input_hook: meillo@0: return apply(self.do_input_hook, args) meillo@0: ch = args and args[0] or None meillo@0: if ch in [8, 127]: # backspace meillo@0: self.input_string = self.input_string[:-1] meillo@0: elif ch == 9 and self.complete_input_hook: meillo@0: self.input_string = self.complete_input_hook(self.input_string) meillo@0: elif ch == 21: # C-u meillo@0: self.input_string = "" meillo@0: elif ch == 23: # C-w meillo@0: self.input_string = re.sub("((.* )?)\w.*", "\\1", self.input_string) meillo@0: elif ch: meillo@0: self.input_string = "%s%c" % (self.input_string, ch) meillo@0: self.show_input() meillo@0: meillo@0: def stop_input(self, *args): meillo@0: self.input_mode = 0 meillo@0: self.cursor(0) meillo@0: app.keymapstack.pop() meillo@0: if not self.input_string: meillo@0: app.status(_("cancel"), 1) meillo@0: elif self.stop_input_hook: meillo@0: apply(self.stop_input_hook, args) meillo@0: self.do_input_hook = None meillo@0: self.stop_input_hook = None meillo@0: self.complete_input_hook = None meillo@0: meillo@0: def cancel_input(self): meillo@0: self.input_string = "" meillo@0: self.stop_input() meillo@0: meillo@0: def cursor(self, visibility): meillo@0: try: curses.curs_set(visibility) meillo@0: except: pass meillo@0: meillo@0: def quit(self): meillo@0: self.player.stop(quiet=1) meillo@0: sys.exit(0) meillo@0: meillo@0: def handler_resize(self, sig, frame): meillo@0: # curses trickery meillo@0: while 1: meillo@0: try: curses.endwin(); break meillo@0: except: time.sleep(1) meillo@0: self.w.refresh() meillo@0: self.win_root.resize() meillo@0: self.win_root.update() meillo@0: meillo@0: def handler_quit(self, sig, frame): meillo@0: self.quit() meillo@0: meillo@0: # ------------------------------------------ meillo@0: def main(): meillo@0: try: meillo@0: opts, args = getopt.getopt(sys.argv[1:], "nrRv") meillo@0: except: meillo@0: usage = _("Usage: %s [-nrRv] [ file | dir | playlist ] ...\n") meillo@0: sys.stderr.write(usage % sys.argv[0]) meillo@0: sys.exit(1) meillo@0: meillo@0: global app meillo@0: app = Application() meillo@0: meillo@0: playlist = [] meillo@0: if not sys.stdin.isatty(): meillo@0: playlist = map(string.strip, sys.stdin.readlines()) meillo@0: os.close(0) meillo@0: os.open("/dev/tty", 0) meillo@0: try: meillo@0: app.setup() meillo@0: for opt, optarg in opts: meillo@0: if opt == "-n": app.restricted = 1 meillo@0: if opt == "-r": app.win_playlist.command_toggle_repeat() meillo@0: if opt == "-R": app.win_playlist.command_toggle_random() meillo@0: if opt == "-v": app.mixer("toggle") meillo@0: if args or playlist: meillo@0: for i in args or playlist: meillo@0: app.win_playlist.add(os.path.abspath(i)) meillo@0: app.win_tab.change_window() meillo@0: app.run() meillo@0: except SystemExit: meillo@0: app.cleanup() meillo@0: except Exception: meillo@0: app.cleanup() meillo@0: import traceback meillo@0: traceback.print_exc() meillo@0: meillo@0: # ------------------------------------------ meillo@0: PLAYERS = [ meillo@0: FrameOffsetPlayer("ogg123 -q -v -k %d %s", "\.ogg$"), meillo@0: FrameOffsetPlayer("splay -f -k %d %s", "(^http://|\.mp[123]$)", 38.28), meillo@0: FrameOffsetPlayer("mpg123 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28), meillo@0: FrameOffsetPlayer("mpg321 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28), meillo@0: TimeOffsetPlayer("madplay -v --display-time=remaining -s %d %s", "\.mp[123]$"), meillo@0: NoOffsetPlayer("mikmod -q -p0 %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm)$"), meillo@0: NoOffsetPlayer("xmp -q %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm|stm)$"), meillo@0: NoOffsetPlayer("play %s", "\.(aiff|au|cdr|mp3|ogg|wav)$"), meillo@0: NoOffsetPlayer("speexdec %s", "\.spx$") meillo@0: ] meillo@0: meillo@0: def VALID_SONG(name): meillo@0: for player in PLAYERS: meillo@0: if player.re_files.search(name): meillo@0: return 1 meillo@0: meillo@0: def VALID_PLAYLIST(name): meillo@0: if re.search("\.(m3u|pls)$", name, re.I): meillo@0: return 1 meillo@0: meillo@0: for rc in [os.path.expanduser("~/.cplayrc"), "/etc/cplayrc"]: meillo@0: try: execfile(rc); break meillo@0: except IOError: pass meillo@0: meillo@0: # ------------------------------------------ meillo@0: if __name__ == "__main__": main()