summaryrefslogtreecommitdiff
path: root/play.py
diff options
context:
space:
mode:
authorAdrian C. (anrxc) <anrxc@sysphere.org>2010-10-20 04:52:43 +0200
committerAdrian C. (anrxc) <anrxc@sysphere.org>2010-10-20 04:52:43 +0200
commit076eefa784697e496914ed662aeac0b458721150 (patch)
tree0dd78427a16723c9a6f9921114c7b5712d4f6887 /play.py
parent1d9768214922915e21b3304587d8f4b41ce22faa (diff)
downloadplay-076eefa784697e496914ed662aeac0b458721150.tar.xz
play: added missing .py extension
Diffstat (limited to 'play.py')
-rwxr-xr-xplay.py1711
1 files changed, 1711 insertions, 0 deletions
diff --git a/play.py b/play.py
new file mode 100755
index 0000000..9f6a376
--- /dev/null
+++ b/play.py
@@ -0,0 +1,1711 @@
+#!/usr/bin/env python2
+# -*- mode:python; coding:utf-8 -*-
+
+__version__ = "play 1.00"
+
+"""
+play - A curses front-end for various audio players based on cplay
+Copyright (C) 1998-2005 Ulf Betlehem <flu@iki.fi>
+Copyright (C) 2008-2010 Adrian C. <anrxc@sysphere.org>
+
+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
+
+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
+
+# ------------------------------------------
+def _(s): return s # Removed gettext support
+
+# ------------------------------------------
+XTERM = re.search("rxvt|xterm", os.environ["TERM"])
+CONTROL_FIFO = "%s/play-control-%s" % (os.environ.get("TMPDIR", "/tmp"), os.environ["USER"])
+
+# ------------------------------------------
+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)
+
+# ------------------------------------------
+def cut(s, n, left=0):
+ if left: return len(s) > n and "<%s" % s[-n+1:] or s
+ else: return len(s) > n and "%s>" % s[:n-1] or s
+
+# ------------------------------------------
+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: args = (key,)
+ method(*args)
+ return 1
+
+# ------------------------------------------
+class Window:
+ chars = string.letters+string.digits+string.punctuation+string.whitespace
+
+ 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 insstr(self, s):
+ if not s: return
+ self.w.addstr(s[:-1])
+ self.w.hline(ord(s[-1]), 1) # insch() work-around
+
+ 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):
+ self.w = self.newwin()
+ self.ypos, self.xpos = self.getbegyx()
+ self.rows, self.cols = self.getmaxyx()
+ self.keypad(1)
+ self.leaveok(0)
+ self.scrollok(0)
+ for child in self.children:
+ child.resize()
+
+ def update(self):
+ self.clear()
+ self.refresh()
+ for child in self.children:
+ child.update()
+
+# ------------------------------------------
+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
+ x and self.hline(ord('='), x)
+ self.move(0, x)
+ # Color of the progress indicator
+ self.attron(curses.color_pair(1))
+ self.insstr('O')
+ self.attroff(curses.color_pair(1))
+ self.touchwin()
+ self.refresh()
+
+ def progress(self, value):
+ 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.tid = None
+
+ def newwin(self):
+ return curses.newwin(1, self.parent.cols-12, self.parent.rows-1, 0)
+
+ def update(self):
+ msg = self.current_message
+ self.move(0, 0)
+ self.clrtoeol()
+ # Color of statusbar messages
+ self.attron(curses.color_pair(3))
+ self.insstr(cut(msg, self.cols))
+ self.attroff(curses.color_pair(3))
+ self.touchwin()
+ self.refresh()
+
+ def status(self, message, duration = 0):
+ self.current_message = str(message)
+ if self.tid: app.timeout.remove(self.tid)
+ if duration: self.tid = app.timeout.add(duration, self.timeout)
+ else: self.tid = None
+ self.update()
+
+ def timeout(self):
+ self.tid = None
+ self.restore_default_status()
+
+ def set_default_status(self, message):
+ if self.current_message == self.default_message: self.status(message)
+ self.default_message = message
+ XTERM and sys.stderr.write("\033]0;%s\a" % (message or "play"))
+
+ 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, 11, self.parent.rows-1, self.parent.cols-11)
+
+ def update(self):
+ h, s = divmod(self.values[self.mode], 3600)
+ m, s = divmod(s, 60)
+ self.move(0, 0)
+ # Color of the statusbar counter
+ self.attron(curses.color_pair(1))
+ self.insstr("[%02d:%02d:%02d]" % (h, m, s))
+ self.attroff(curses.color_pair(1))
+ self.touchwin()
+ self.refresh()
+
+ def counter(self, values):
+ self.values = values
+ self.update()
+
+ def toggle_mode(self):
+ self.mode = not self.mode
+ tmp = [_("elapsed"), _("remaining")][self.mode]
+ app.status(_("Counting %s time") % tmp, 1)
+ self.update()
+
+# ------------------------------------------
+class RootWindow(Window):
+ def __init__(self, parent):
+ Window.__init__(self, parent)
+ keymap = Keymap()
+ app.keymapstack.push(keymap)
+ self.win_progress = ProgressWindow(self)
+ self.win_status = StatusWindow(self)
+ self.win_counter = CounterWindow(self)
+ self.win_tab = TabWindow(self)
+ keymap.bind(12, self.update, ()) # C-l
+ keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1, 1)) # C-b
+ keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1, 1)) # C-f
+ keymap.bind([1, '^'], app.seek, (0, 0)) # C-a
+ keymap.bind([5, '$'], app.seek, (-1, 0)) # C-e
+ keymap.bind(range(48,58), app.key_volume) # 0123456789
+ keymap.bind(['+', '='], app.mixer, ("cue", 1))
+ keymap.bind('-', app.mixer, ("cue", -1))
+ keymap.bind('n', app.next_song, ())
+ keymap.bind('p', app.prev_song, ())
+ keymap.bind('z', app.toggle_pause, ())
+ keymap.bind('x', app.toggle_stop, ())
+ keymap.bind('c', self.win_counter.toggle_mode, ())
+ keymap.bind('Q', app.quit, ())
+ keymap.bind('q', self.command_quit, ())
+ keymap.bind('v', app.mixer, ("toggle",))
+ keymap.bind(',', app.command_macro, ())
+
+ def command_quit(self):
+ app.do_input_hook = self.do_quit
+ app.start_input(_("Quit? (y/N)"))
+
+ def do_quit(self, ch):
+ if chr(ch) == 'y': app.quit()
+ app.stop_input()
+
+# ------------------------------------------
+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)
+
+ keymap = Keymap()
+ keymap.bind('\t', self.change_window, ()) # tab
+ keymap.bind('h', self.help, ())
+ app.keymapstack.push(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):
+ child = self.children[self.active_child]
+ self.move(0, 0)
+ self.clrtoeol()
+ # Color of the window titlebar text
+ self.attron(curses.color_pair(2))
+ self.insstr(child.get_title())
+ self.attroff(curses.color_pair(2))
+ 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.last_search = ""
+ self.hoffset = 0
+ 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.keymap.bind(['>'], self.hscroll, (8,))
+ self.keymap.bind(['<'], self.hscroll, (-8,))
+
+ 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))
+ first, last = self.scrptr, self.scrptr + self.rows - 1
+ if (self.bufptr < first): first = self.bufptr
+ if (self.bufptr > last): first = self.bufptr - self.rows + 1
+ if force or self.scrptr != first:
+ self.scrptr = first
+ self.move(0, 0)
+ self.clrtobot()
+ i = 0
+ for entry in self.buffer[first:first+self.rows]:
+ self.move(i, 0)
+ i = i + 1
+ self.putstr(entry)
+ if self.visible:
+ self.refresh()
+ self.parent.update_title()
+ # Color of a selected list item
+ self.update_line(curses.color_pair(4))
+
+ 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)
+ self.putstr(self.current())
+ if attr: self.attroff(attr)
+ if self.visible and refresh: self.refresh()
+
+ def get_title(self, data=""):
+ pos = "%s-%s/%s" % (self.scrptr+min(1, len(self.buffer)),
+ min(self.scrptr+self.rows, len(self.buffer)),
+ len(self.buffer))
+ width = self.cols-len(pos)-2
+ data = cut(data, width-len(self.name), 1)
+ return "%-*s %s" % (width, cut(self.name+data, width), pos)
+
+ def putstr(self, entry, *pos):
+ s = str(entry)
+ pos and self.move(*pos)
+ if self.hoffset: s = "<%s" % s[self.hoffset+1:]
+ self.insstr(cut(s, self.cols))
+
+ def current(self):
+ if len(self.buffer) == 0: return None
+ if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1
+ return self.buffer[self.bufptr]
+
+ def cursor_move(self, ydiff):
+ if app.input_mode: app.cancel_input()
+ 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):
+ self.bufptr = self.scrptr - 1
+ if self.bufptr < 0: self.bufptr = len(self.buffer) - 1
+ self.scrptr = max(0, self.bufptr - self.rows)
+ self.update()
+
+ def cursor_npage(self):
+ self.bufptr = self.scrptr + self.rows
+ if self.bufptr > len(self.buffer) - 1: self.bufptr = 0
+ self.scrptr = self.bufptr
+ self.update()
+
+ def cursor_home(self): self.cursor_move(-self.bufptr)
+
+ def cursor_end(self): self.cursor_move(-self.bufptr - 1)
+
+ def start_search(self, type, direction):
+ self.search_direction = direction
+ self.not_found = 0
+ if app.input_mode:
+ app.input_prompt = "%s: " % type
+ self.do_search(advance = direction)
+ else:
+ app.do_input_hook = self.do_search
+ app.stop_input_hook = self.stop_search
+ app.start_input(type)
+
+ def stop_search(self):
+ self.last_search = app.input_string
+ app.status(_("ok"), 1)
+
+ def do_search(self, ch = None, advance = 0):
+ if ch in [8, 127]: app.input_string = app.input_string[:-1]
+ elif ch: app.input_string = "%s%c" % (app.input_string, ch)
+ else: app.input_string = app.input_string or self.last_search
+ index = self.bufptr + advance
+ while 1:
+ if not 0 <= index < len(self.buffer):
+ app.status(_("Not found: %s ") % app.input_string)
+ self.not_found = 1
+ break
+ line = string.lower(str(self.buffer[index]))
+ if string.find(line, string.lower(app.input_string)) != -1:
+ app.show_input()
+ self.update_line(refresh = 0)
+ self.bufptr = index
+ self.update(force = 0)
+ self.not_found = 0
+ break
+ if self.not_found:
+ app.status(_("Not found: %s ") % app.input_string)
+ break
+ index = index + self.search_direction
+
+ def hscroll(self, value):
+ self.hoffset = max(0, self.hoffset + value)
+ self.update()
+
+# ------------------------------------------
+class HelpWindow(ListWindow):
+ def __init__(self, parent):
+ ListWindow.__init__(self, parent)
+ self.name = _("Help")
+ self.keymap.bind('q', self.parent.help, ())
+ self.buffer = string.split(_("""\
+ Global t, T : tag current/regex
+ ------ u, U : untag current/regex
+ Up, Down, k, j, C-p, C-n, Sp, i : invert current/all
+ PgUp, PgDn, K, J, !, , : shell, macro
+ Home, End, g, G : movement
+ Enter : chdir or play Filelist
+ Tab : filelist/playlist --------
+ n, p : next/prev track a : add (tagged) to playlist
+ z, x : toggle pause/stop s : recursive search
+ BS, o : goto parent/specified dir
+ Left, Right, m, ' : set/get bookmark
+ C-f, C-b : seek forward/backward
+ C-a, C-e : restart/end track Playlist
+ C-s, C-r, / : isearch --------
+ C-g, Esc : cancel d, D : delete (tagged) tracks/playlist
+ 1..9, +, - : volume control m, M : move tagged tracks after/before
+ c, v : counter/volume mode r, R : toggle repeat/Random mode
+ <, > : horizontal scrolling s, S : shuffle/Sort playlist
+ C-l, l : refresh, list mode w, @ : write playlist, jump to active
+ h, q, Q : help, quit?, Quit! X : stop playlist after each track
+"""), "\n")
+
+# ------------------------------------------
+class ListEntry:
+ def __init__(self, pathname, dir=0):
+ self.filename = os.path.basename(pathname)
+ self.pathname = pathname
+ self.slash = dir and "/" or ""
+ self.tagged = 0
+
+ def set_tagged(self, value):
+ self.tagged = value
+
+ def is_tagged(self):
+ return self.tagged == 1
+
+ def __str__(self):
+ mark = self.is_tagged() and "*" or " "
+ return "%s %s%s" % (mark, self.vp(), self.slash)
+
+ def vp(self):
+ return self.vps[0][1](self)
+
+ def vp_filename(self):
+ return self.filename or self.pathname
+
+ def vp_pathname(self):
+ return self.pathname
+
+ vps = [[_("filename"), vp_filename],
+ [_("pathname"), vp_pathname]]
+
+# ------------------------------------------
+class PlaylistEntry(ListEntry):
+ def __init__(self, pathname):
+ ListEntry.__init__(self, pathname)
+ self.metadata = None
+ self.active = 0
+
+ def set_active(self, value):
+ self.active = value
+
+ def is_active(self):
+ return self.active == 1
+
+ def vp_metadata(self):
+ return self.metadata or self.read_metadata()
+
+ def read_metadata(self):
+ self.metadata = get_tag(self.pathname)
+ return self.metadata
+
+ vps = ListEntry.vps[:] + [[_("metadata"), vp_metadata]]
+
+# ------------------------------------------
+class TagListWindow(ListWindow):
+ def __init__(self, parent):
+ ListWindow.__init__(self, parent)
+ self.keymap.bind(' ', self.command_tag_untag, ())
+ self.keymap.bind('i', self.command_invert_tags, ())
+ self.keymap.bind('t', self.command_tag, (1,))
+ self.keymap.bind('u', self.command_tag, (0,))
+ self.keymap.bind('T', self.command_tag_regexp, (1,))
+ self.keymap.bind('U', self.command_tag_regexp, (0,))
+ self.keymap.bind('l', self.command_change_viewpoint, ())
+ self.keymap.bind('!', self.command_shell, ())
+
+ def command_shell(self):
+ if app.restricted: return
+ app.stop_input_hook = self.stop_shell
+ app.complete_input_hook = self.complete_shell
+ app.start_input(_("shell$ "), colon=0)
+
+ def stop_shell(self):
+ s = app.input_string
+ curses.endwin()
+ sys.stderr.write("\n")
+ argv = map(lambda x: x.pathname, self.get_tagged())
+ argv or self.current() and argv.append(self.current().pathname)
+ argv = ["/bin/sh", "-c", s, "--"] + argv
+ pid = os.fork()
+ if pid == 0:
+ try: os.execv(argv[0], argv)
+ except: os._exit(1)
+ try:
+ try: pid, r = os.waitpid(pid, os.WNOHANG)
+ except os.error: pass
+ sys.stderr.write("\nshell returned %s, press return!\n" % r)
+ except: pass
+ sys.stdin.readline()
+ app.win_root.update()
+ app.restore_default_status()
+ app.cursor(0)
+
+ def complete_shell(self, line):
+ return self.complete_generic(line, quote=1)
+
+ def complete_generic(self, line, quote=0):
+ import glob
+ if quote:
+ s = re.sub('.*[^\\\\][ \'"()\[\]{}$`]', '', line)
+ s, part = re.sub('\\\\', '', s), line[:len(line)-len(s)]
+ else:
+ s, part = line, ""
+ results = glob.glob(os.path.expanduser(s)+"*")
+ if len(results) == 0:
+ return line
+ if len(results) == 1:
+ lm = results[0]
+ lm = lm + (os.path.isdir(lm) and "/" or "")
+ else:
+ lm = results[0]
+ for result in results:
+ for i in range(min(len(result), len(lm))):
+ if result[i] != lm[i]:
+ lm = lm[:i]
+ break
+ if quote: lm = re.sub('([ \'"()\[\]{}$`])', '\\\\\\1', lm)
+ return part + lm
+
+ def command_change_viewpoint(self, klass=ListEntry):
+ klass.vps.append(klass.vps.pop(0))
+ app.status(_("Listing %s") % klass.vps[0][0], 1)
+ app.player.update_status()
+ self.update()
+
+ def command_invert_tags(self):
+ for i in self.buffer:
+ i.set_tagged(not i.is_tagged())
+ self.update()
+
+ def command_tag_untag(self):
+ if not self.buffer: return
+ tmp = self.buffer[self.bufptr]
+ tmp.set_tagged(not tmp.is_tagged())
+ self.cursor_move(1)
+
+ def command_tag(self, value):
+ if not self.buffer: return
+ self.buffer[self.bufptr].set_tagged(value)
+ self.cursor_move(1)
+
+ def command_tag_regexp(self, value):
+ self.tag_value = value
+ app.stop_input_hook = self.stop_tag_regexp
+ app.start_input(value and _("Tag regexp") or _("Untag regexp"))
+
+ def stop_tag_regexp(self):
+ try:
+ r = re.compile(app.input_string, re.I)
+ for entry in self.buffer:
+ if r.search(str(entry)):
+ entry.set_tagged(self.tag_value)
+ self.update()
+ app.status(_("ok"), 1)
+ except re.error, e:
+ app.status(e, 2)
+
+ def get_tagged(self):
+ return filter(lambda x: x.is_tagged(), self.buffer)
+
+ def not_tagged(self, l):
+ return filter(lambda x: not x.is_tagged(), l)
+
+# ------------------------------------------
+class FilelistWindow(TagListWindow):
+ def __init__(self, parent):
+ TagListWindow.__init__(self, parent)
+ self.oldposition = {}
+ try: self.chdir(os.getcwd())
+ except OSError: self.chdir(os.environ['HOME'])
+ self.startdir = self.cwd
+ self.mtime_when = 0
+ self.mtime = None
+ self.keymap.bind(['\n', curses.KEY_ENTER],
+ self.command_chdir_or_play, ())
+ self.keymap.bind(['.', 127, curses.KEY_BACKSPACE],
+ self.command_chparentdir, ())
+ self.keymap.bind('a', self.command_add_recursively, ())
+ self.keymap.bind('o', self.command_goto, ())
+ self.keymap.bind('s', self.command_search_recursively, ())
+ self.keymap.bind('m', self.command_set_bookmark, ())
+ self.keymap.bind("'", self.command_get_bookmark, ())
+ self.bookmarks = { 39: [self.cwd, 0] }
+
+ def command_get_bookmark(self):
+ app.do_input_hook = self.do_get_bookmark
+ app.start_input(_("bookmark"))
+
+ def do_get_bookmark(self, ch):
+ app.input_string = ch
+ bookmark = self.bookmarks.get(ch)
+ if bookmark:
+ self.bookmarks[39] = [self.cwd, self.bufptr]
+ dir, pos = bookmark
+ self.chdir(dir)
+ self.listdir()
+ self.bufptr = pos
+ self.update()
+ app.status(_("ok"), 1)
+ else:
+ app.status(_("Not found!"), 1)
+ app.stop_input()
+
+ def command_set_bookmark(self):
+ app.do_input_hook = self.do_set_bookmark
+ app.start_input(_("set bookmark"))
+
+ def do_set_bookmark(self, ch):
+ app.input_string = ch
+ self.bookmarks[ch] = [self.cwd, self.bufptr]
+ ch and app.status(_("ok"), 1) or app.stop_input()
+
+ def command_search_recursively(self):
+ app.stop_input_hook = self.stop_search_recursively
+ app.start_input(_("search"))
+
+ def stop_search_recursively(self):
+ try: re_tmp = re.compile(app.input_string, re.I)
+ except re.error, e:
+ app.status(e, 2)
+ return
+ app.status(_("Searching..."))
+ results = []
+ for entry in self.buffer:
+ if entry.filename == "..":
+ continue
+ if re_tmp.search(entry.filename):
+ results.append(entry)
+ elif os.path.isdir(entry.pathname):
+ try: self.search_recursively(re_tmp, entry.pathname, results)
+ except: pass
+ if not self.search_mode:
+ self.chdir(os.path.join(self.cwd,_("search results")))
+ self.search_mode = 1
+ self.buffer = results
+ self.bufptr = 0
+ self.parent.update_title()
+ self.update()
+ app.restore_default_status()
+
+ def search_recursively(self, re_tmp, dir, results):
+ for filename in os.listdir(dir):
+ pathname = os.path.join(dir, filename)
+ if re_tmp.search(filename):
+ if os.path.isdir(pathname):
+ results.append(ListEntry(pathname, 1))
+ elif VALID_PLAYLIST(filename) or VALID_SONG(filename):
+ results.append(ListEntry(pathname))
+ elif os.path.isdir(pathname):
+ self.search_recursively(re_tmp, pathname, results)
+
+ def get_title(self):
+ self.name = _("Filelist: ")
+ return ListWindow.get_title(self, re.sub("/?$", "/", self.cwd))
+
+ def listdir_maybe(self, now=0):
+ if now < self.mtime_when+2: return
+ self.mtime_when = now
+ self.oldposition[self.cwd] = self.bufptr
+ try: self.mtime == os.stat(self.cwd)[8] or self.listdir(quiet=1)
+ except os.error: pass
+
+ def listdir(self, quiet=0, prevdir=None):
+ quiet or app.status(_("Reading directory..."))
+ self.search_mode = 0
+ dirs = []
+ files = []
+ try:
+ self.mtime = os.stat(self.cwd)[8]
+ self.mtime_when = time.time()
+ filenames = os.listdir(self.cwd)
+ filenames.sort()
+ for filename in filenames:
+ if filename[0] == ".": continue
+ pathname = os.path.join(self.cwd, filename)
+ if os.path.isdir(pathname): dirs.append(pathname)
+ elif VALID_SONG(filename): files.append(pathname)
+ elif VALID_PLAYLIST(filename): files.append(pathname)
+ except os.error: pass
+ dots = ListEntry(os.path.join(self.cwd, ".."), 1)
+ self.buffer = [[dots], []][self.cwd == "/"]
+ for i in dirs: self.buffer.append(ListEntry(i, 1))
+ for i in files: self.buffer.append(ListEntry(i))
+ if prevdir:
+ for self.bufptr in range(len(self.buffer)):
+ if self.buffer[self.bufptr].filename == prevdir: break
+ else: self.bufptr = 0
+ elif self.oldposition.has_key(self.cwd):
+ self.bufptr = self.oldposition[self.cwd]
+ else: self.bufptr = 0
+ self.parent.update_title()
+ self.update()
+ quiet or app.restore_default_status()
+
+ def chdir(self, dir):
+ if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr
+ self.cwd = os.path.normpath(dir)
+ try: os.chdir(self.cwd)
+ except: pass
+
+ def command_chdir_or_play(self):
+ if not self.buffer: return
+ if self.current().filename == "..":
+ self.command_chparentdir()
+ elif os.path.isdir(self.current().pathname):
+ self.chdir(self.current().pathname)
+ self.listdir()
+ elif VALID_SONG(self.current().filename):
+ app.play(self.current())
+
+ def command_chparentdir(self):
+ if app.restricted and self.cwd == self.startdir: return
+ dir = os.path.basename(self.cwd)
+ self.chdir(os.path.dirname(self.cwd))
+ self.listdir(prevdir=dir)
+
+ def command_goto(self):
+ if app.restricted: return
+ app.stop_input_hook = self.stop_goto
+ app.complete_input_hook = self.complete_generic
+ app.start_input(_("goto"))
+
+ def stop_goto(self):
+ dir = os.path.expanduser(app.input_string)
+ if dir[0] != '/': dir = os.path.join(self.cwd, dir)
+ if not os.path.isdir(dir):
+ app.status(_("Not a directory!"), 1)
+ return
+ self.chdir(dir)
+ self.listdir()
+
+ def command_add_recursively(self):
+ l = self.get_tagged()
+ if not l:
+ app.win_playlist.add(self.current().pathname)
+ self.cursor_move(1)
+ return
+ app.status(_("Adding tagged files"), 1)
+ for entry in l:
+ app.win_playlist.add(entry.pathname, quiet=1)
+ entry.set_tagged(0)
+ self.update()
+
+# ------------------------------------------
+class PlaylistWindow(TagListWindow):
+ def __init__(self, parent):
+ TagListWindow.__init__(self, parent)
+ self.pathname = None
+ self.repeat = 0
+ self.random = 0
+ self.random_prev = []
+ self.random_next = []
+ self.random_left = []
+ self.stop = 0
+ self.keymap.bind(['\n', curses.KEY_ENTER],
+ self.command_play, ())
+ self.keymap.bind('d', self.command_delete, ())
+ self.keymap.bind('D', self.command_delete_all, ())
+ self.keymap.bind('m', self.command_move, (1,))
+ self.keymap.bind('M', self.command_move, (0,))
+ self.keymap.bind('s', self.command_shuffle, ())
+ self.keymap.bind('S', self.command_sort, ())
+ self.keymap.bind('r', self.command_toggle_repeat, ())
+ self.keymap.bind('R', self.command_toggle_random, ())
+ self.keymap.bind('X', self.command_toggle_stop, ())
+ self.keymap.bind('w', self.command_save_playlist, ())
+ self.keymap.bind('@', self.command_jump_to_active, ())
+
+ def command_change_viewpoint(self, klass=PlaylistEntry):
+ TagListWindow.command_change_viewpoint(self, klass)
+
+ def get_title(self):
+ space_out = lambda value, s: value and s or " "*len(s)
+ self.name = _("Playlist %s %s %s") % (
+ space_out(self.repeat, _("[repeat]")),
+ space_out(self.random, _("[random]")),
+ space_out(self.stop, _("[stop]")))
+ return ListWindow.get_title(self)
+
+ def append(self, item):
+ self.buffer.append(item)
+ if self.random: self.random_left.append(item)
+
+ def add_dir(self, dir):
+ try:
+ filenames = os.listdir(dir)
+ filenames.sort()
+ subdirs = []
+ for filename in filenames:
+ pathname = os.path.join(dir, filename)
+ if VALID_SONG(filename):
+ self.append(PlaylistEntry(pathname))
+ if os.path.isdir(pathname):
+ subdirs.append(pathname)
+ map(self.add_dir, subdirs)
+ except Exception, e:
+ app.status(e, 2)
+
+ def add_m3u(self, line):
+ if re.match("^(#.*)?$", line): return
+ if re.match("^(/|http://)", line):
+ self.append(PlaylistEntry(self.fix_url(line)))
+ else:
+ dirname = os.path.dirname(self.pathname)
+ self.append(PlaylistEntry(os.path.join(dirname, line)))
+
+ def add_pls(self, line):
+ # todo - support title & length
+ m = re.match("File(\d+)=(.*)", line)
+ if m: self.append(PlaylistEntry(self.fix_url(m.group(2))))
+
+ def add_playlist(self, pathname):
+ self.pathname = pathname
+ if re.search("\.m3u$", pathname, re.I): f = self.add_m3u
+ if re.search("\.pls$", pathname, re.I): f = self.add_pls
+ file = open(pathname)
+ map(f, map(string.strip, file.readlines()))
+ file.close()
+
+ def add(self, pathname, quiet=0):
+ try:
+ if os.path.isdir(pathname):
+ app.status(_("Working..."), 10)
+ self.add_dir(pathname)
+ elif VALID_PLAYLIST(pathname):
+ self.add_playlist(pathname)
+ else:
+ pathname = self.fix_url(pathname)
+ self.append(PlaylistEntry(pathname))
+ # todo - refactor
+ filename = os.path.basename(pathname) or pathname
+ quiet or self.update()
+ quiet or app.status(_("Added: %s") % filename, 1)
+ except Exception, e:
+ app.status(e, 2)
+
+ def fix_url(self, url):
+ return re.sub("(http://[^/]+)/?(.*)", "\\1/\\2", url)
+
+ def putstr(self, entry, *pos):
+ # Color of an active *playlist* item
+ if entry.is_active(): self.attron(curses.color_pair(3))
+ ListWindow.putstr(self, entry, *pos)
+ if entry.is_active(): self.attroff(curses.color_pair(3))
+
+ def change_active_entry(self, direction):
+ if not self.buffer: return
+ old = self.get_active_entry()
+ new = None
+ if self.random:
+ if direction > 0:
+ if self.random_next: new = self.random_next.pop()
+ elif self.random_left: pass
+ elif self.repeat: self.random_left = self.buffer[:]
+ else: return
+ if not new:
+ import random
+ new = random.choice(self.random_left)
+ self.random_left.remove(new)
+ try: self.random_prev.remove(new)
+ except ValueError: pass
+ self.random_prev.append(new)
+ else:
+ if len(self.random_prev) > 1:
+ self.random_next.append(self.random_prev.pop())
+ new = self.random_prev[-1]
+ else: return
+ old and old.set_active(0)
+ elif old:
+ index = self.buffer.index(old)+direction
+ if not (0 <= index < len(self.buffer) or self.repeat): return
+ old.set_active(0)
+ new = self.buffer[index % len(self.buffer)]
+ else:
+ new = self.buffer[0]
+ new.set_active(1)
+ self.update()
+ return new
+
+ def get_active_entry(self):
+ for entry in self.buffer:
+ if entry.is_active(): return entry
+
+ def command_jump_to_active(self):
+ entry = self.get_active_entry()
+ if not entry: return
+ self.bufptr = self.buffer.index(entry)
+ self.update()
+
+ def command_play(self):
+ if not self.buffer: return
+ entry = self.get_active_entry()
+ entry and entry.set_active(0)
+ entry = self.current()
+ entry.set_active(1)
+ self.update()
+ app.play(entry)
+
+ def command_delete(self):
+ if not self.buffer: return
+ current_entry, n = self.current(), len(self.buffer)
+ self.buffer = self.not_tagged(self.buffer)
+ if n > len(self.buffer):
+ try: self.bufptr = self.buffer.index(current_entry)
+ except ValueError: pass
+ else:
+ current_entry.set_tagged(1)
+ del self.buffer[self.bufptr]
+ if self.random:
+ self.random_prev = self.not_tagged(self.random_prev)
+ self.random_next = self.not_tagged(self.random_next)
+ self.random_left = self.not_tagged(self.random_left)
+ self.update()
+
+ def command_delete_all(self):
+ self.buffer = []
+ self.random_prev = []
+ self.random_next = []
+ self.random_left = []
+ app.status(_("Deleted playlist"), 1)
+ self.update()
+
+ def command_move(self, after):
+ if not self.buffer: return
+ current_entry, l = self.current(), self.get_tagged()
+ if not l or current_entry.is_tagged(): return
+ self.buffer = self.not_tagged(self.buffer)
+ self.bufptr = self.buffer.index(current_entry)+after
+ self.buffer[self.bufptr:self.bufptr] = l
+ self.update()
+
+ def command_shuffle(self):
+ import random
+ l = []
+ n = len(self.buffer)
+ while n > 0:
+ n = n-1
+ r = random.randint(0, n)
+ l.append(self.buffer[r])
+ del self.buffer[r]
+ self.buffer = l
+ self.bufptr = 0
+ self.update()
+ app.status(_("Shuffled playlist... Oops?"), 1)
+
+ def command_sort(self):
+ app.status(_("Working..."))
+ self.buffer.sort(lambda x, y: x.vp() > y.vp() or -1)
+ self.bufptr = 0
+ self.update()
+ app.status(_("Sorted playlist"), 1)
+
+ def command_toggle_repeat(self):
+ self.toggle("repeat", _("Repeat: %s"))
+
+ def command_toggle_random(self):
+ self.toggle("random", _("Random: %s"))
+ self.random_prev = []
+ self.random_next = []
+ self.random_left = self.buffer[:]
+
+ def command_toggle_stop(self):
+ self.toggle("stop", _("Stop playlist: %s"))
+
+ def toggle(self, attr, format):
+ setattr(self, attr, not getattr(self, attr))
+ app.status(format % (getattr(self, attr) and _("on") or _("off")), 1)
+ self.parent.update_title()
+
+ def command_save_playlist(self):
+ if app.restricted: return
+ default = self.pathname or "%s/" % app.win_filelist.cwd
+ app.stop_input_hook = self.stop_save_playlist
+ app.start_input(_("Save playlist"), default)
+
+ def stop_save_playlist(self):
+ pathname = app.input_string
+ if pathname[0] != '/':
+ pathname = os.path.join(app.win_filelist.cwd, pathname)
+ if not re.search("\.m3u$", pathname, re.I):
+ pathname = "%s%s" % (pathname, ".m3u")
+ try:
+ file = open(pathname, "w")
+ for entry in self.buffer:
+ file.write("%s\n" % entry.pathname)
+ file.close()
+ self.pathname = pathname
+ app.status(_("ok"), 1)
+ except IOError, e:
+ app.status(e, 2)
+
+# ------------------------------------------
+def get_tag(pathname):
+ if re.compile("^http://").match(pathname) or not os.path.exists(pathname):
+ return pathname
+ tags = {}
+ # FIXME: use magic instead of file extensions to identify OGGs and MP3s
+ if re.compile(".*\.ogg$", re.I).match(pathname):
+ try:
+ import ogg.vorbis
+ vf = ogg.vorbis.VorbisFile(pathname)
+ vc = vf.comment()
+ tags = vc.as_dict()
+ except: return os.path.basename(pathname)
+ elif re.compile(".*\.mp3$", re.I).match(pathname):
+ try:
+ import ID3
+ vc = ID3.ID3(pathname, as_tuple=1)
+ tags = vc.as_dict()
+ except: return os.path.basename(pathname)
+ else:
+ return os.path.basename(pathname)
+
+ artist = tags.get("ARTIST", [""])[0]
+ title = tags.get("TITLE", [""])[0]
+ tag = os.path.basename(pathname)
+ try:
+ import codecs
+ if artist and title:
+ tag = codecs.latin_1_encode(artist)[0] + " - " + codecs.latin_1_encode(title)[0]
+ elif artist:
+ tag = artist
+ elif title:
+ tag = title
+ return codecs.latin_1_encode(tag)[0]
+ except: return tag
+
+# ------------------------------------------
+class Player:
+
+ stdin_r, stdin_w = os.pipe()
+ stdout_r, stdout_w = os.pipe()
+ stderr_r, stderr_w = os.pipe()
+
+ def __init__(self, commandline, files, fps=1):
+ self.commandline = commandline
+ self.re_files = re.compile(files, re.I)
+ self.fps = fps
+ self.entry = None
+ self.stopped = 0
+ self.paused = 0
+ self.time_setup = None
+ self.buf = ''
+ self.tid = None
+
+ def setup(self, entry, offset):
+ self.argv = string.split(self.commandline)
+ self.argv[0] = which(self.argv[0])
+ for i in range(len(self.argv)):
+ if self.argv[i] == "%s": self.argv[i] = entry.pathname
+ if self.argv[i] == "%d": self.argv[i] = str(offset*self.fps)
+ self.entry = entry
+ if offset == 0:
+ app.progress(0)
+ self.offset = 0
+ self.length = 0
+ self.values = [0, 0]
+ self.time_setup = time.time()
+ return self.argv[0]
+
+ def play(self):
+ self.pid = os.fork()
+ if self.pid == 0:
+ os.dup2(self.stdin_w, sys.stdin.fileno())
+ os.dup2(self.stdout_w, sys.stdout.fileno())
+ os.dup2(self.stderr_w, sys.stderr.fileno())
+ os.setpgrp()
+ try: os.execv(self.argv[0], self.argv)
+ except: os._exit(1)
+ self.stopped = 0
+ self.paused = 0
+ self.step = 0
+ self.update_status()
+
+ def stop(self, quiet=0):
+ self.paused and self.toggle_pause(quiet)
+ try:
+ while 1:
+ try: os.kill(-self.pid, signal.SIGINT)
+ except os.error: pass
+ os.waitpid(self.pid, os.WNOHANG)
+ except Exception: pass
+ self.stopped = 1
+ quiet or self.update_status()
+
+ def toggle_pause(self, quiet=0):
+ try: os.kill(-self.pid, [signal.SIGSTOP, signal.SIGCONT][self.paused])
+ except os.error: return
+ self.paused = not self.paused
+ quiet or self.update_status()
+
+ def parse_progress(self):
+ if self.stopped or self.step: self.tid = None
+ else:
+ self.parse_buf()
+ self.tid = app.timeout.add(1.0, self.parse_progress)
+
+ def read_fd(self, fd):
+ self.buf = os.read(fd, 512)
+ self.tid or self.parse_progress()
+
+ def poll(self):
+ try: os.waitpid(self.pid, os.WNOHANG)
+ except:
+ # something broken? try again
+ if self.time_setup and (time.time() - self.time_setup) < 2.0:
+ self.play()
+ return 0
+ app.set_default_status("")
+ app.counter([0,0])
+ app.progress(0)
+ return 1
+
+ def seek(self, offset, relative):
+ if relative:
+ d = offset * self.length * 0.002
+ self.step = self.step * (self.step * d > 0) + d
+ self.offset = min(self.length, max(0, self.offset+self.step))
+ else:
+ self.step = 1
+ self.offset = (offset < 0) and self.length+offset or offset
+ self.show_position()
+
+ def set_position(self, offset, length, values):
+ self.offset = offset
+ self.length = length
+ self.values = values
+ self.show_position()
+
+ def show_position(self):
+ app.counter(self.values)
+ app.progress(self.length and (float(self.offset) / self.length))
+
+ def update_status(self):
+ if not self.entry:
+ app.set_default_status("")
+ elif self.stopped:
+ app.set_default_status(_("Stopped: %s") % self.entry.vp())
+ elif self.paused:
+ app.set_default_status(_("Paused: %s") % self.entry.vp())
+ else:
+ app.set_default_status(_("Playing: %s") % self.entry.vp())
+
+# ------------------------------------------
+class FrameOffsetPlayer(Player):
+ re_progress = re.compile("Time.*\s(\d+):(\d+).*\[(\d+):(\d+)")
+
+ def parse_buf(self):
+ match = self.re_progress.search(self.buf)
+ if match:
+ m1, s1, m2, s2 = map(string.atoi, match.groups())
+ head, tail = m1*60+s1, m2*60+s2
+ self.set_position(head, head+tail, [head, tail])
+
+# ------------------------------------------
+class FrameOffsetPlayerMpp(Player):
+ re_progress = re.compile(".*\s(\d+):(\d+).*\s(\d+):(\d+)")
+
+ def parse_buf(self):
+ match = self.re_progress.search(self.buf)
+ if match:
+ m1, s1, m2, s2 = map(string.atoi, match.groups())
+ head = m1*60+s1
+ tail = (m2*60+s2) - head
+ self.set_position(head, head+tail , [head, tail])
+
+# ------------------------------------------
+class TimeOffsetPlayer(Player):
+ re_progress = re.compile("(\d+):(\d+):(\d+)")
+
+ def parse_buf(self):
+ match = self.re_progress.search(self.buf)
+ if match:
+ h, m, s = map(string.atoi, match.groups())
+ tail = h*3600+m*60+s
+ head = max(self.length, tail) - tail
+ self.set_position(head, head+tail, [head, tail])
+
+# ------------------------------------------
+class TimeOffsetPlayerMplayer(Player):
+ re_progress = re.compile("^A:.*?(\d+)\.\d \([^)]+\) of (\d+)\.\d")
+
+ def parse_buf(self):
+ match = self.re_progress.search(self.buf)
+ if match:
+ curS, totS = map(string.atoi, match.groups())
+ position, length = curS, totS
+ self.set_position(position, length, [position, length])
+
+# ------------------------------------------
+class NoOffsetPlayer(Player):
+
+ def parse_buf(self):
+ head = self.offset+1
+ self.set_position(head, 0, [head, head])
+
+ def seek(self, *dummy):
+ return 1
+
+# ------------------------------------------
+class Timeout:
+ def __init__(self):
+ self.next = 0
+ self.dict = {}
+
+ def add(self, timeout, func, args=()):
+ tid = self.next = self.next + 1
+ self.dict[tid] = (func, args, time.time() + timeout)
+ return tid
+
+ def remove(self, tid):
+ del self.dict[tid]
+
+ def check(self, now):
+ for tid, (func, args, timeout) in self.dict.items():
+ if now >= timeout:
+ self.remove(tid)
+ func(*args)
+ return len(self.dict) and 0.2 or None
+
+# ------------------------------------------
+class FIFOControl:
+ def __init__(self):
+ self.commands = {
+ "pause" : [app.toggle_pause, []],
+ "next" : [app.next_song, []],
+ "prev" : [app.prev_song, []],
+ "forward" : [app.seek, [1, 1]],
+ "backward" : [app.seek, [-1, 1]],
+ "play" : [app.toggle_stop, []],
+ "stop" : [app.toggle_stop, []],
+ "volume" : [self.volume, None],
+ "macro" : [app.run_macro, None],
+ "add" : [app.win_playlist.add, None],
+ "empty" : [app.win_playlist.command_delete_all, []],
+ "quit" : [app.quit, []]
+ }
+ self.fd = None
+ try:
+ if os.path.exists(CONTROL_FIFO):
+ os.unlink(CONTROL_FIFO)
+ os.mkfifo(CONTROL_FIFO, 0600)
+ self.fd = open(CONTROL_FIFO, "rb+", 0)
+ except IOError:
+ return
+
+ def handle_command(self):
+ argv = self.fd.readline().strip().split(" ", 1)
+ if argv[0] in self.commands.keys():
+ f, a = self.commands[argv[0]]
+ if a is None: a = argv[1:]
+ f(*a)
+
+ def volume(self, s):
+ argv = s.split()
+ app.mixer(argv[0], int(argv[1]))
+
+# ------------------------------------------
+class Application:
+ def __init__(self):
+ self.keymapstack = KeymapStack()
+ self.input_mode = 0
+ self.input_prompt = ""
+ self.input_string = ""
+ self.do_input_hook = None
+ self.stop_input_hook = None
+ self.complete_input_hook = None
+ self.channels = []
+ self.restricted = 0
+ self.input_keymap = Keymap()
+ self.input_keymap.bind(list(Window.chars), self.do_input)
+ self.input_keymap.bind([127, curses.KEY_BACKSPACE], self.do_input, (8,))
+ self.input_keymap.bind([21, 23], self.do_input)
+ self.input_keymap.bind(['\a', 27], self.cancel_input, ())
+ self.input_keymap.bind(['\n', curses.KEY_ENTER], self.stop_input, ())
+
+ def command_macro(self):
+ app.do_input_hook = self.do_macro
+ app.start_input(_("macro"))
+
+ def do_macro(self, ch):
+ app.stop_input()
+ self.run_macro(chr(ch))
+
+ def run_macro(self, c):
+ for i in MACRO.get(c, ""):
+ self.keymapstack.process(ord(i))
+
+ 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()
+ # Function start_color() called after initscr
+ curses.start_color()
+ # Added to support transparency
+ curses.use_default_colors()
+ # Custom color pairs
+ # pair 0 is always white on black
+ # color -1 is the default color if use_default_colors() is called
+ curses.init_pair(1, curses.COLOR_RED, -1)
+ curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
+ curses.init_pair(3, curses.COLOR_YELLOW, -1)
+ curses.init_pair(4, curses.COLOR_YELLOW, curses.COLOR_BLACK)
+ # If you'd like it more colorful, you can play with:
+ # BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN and WHITE
+ #
+ curses.cbreak()
+ curses.noecho()
+ try: curses.meta(1)
+ except: pass
+ self.cursor(0)
+ 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.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.win_status = self.win_root.win_status
+ self.status = self.win_status.status
+ self.set_default_status = self.win_status.set_default_status
+ self.restore_default_status = self.win_status.restore_default_status
+ self.counter = self.win_root.win_counter.counter
+ self.progress = self.win_root.win_progress.progress
+ self.player = PLAYERS[0]
+ self.timeout = Timeout()
+ self.play_tid = None
+ self.kludge = 0
+ self.win_filelist.listdir()
+ self.control = FIFOControl()
+
+ def cleanup(self):
+ try: curses.endwin()
+ except curses.error: return
+ XTERM and sys.stderr.write("\033]0;%s\a" % "xterm")
+ tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr)
+ print
+ try:
+ if os.path.exists(CONTROL_FIFO): os.unlink(CONTROL_FIFO)
+ except IOError:
+ pass
+
+ def run(self):
+ while 1:
+ now = time.time()
+ timeout = self.timeout.check(now)
+ self.win_filelist.listdir_maybe(now)
+ if not self.player.stopped:
+ timeout = 0.5
+ if self.kludge and self.player.poll():
+ self.player.stopped = 1 # end of playlist hack
+ if not self.win_playlist.stop:
+ entry = self.win_playlist.change_active_entry(1)
+ entry and self.play(entry)
+ R = [sys.stdin, self.player.stdout_r, self.player.stderr_r]
+ self.control.fd and R.append(self.control.fd)
+ try: r, w, e = select.select(R, [], [], timeout)
+ except select.error: continue
+ self.kludge = 1
+ # user
+ if sys.stdin in r:
+ c = self.win_root.getch()
+ self.keymapstack.process(c)
+ # player
+ if self.player.stderr_r in r:
+ self.player.read_fd(self.player.stderr_r)
+ # player
+ if self.player.stdout_r in r:
+ self.player.read_fd(self.player.stdout_r)
+ # remote
+ if self.control.fd in r:
+ self.control.handle_command()
+
+ def play(self, entry, offset = 0):
+ self.kludge = 0
+ self.play_tid = None
+ if entry is None or offset is None: return
+ self.player.stop(quiet=1)
+ for self.player in PLAYERS:
+ if self.player.re_files.search(entry.pathname):
+ if self.player.setup(entry, offset): break
+ else:
+ app.status(_("Player not found!"), 1)
+ self.player.stopped = 0 # keep going
+ return
+ self.player.play()
+
+ def delayed_play(self, entry, offset):
+ if self.play_tid: self.timeout.remove(self.play_tid)
+ self.play_tid = self.timeout.add(0.5, self.play, (entry, offset))
+
+ def next_song(self):
+ self.delayed_play(self.win_playlist.change_active_entry(1), 0)
+
+ def prev_song(self):
+ self.delayed_play(self.win_playlist.change_active_entry(-1), 0)
+
+ def seek(self, offset, relative):
+ if not self.player.entry: return
+ self.player.seek(offset, relative)
+ self.delayed_play(self.player.entry, self.player.offset)
+
+ def toggle_pause(self):
+ if not self.player.entry: return
+ if not self.player.stopped: self.player.toggle_pause()
+
+ def toggle_stop(self):
+ if not self.player.entry: return
+ if not self.player.stopped: self.player.stop()
+ else: self.play(self.player.entry, self.player.offset)
+
+ def key_volume(self, ch):
+ self.mixer("set", (ch & 0x0f)*10)
+
+ def mixer(self, cmd=None, arg=None):
+ try: self._mixer(cmd, arg)
+ except Exception, e: app.status(e, 2)
+
+ def _mixer(self, cmd, arg):
+ try:
+ import ossaudiodev
+ mixer = ossaudiodev.openmixer()
+ get, set = mixer.get, mixer.set
+ self.channels = self.channels or \
+ [['MASTER', ossaudiodev.SOUND_MIXER_VOLUME],
+ ['PCM', ossaudiodev.SOUND_MIXER_PCM]]
+ except ImportError:
+ import oss
+ mixer = oss.open_mixer()
+ get, set = mixer.read_channel, mixer.write_channel
+ self.channels = self.channels or \
+ [['MASTER', oss.SOUND_MIXER_VOLUME],
+ ['PCM', oss.SOUND_MIXER_PCM]]
+ if cmd == "toggle": self.channels.insert(0, self.channels.pop())
+ name, channel = self.channels[0]
+ if cmd == "cue": arg = min(100, max(0, get(channel)[0] + arg))
+ if cmd in ["set", "cue"]: set(channel, (arg, arg))
+ app.status(_("%s volume %s%%") % (name, get(channel)[0]), 1)
+ mixer.close()
+
+ def show_input(self):
+ n = len(self.input_prompt)+1
+ s = cut(self.input_string, self.win_status.cols-n, left=1)
+ app.status("%s%s " % (self.input_prompt, s))
+
+ def start_input(self, prompt="", data="", colon=1):
+ self.input_mode = 1
+ self.cursor(1)
+ app.keymapstack.push(self.input_keymap)
+ self.input_prompt = prompt + (colon and ": " or "")
+ self.input_string = data
+ self.show_input()
+
+ def do_input(self, *args):
+ if self.do_input_hook:
+ return 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 == 9 and self.complete_input_hook:
+ self.input_string = self.complete_input_hook(self.input_string)
+ elif ch == 21: # C-u
+ self.input_string = ""
+ elif ch == 23: # C-w
+ self.input_string = re.sub("((.* )?)\w.*", "\\1", self.input_string)
+ elif ch:
+ self.input_string = "%s%c" % (self.input_string, ch)
+ self.show_input()
+
+ def stop_input(self, *args):
+ self.input_mode = 0
+ self.cursor(0)
+ app.keymapstack.pop()
+ if not self.input_string:
+ app.status(_("cancel"), 1)
+ elif self.stop_input_hook:
+ self.stop_input_hook(*args)
+ self.do_input_hook = None
+ self.stop_input_hook = None
+ self.complete_input_hook = None
+
+ def cancel_input(self):
+ self.input_string = ""
+ self.stop_input()
+
+ def cursor(self, visibility):
+ try: curses.curs_set(visibility)
+ except: pass
+
+ def quit(self, status=0):
+ self.player.stop(quiet=1)
+ sys.exit(status)
+
+ def handler_resize(self, sig, frame):
+ # curses trickery
+ while 1:
+ try: curses.endwin(); break
+ except: time.sleep(1)
+ self.w.refresh()
+ self.win_root.resize()
+ self.win_root.update()
+
+ def handler_quit(self, sig, frame):
+ self.quit(1)
+
+# ------------------------------------------
+def main():
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "nrRv")
+ except:
+ usage = _("Usage: %s [-nrRv] [ file | dir | playlist ] ...\n")
+ sys.stderr.write(usage % sys.argv[0])
+ sys.exit(1)
+
+ global app
+ app = Application()
+
+ 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 == "-n": app.restricted = 1
+ if opt == "-r": app.win_playlist.command_toggle_repeat()
+ if opt == "-R": app.win_playlist.command_toggle_random()
+ if opt == "-v": app.mixer("toggle")
+ if args or playlist:
+ for i in args or playlist:
+ i = os.path.exists(i) and os.path.abspath(i) or i
+ app.win_playlist.add(i)
+ app.win_tab.change_window()
+ app.run()
+ except SystemExit:
+ app.cleanup()
+ # Can we display colors?
+ #print curses.has_colors()
+ except Exception:
+ app.cleanup()
+ import traceback
+ traceback.print_exc()
+
+# ------------------------------------------
+PLAYERS = [
+ FrameOffsetPlayer("ogg123 -q -v -k %d %s", "\.(ogg|flac|spx)$"),
+ FrameOffsetPlayer("splay -f -k %d %s", "(^http://|\.mp[123]$)", 38.28),
+ FrameOffsetPlayer("mpg123 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28),
+ FrameOffsetPlayer("mpg321 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28),
+ FrameOffsetPlayerMpp("mppdec --gain 2 --start %d %s", "\.mp[cp+]$"),
+ TimeOffsetPlayer("madplay -v --display-time=remaining -s %d %s", "\.mp[123]$"),
+ NoOffsetPlayer("mikmod -q -p0 %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm)$"),
+ NoOffsetPlayer("xmp -q %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm|stm)$"),
+ NoOffsetPlayer("play %s", "\.(aiff|au|cdr|mp3|ogg|wav)$"),
+ NoOffsetPlayer("speexdec %s", "\.spx$"),
+ TimeOffsetPlayerMplayer("mplayer %s", "^http://|\.(mp[123]|m4a|ogg|flac|spx|mp[cp+]|mod|xm|fm|s3m|med|col|669|it|mtm|stm|aiff|au|cdr|wav|wma)$")
+ ]
+
+MACRO = {}
+
+def VALID_SONG(name):
+ for player in PLAYERS:
+ if player.re_files.search(name):
+ return 1
+
+def VALID_PLAYLIST(name):
+ if re.search("\.(m3u|pls)$", name, re.I):
+ return 1
+
+# ------------------------------------------
+if __name__ == "__main__":
+ main()