cplay
diff cplay @ 0:aa5f022eac8a
Use upstream cplay-1.49 as a start
author | markus schnalke <meillo@marmaro.de> |
---|---|
date | Wed, 27 Sep 2017 09:22:32 +0200 |
parents | |
children | c7d8ec7da73b |
line diff
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/cplay Wed Sep 27 09:22:32 2017 +0200 1.3 @@ -0,0 +1,1655 @@ 1.4 +#!/usr/bin/env python 1.5 +# -*- python -*- 1.6 + 1.7 +__version__ = "cplay 1.49" 1.8 + 1.9 +""" 1.10 +cplay - A curses front-end for various audio players 1.11 +Copyright (C) 1998-2003 Ulf Betlehem <flu@iki.fi> 1.12 + 1.13 +This program is free software; you can redistribute it and/or 1.14 +modify it under the terms of the GNU General Public License 1.15 +as published by the Free Software Foundation; either version 2 1.16 +of the License, or (at your option) any later version. 1.17 + 1.18 +This program is distributed in the hope that it will be useful, 1.19 +but WITHOUT ANY WARRANTY; without even the implied warranty of 1.20 +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1.21 +GNU General Public License for more details. 1.22 + 1.23 +You should have received a copy of the GNU General Public License 1.24 +along with this program; if not, write to the Free Software 1.25 +Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 1.26 +""" 1.27 + 1.28 +# ------------------------------------------ 1.29 +from types import * 1.30 + 1.31 +import os 1.32 +import sys 1.33 +import time 1.34 +import getopt 1.35 +import signal 1.36 +import string 1.37 +import select 1.38 +import re 1.39 + 1.40 +try: from ncurses import curses 1.41 +except ImportError: import curses 1.42 + 1.43 +try: import tty 1.44 +except ImportError: tty = None 1.45 + 1.46 +try: import locale; locale.setlocale(locale.LC_ALL, "") 1.47 +except: pass 1.48 + 1.49 +# ------------------------------------------ 1.50 +_locale_domain = "cplay" 1.51 +_locale_dir = "/usr/local/share/locale" 1.52 + 1.53 +try: 1.54 + import gettext # python 2.0 1.55 + gettext.install(_locale_domain, _locale_dir) 1.56 +except ImportError: 1.57 + try: 1.58 + import fintl 1.59 + fintl.bindtextdomain(_locale_domain, _locale_dir) 1.60 + fintl.textdomain(_locale_domain) 1.61 + _ = fintl.gettext 1.62 + except ImportError: 1.63 + def _(s): return s 1.64 +except: 1.65 + def _(s): return s 1.66 + 1.67 +# ------------------------------------------ 1.68 +XTERM = re.search("rxvt|xterm", os.environ["TERM"]) 1.69 +CONTROL_FIFO = "/var/tmp/cplay_control" 1.70 + 1.71 +# ------------------------------------------ 1.72 +def which(program): 1.73 + for path in string.split(os.environ["PATH"], ":"): 1.74 + if os.path.exists(os.path.join(path, program)): 1.75 + return os.path.join(path, program) 1.76 + 1.77 +# ------------------------------------------ 1.78 +def cut(s, n, left=0): 1.79 + if left: return len(s) > n and "<%s" % s[-n+1:] or s 1.80 + else: return len(s) > n and "%s>" % s[:n-1] or s 1.81 + 1.82 +# ------------------------------------------ 1.83 +class Stack: 1.84 + def __init__(self): 1.85 + self.items = () 1.86 + 1.87 + def push(self, item): 1.88 + self.items = (item,) + self.items 1.89 + 1.90 + def pop(self): 1.91 + self.items, item = self.items[1:], self.items[0] 1.92 + return item 1.93 + 1.94 +# ------------------------------------------ 1.95 +class KeymapStack(Stack): 1.96 + def process(self, code): 1.97 + for keymap in self.items: 1.98 + if keymap and keymap.process(code): 1.99 + break 1.100 + 1.101 +# ------------------------------------------ 1.102 +class Keymap: 1.103 + def __init__(self): 1.104 + self.methods = [None] * curses.KEY_MAX 1.105 + 1.106 + def bind(self, key, method, args=None): 1.107 + if type(key) in (TupleType, ListType): 1.108 + for i in key: self.bind(i, method, args) 1.109 + return 1.110 + if type(key) is StringType: 1.111 + key = ord(key) 1.112 + self.methods[key] = (method, args) 1.113 + 1.114 + def process(self, key): 1.115 + if self.methods[key] is None: return 0 1.116 + method, args = self.methods[key] 1.117 + if args is None: 1.118 + apply(method, (key,)) 1.119 + else: 1.120 + apply(method, args) 1.121 + return 1 1.122 + 1.123 +# ------------------------------------------ 1.124 +class Window: 1.125 + chars = string.letters+string.digits+string.punctuation+string.whitespace 1.126 + 1.127 + t = ['?'] * 256 1.128 + for c in chars: t[ord(c)] = c 1.129 + translationTable = string.join(t, ""); del t 1.130 + 1.131 + def __init__(self, parent): 1.132 + self.parent = parent 1.133 + self.children = [] 1.134 + self.name = None 1.135 + self.keymap = None 1.136 + self.visible = 1 1.137 + self.resize() 1.138 + if parent: parent.children.append(self) 1.139 + 1.140 + def insstr(self, s): 1.141 + if not s: return 1.142 + self.w.addstr(s[:-1]) 1.143 + self.w.hline(ord(s[-1]), 1) # insch() work-around 1.144 + 1.145 + def __getattr__(self, name): 1.146 + return getattr(self.w, name) 1.147 + 1.148 + def getmaxyx(self): 1.149 + y, x = self.w.getmaxyx() 1.150 + try: curses.version # tested with 1.2 and 1.6 1.151 + except AttributeError: 1.152 + # pyncurses - emulate traditional (silly) behavior 1.153 + y, x = y+1, x+1 1.154 + return y, x 1.155 + 1.156 + def touchwin(self): 1.157 + try: self.w.touchwin() 1.158 + except AttributeError: self.touchln(0, self.getmaxyx()[0]) 1.159 + 1.160 + def attron(self, attr): 1.161 + try: self.w.attron(attr) 1.162 + except AttributeError: self.w.attr_on(attr) 1.163 + 1.164 + def attroff(self, attr): 1.165 + try: self.w.attroff(attr) 1.166 + except AttributeError: self.w.attr_off(attr) 1.167 + 1.168 + def newwin(self): 1.169 + return curses.newwin(0, 0, 0, 0) 1.170 + 1.171 + def resize(self): 1.172 + self.w = self.newwin() 1.173 + self.ypos, self.xpos = self.getbegyx() 1.174 + self.rows, self.cols = self.getmaxyx() 1.175 + self.keypad(1) 1.176 + self.leaveok(0) 1.177 + self.scrollok(0) 1.178 + for child in self.children: 1.179 + child.resize() 1.180 + 1.181 + def update(self): 1.182 + self.clear() 1.183 + self.refresh() 1.184 + for child in self.children: 1.185 + child.update() 1.186 + 1.187 +# ------------------------------------------ 1.188 +class ProgressWindow(Window): 1.189 + def __init__(self, parent): 1.190 + Window.__init__(self, parent) 1.191 + self.value = 0 1.192 + 1.193 + def newwin(self): 1.194 + return curses.newwin(1, self.parent.cols, self.parent.rows-2, 0) 1.195 + 1.196 + def update(self): 1.197 + self.move(0, 0) 1.198 + self.hline(ord('-'), self.cols) 1.199 + if self.value > 0: 1.200 + self.move(0, 0) 1.201 + x = int(self.value * self.cols) # 0 to cols-1 1.202 + x and self.hline(ord('='), x) 1.203 + self.move(0, x) 1.204 + self.insstr('|') 1.205 + self.touchwin() 1.206 + self.refresh() 1.207 + 1.208 + def progress(self, value): 1.209 + self.value = min(value, 0.99) 1.210 + self.update() 1.211 + 1.212 +# ------------------------------------------ 1.213 +class StatusWindow(Window): 1.214 + def __init__(self, parent): 1.215 + Window.__init__(self, parent) 1.216 + self.default_message = '' 1.217 + self.current_message = '' 1.218 + self.tid = None 1.219 + 1.220 + def newwin(self): 1.221 + return curses.newwin(1, self.parent.cols-12, self.parent.rows-1, 0) 1.222 + 1.223 + def update(self): 1.224 + msg = string.translate(self.current_message, Window.translationTable) 1.225 + self.move(0, 0) 1.226 + self.clrtoeol() 1.227 + self.insstr(cut(msg, self.cols)) 1.228 + self.touchwin() 1.229 + self.refresh() 1.230 + 1.231 + def status(self, message, duration = 0): 1.232 + self.current_message = str(message) 1.233 + if self.tid: app.timeout.remove(self.tid) 1.234 + if duration: self.tid = app.timeout.add(duration, self.timeout) 1.235 + else: self.tid = None 1.236 + self.update() 1.237 + 1.238 + def timeout(self): 1.239 + self.tid = None 1.240 + self.restore_default_status() 1.241 + 1.242 + def set_default_status(self, message): 1.243 + if self.current_message == self.default_message: self.status(message) 1.244 + self.default_message = message 1.245 + XTERM and sys.stderr.write("\033]0;%s\a" % (message or "cplay")) 1.246 + 1.247 + def restore_default_status(self): 1.248 + self.status(self.default_message) 1.249 + 1.250 +# ------------------------------------------ 1.251 +class CounterWindow(Window): 1.252 + def __init__(self, parent): 1.253 + Window.__init__(self, parent) 1.254 + self.values = [0, 0] 1.255 + self.mode = 1 1.256 + 1.257 + def newwin(self): 1.258 + return curses.newwin(1, 11, self.parent.rows-1, self.parent.cols-11) 1.259 + 1.260 + def update(self): 1.261 + h, s = divmod(self.values[self.mode], 3600) 1.262 + m, s = divmod(s, 60) 1.263 + self.move(0, 0) 1.264 + self.attron(curses.A_BOLD) 1.265 + self.insstr("%02dh %02dm %02ds" % (h, m, s)) 1.266 + self.attroff(curses.A_BOLD) 1.267 + self.touchwin() 1.268 + self.refresh() 1.269 + 1.270 + def counter(self, values): 1.271 + self.values = values 1.272 + self.update() 1.273 + 1.274 + def toggle_mode(self): 1.275 + self.mode = not self.mode 1.276 + tmp = [_("elapsed"), _("remaining")][self.mode] 1.277 + app.status(_("Counting %s time") % tmp, 1) 1.278 + self.update() 1.279 + 1.280 +# ------------------------------------------ 1.281 +class RootWindow(Window): 1.282 + def __init__(self, parent): 1.283 + Window.__init__(self, parent) 1.284 + keymap = Keymap() 1.285 + app.keymapstack.push(keymap) 1.286 + self.win_progress = ProgressWindow(self) 1.287 + self.win_status = StatusWindow(self) 1.288 + self.win_counter = CounterWindow(self) 1.289 + self.win_tab = TabWindow(self) 1.290 + keymap.bind(12, self.update, ()) # C-l 1.291 + keymap.bind([curses.KEY_LEFT, 2], app.seek, (-1, 1)) # C-b 1.292 + keymap.bind([curses.KEY_RIGHT, 6], app.seek, (1, 1)) # C-f 1.293 + keymap.bind([1, '^'], app.seek, (0, 0)) # C-a 1.294 + keymap.bind([5, '$'], app.seek, (-1, 0)) # C-e 1.295 + keymap.bind(range(48,58), app.key_volume) # 0123456789 1.296 + keymap.bind(['+', '='], app.inc_volume, ()) 1.297 + keymap.bind('-', app.dec_volume, ()) 1.298 + keymap.bind('n', app.next_song, ()) 1.299 + keymap.bind('p', app.prev_song, ()) 1.300 + keymap.bind('z', app.toggle_pause, ()) 1.301 + keymap.bind('x', app.toggle_stop, ()) 1.302 + keymap.bind('c', self.win_counter.toggle_mode, ()) 1.303 + keymap.bind('Q', app.quit, ()) 1.304 + keymap.bind('q', self.command_quit, ()) 1.305 + keymap.bind('v', app.mixer, ("toggle",)) 1.306 + 1.307 + def command_quit(self): 1.308 + app.do_input_hook = self.do_quit 1.309 + app.start_input(_("Quit? (y/N)")) 1.310 + 1.311 + def do_quit(self, ch): 1.312 + if chr(ch) == 'y': app.quit() 1.313 + app.stop_input() 1.314 + 1.315 +# ------------------------------------------ 1.316 +class TabWindow(Window): 1.317 + def __init__(self, parent): 1.318 + Window.__init__(self, parent) 1.319 + self.active_child = 0 1.320 + 1.321 + self.win_filelist = self.add(FilelistWindow) 1.322 + self.win_playlist = self.add(PlaylistWindow) 1.323 + self.win_help = self.add(HelpWindow) 1.324 + 1.325 + keymap = Keymap() 1.326 + keymap.bind('\t', self.change_window, ()) # tab 1.327 + keymap.bind('h', self.help, ()) 1.328 + app.keymapstack.push(keymap) 1.329 + app.keymapstack.push(self.children[self.active_child].keymap) 1.330 + 1.331 + def newwin(self): 1.332 + return curses.newwin(self.parent.rows-2, self.parent.cols, 0, 0) 1.333 + 1.334 + def update(self): 1.335 + self.update_title() 1.336 + self.move(1, 0) 1.337 + self.hline(ord('-'), self.cols) 1.338 + self.move(2, 0) 1.339 + self.clrtobot() 1.340 + self.refresh() 1.341 + child = self.children[self.active_child] 1.342 + child.visible = 1 1.343 + child.update() 1.344 + 1.345 + def update_title(self, refresh = 1): 1.346 + child = self.children[self.active_child] 1.347 + self.move(0, 0) 1.348 + self.clrtoeol() 1.349 + self.attron(curses.A_BOLD) 1.350 + self.insstr(child.get_title()) 1.351 + self.attroff(curses.A_BOLD) 1.352 + if refresh: self.refresh() 1.353 + 1.354 + def add(self, Class): 1.355 + win = Class(self) 1.356 + win.visible = 0 1.357 + return win 1.358 + 1.359 + def change_window(self, window = None): 1.360 + app.keymapstack.pop() 1.361 + self.children[self.active_child].visible = 0 1.362 + if window: 1.363 + self.active_child = self.children.index(window) 1.364 + else: 1.365 + # toggle windows 0 and 1 1.366 + self.active_child = not self.active_child 1.367 + app.keymapstack.push(self.children[self.active_child].keymap) 1.368 + self.update() 1.369 + 1.370 + def help(self): 1.371 + if self.children[self.active_child] == self.win_help: 1.372 + self.change_window(self.win_last) 1.373 + else: 1.374 + self.win_last = self.children[self.active_child] 1.375 + self.change_window(self.win_help) 1.376 + app.status(__version__, 2) 1.377 + 1.378 +# ------------------------------------------ 1.379 +class ListWindow(Window): 1.380 + def __init__(self, parent): 1.381 + Window.__init__(self, parent) 1.382 + self.buffer = [] 1.383 + self.bufptr = self.scrptr = 0 1.384 + self.search_direction = 0 1.385 + self.last_search = "" 1.386 + self.hoffset = 0 1.387 + self.keymap = Keymap() 1.388 + self.keymap.bind(['k', curses.KEY_UP, 16], self.cursor_move, (-1,)) 1.389 + self.keymap.bind(['j', curses.KEY_DOWN, 14], self.cursor_move, (1,)) 1.390 + self.keymap.bind(['K', curses.KEY_PPAGE], self.cursor_ppage, ()) 1.391 + self.keymap.bind(['J', curses.KEY_NPAGE], self.cursor_npage, ()) 1.392 + self.keymap.bind(['g', curses.KEY_HOME], self.cursor_home, ()) 1.393 + self.keymap.bind(['G', curses.KEY_END], self.cursor_end, ()) 1.394 + self.keymap.bind(['?', 18], self.start_search, 1.395 + (_("backward-isearch"), -1)) 1.396 + self.keymap.bind(['/', 19], self.start_search, 1.397 + (_("forward-isearch"), 1)) 1.398 + self.keymap.bind(['>'], self.hscroll, (8,)) 1.399 + self.keymap.bind(['<'], self.hscroll, (-8,)) 1.400 + 1.401 + def newwin(self): 1.402 + return curses.newwin(self.parent.rows-2, self.parent.cols, 1.403 + self.parent.ypos+2, self.parent.xpos) 1.404 + 1.405 + def update(self, force = 1): 1.406 + self.bufptr = max(0, min(self.bufptr, len(self.buffer) - 1)) 1.407 + scrptr = (self.bufptr / self.rows) * self.rows 1.408 + if force or self.scrptr != scrptr: 1.409 + self.scrptr = scrptr 1.410 + self.move(0, 0) 1.411 + self.clrtobot() 1.412 + i = 0 1.413 + for entry in self.buffer[self.scrptr:]: 1.414 + self.move(i, 0) 1.415 + i = i + 1 1.416 + self.putstr(entry) 1.417 + if self.getyx()[0] == self.rows - 1: break 1.418 + if self.visible: 1.419 + self.refresh() 1.420 + self.parent.update_title() 1.421 + self.update_line(curses.A_REVERSE) 1.422 + 1.423 + def update_line(self, attr = None, refresh = 1): 1.424 + if not self.buffer: return 1.425 + ypos = self.bufptr - self.scrptr 1.426 + if attr: self.attron(attr) 1.427 + self.move(ypos, 0) 1.428 + self.hline(ord(' '), self.cols) 1.429 + self.putstr(self.current()) 1.430 + if attr: self.attroff(attr) 1.431 + if self.visible and refresh: self.refresh() 1.432 + 1.433 + def get_title(self, data=""): 1.434 + pos = "%s-%s/%s" % (self.scrptr+min(1, len(self.buffer)), 1.435 + min(self.scrptr+self.rows, len(self.buffer)), 1.436 + len(self.buffer)) 1.437 + width = self.cols-len(pos)-2 1.438 + data = cut(data, width-len(self.name), 1) 1.439 + return "%-*s %s" % (width, cut(self.name+data, width), pos) 1.440 + 1.441 + def putstr(self, entry, *pos): 1.442 + s = string.translate(str(entry), Window.translationTable) 1.443 + pos and apply(self.move, pos) 1.444 + if self.hoffset: s = "<%s" % s[self.hoffset+1:] 1.445 + self.insstr(cut(s, self.cols)) 1.446 + 1.447 + def current(self): 1.448 + if self.bufptr >= len(self.buffer): self.bufptr = len(self.buffer) - 1 1.449 + return self.buffer[self.bufptr] 1.450 + 1.451 + def cursor_move(self, ydiff): 1.452 + if app.input_mode: app.cancel_input() 1.453 + if not self.buffer: return 1.454 + self.update_line(refresh = 0) 1.455 + self.bufptr = (self.bufptr + ydiff) % len(self.buffer) 1.456 + self.update(force = 0) 1.457 + 1.458 + def cursor_ppage(self): 1.459 + tmp = self.bufptr % self.rows 1.460 + if tmp == self.bufptr: 1.461 + self.cursor_move(-(tmp+(len(self.buffer) % self.rows) or self.rows)) 1.462 + else: 1.463 + self.cursor_move(-(tmp+self.rows)) 1.464 + 1.465 + def cursor_npage(self): 1.466 + tmp = self.rows - self.bufptr % self.rows 1.467 + if self.bufptr + tmp > len(self.buffer): 1.468 + self.cursor_move(len(self.buffer) - self.bufptr) 1.469 + else: 1.470 + self.cursor_move(tmp) 1.471 + 1.472 + def cursor_home(self): self.cursor_move(-self.bufptr) 1.473 + 1.474 + def cursor_end(self): self.cursor_move(-self.bufptr - 1) 1.475 + 1.476 + def start_search(self, type, direction): 1.477 + self.search_direction = direction 1.478 + self.not_found = 0 1.479 + if app.input_mode: 1.480 + app.input_prompt = "%s: " % type 1.481 + self.do_search(advance = direction) 1.482 + else: 1.483 + app.do_input_hook = self.do_search 1.484 + app.stop_input_hook = self.stop_search 1.485 + app.start_input(type) 1.486 + 1.487 + def stop_search(self): 1.488 + self.last_search = app.input_string 1.489 + app.status(_("ok"), 1) 1.490 + 1.491 + def do_search(self, ch = None, advance = 0): 1.492 + if ch in [8, 127]: app.input_string = app.input_string[:-1] 1.493 + elif ch: app.input_string = "%s%c" % (app.input_string, ch) 1.494 + else: app.input_string = app.input_string or self.last_search 1.495 + index = self.bufptr + advance 1.496 + while 1: 1.497 + if not 0 <= index < len(self.buffer): 1.498 + app.status(_("Not found: %s ") % app.input_string) 1.499 + self.not_found = 1 1.500 + break 1.501 + line = string.lower(str(self.buffer[index])) 1.502 + if string.find(line, string.lower(app.input_string)) != -1: 1.503 + app.show_input() 1.504 + self.update_line(refresh = 0) 1.505 + self.bufptr = index 1.506 + self.update(force = 0) 1.507 + self.not_found = 0 1.508 + break 1.509 + if self.not_found: 1.510 + app.status(_("Not found: %s ") % app.input_string) 1.511 + break 1.512 + index = index + self.search_direction 1.513 + 1.514 + def hscroll(self, value): 1.515 + self.hoffset = max(0, self.hoffset + value) 1.516 + self.update() 1.517 + 1.518 +# ------------------------------------------ 1.519 +class HelpWindow(ListWindow): 1.520 + def __init__(self, parent): 1.521 + ListWindow.__init__(self, parent) 1.522 + self.name = _("Help") 1.523 + self.keymap.bind('q', self.parent.help, ()) 1.524 + self.buffer = string.split(_("""\ 1.525 + Global t, T : tag current/regex 1.526 + ------ u, U : untag current/regex 1.527 + Up, Down, k, j, C-p, C-n, Sp, i : invert current/all 1.528 + PgUp, PgDn, K, J, ! : shell ($@ = tagged or current) 1.529 + Home, End, g, G : movement 1.530 + Enter : chdir or play Filelist 1.531 + Tab : filelist/playlist -------- 1.532 + n, p : next/prev track a : add (tagged) to playlist 1.533 + z, x : toggle pause/stop s : recursive search 1.534 + BS, o : goto parent/specified dir 1.535 + Left, Right, m, ' : set/get bookmark 1.536 + C-f, C-b : seek forward/backward 1.537 + C-a, C-e : restart/end track Playlist 1.538 + C-s, C-r, / : isearch -------- 1.539 + C-g, Esc : cancel d, D : delete (tagged) tracks/playlist 1.540 + 1..9, +, - : volume control m, M : move tagged tracks after/before 1.541 + c, v : counter/volume mode r, R : toggle repeat/Random mode 1.542 + <, > : horizontal scrolling s, S : shuffle/Sort playlist 1.543 + C-l, l : refresh, list mode w, @ : write playlist, jump to active 1.544 + h, q, Q : help, quit?, Quit! X : stop playlist after each track 1.545 +"""), "\n") 1.546 + 1.547 +# ------------------------------------------ 1.548 +class ListEntry: 1.549 + def __init__(self, pathname, dir=0): 1.550 + self.filename = os.path.basename(pathname) 1.551 + self.pathname = pathname 1.552 + self.slash = dir and "/" or "" 1.553 + self.tagged = 0 1.554 + 1.555 + def set_tagged(self, value): 1.556 + self.tagged = value 1.557 + 1.558 + def is_tagged(self): 1.559 + return self.tagged == 1 1.560 + 1.561 + def __str__(self): 1.562 + mark = self.is_tagged() and "#" or " " 1.563 + return "%s %s%s" % (mark, self.vp(), self.slash) 1.564 + 1.565 + def vp(self): 1.566 + return self.vps[0][1](self) 1.567 + 1.568 + def vp_filename(self): 1.569 + return self.filename or self.pathname 1.570 + 1.571 + def vp_pathname(self): 1.572 + return self.pathname 1.573 + 1.574 + vps = [[_("filename"), vp_filename], 1.575 + [_("pathname"), vp_pathname]] 1.576 + 1.577 +# ------------------------------------------ 1.578 +class PlaylistEntry(ListEntry): 1.579 + def __init__(self, pathname): 1.580 + ListEntry.__init__(self, pathname) 1.581 + self.metadata = None 1.582 + self.active = 0 1.583 + 1.584 + def set_active(self, value): 1.585 + self.active = value 1.586 + 1.587 + def is_active(self): 1.588 + return self.active == 1 1.589 + 1.590 + def vp_metadata(self): 1.591 + return self.metadata or self.read_metadata() 1.592 + 1.593 + def read_metadata(self): 1.594 + self.metadata = get_tag(self.pathname) 1.595 + return self.metadata 1.596 + 1.597 + vps = ListEntry.vps[:] 1.598 + 1.599 +# ------------------------------------------ 1.600 +class TagListWindow(ListWindow): 1.601 + def __init__(self, parent): 1.602 + ListWindow.__init__(self, parent) 1.603 + self.keymap.bind(' ', self.command_tag_untag, ()) 1.604 + self.keymap.bind('i', self.command_invert_tags, ()) 1.605 + self.keymap.bind('t', self.command_tag, (1,)) 1.606 + self.keymap.bind('u', self.command_tag, (0,)) 1.607 + self.keymap.bind('T', self.command_tag_regexp, (1,)) 1.608 + self.keymap.bind('U', self.command_tag_regexp, (0,)) 1.609 + self.keymap.bind('l', self.command_change_viewpoint, ()) 1.610 + 1.611 + def command_change_viewpoint(self, klass=ListEntry): 1.612 + klass.vps.append(klass.vps.pop(0)) 1.613 + app.status(_("Listing %s") % klass.vps[0][0], 1) 1.614 + app.player.update_status() 1.615 + self.update() 1.616 + 1.617 + def command_invert_tags(self): 1.618 + for i in self.buffer: 1.619 + i.set_tagged(not i.is_tagged()) 1.620 + self.update() 1.621 + 1.622 + def command_tag_untag(self): 1.623 + if not self.buffer: return 1.624 + tmp = self.buffer[self.bufptr] 1.625 + tmp.set_tagged(not tmp.is_tagged()) 1.626 + self.cursor_move(1) 1.627 + 1.628 + def command_tag(self, value): 1.629 + if not self.buffer: return 1.630 + self.buffer[self.bufptr].set_tagged(value) 1.631 + self.cursor_move(1) 1.632 + 1.633 + def command_tag_regexp(self, value): 1.634 + self.tag_value = value 1.635 + app.stop_input_hook = self.stop_tag_regexp 1.636 + app.start_input(value and _("Tag regexp") or _("Untag regexp")) 1.637 + 1.638 + def stop_tag_regexp(self): 1.639 + try: 1.640 + r = re.compile(app.input_string, re.I) 1.641 + for entry in self.buffer: 1.642 + if r.search(str(entry)): 1.643 + entry.set_tagged(self.tag_value) 1.644 + self.update() 1.645 + app.status(_("ok"), 1) 1.646 + except re.error, e: 1.647 + app.status(e, 2) 1.648 + 1.649 + def get_tagged(self): 1.650 + return filter(lambda x: x.is_tagged(), self.buffer) 1.651 + 1.652 + def not_tagged(self, l): 1.653 + return filter(lambda x: not x.is_tagged(), l) 1.654 + 1.655 +# ------------------------------------------ 1.656 +class FilelistWindow(TagListWindow): 1.657 + def __init__(self, parent): 1.658 + TagListWindow.__init__(self, parent) 1.659 + self.oldposition = {} 1.660 + try: self.chdir(os.getcwd()) 1.661 + except OSError: self.chdir(os.environ['HOME']) 1.662 + self.startdir = self.cwd 1.663 + self.mtime_when = 0 1.664 + self.mtime = None 1.665 + self.keymap.bind(['\n', curses.KEY_ENTER], 1.666 + self.command_chdir_or_play, ()) 1.667 + self.keymap.bind(['.', curses.KEY_BACKSPACE], 1.668 + self.command_chparentdir, ()) 1.669 + self.keymap.bind('a', self.command_add_recursively, ()) 1.670 + self.keymap.bind('o', self.command_goto, ()) 1.671 + self.keymap.bind('s', self.command_search_recursively, ()) 1.672 + self.keymap.bind('m', self.command_set_bookmark, ()) 1.673 + self.keymap.bind("'", self.command_get_bookmark, ()) 1.674 + self.keymap.bind('!', self.command_shell, ()) 1.675 + self.bookmarks = { 39: [self.cwd, 0] } 1.676 + 1.677 + def command_shell(self): 1.678 + if app.restricted: return 1.679 + app.stop_input_hook = self.stop_shell 1.680 + app.complete_input_hook = self.complete_shell 1.681 + app.start_input(_("shell$ "), colon=0) 1.682 + 1.683 + def stop_shell(self): 1.684 + s = app.input_string 1.685 + curses.endwin() 1.686 + sys.stderr.write("\n") 1.687 + argv = map(lambda x: x.pathname, self.get_tagged() or [self.current()]) 1.688 + argv = ["/bin/sh", "-c", s, "--"] + argv 1.689 + pid = os.fork() 1.690 + if pid == 0: 1.691 + try: os.execv(argv[0], argv) 1.692 + except: os._exit(1) 1.693 + pid, r = os.waitpid(pid, 0) 1.694 + sys.stderr.write("\nshell returned %s, press return!\n" % r) 1.695 + sys.stdin.readline() 1.696 + app.win_root.update() 1.697 + app.restore_default_status() 1.698 + app.cursor(0) 1.699 + 1.700 + def complete_shell(self, line): 1.701 + return self.complete_generic(line, quote=1) 1.702 + 1.703 + def complete_generic(self, line, quote=0): 1.704 + import glob 1.705 + if quote: 1.706 + s = re.sub('.*[^\\\\][ \'"()\[\]{}$`]', '', line) 1.707 + s, part = re.sub('\\\\', '', s), line[:len(line)-len(s)] 1.708 + else: 1.709 + s, part = line, "" 1.710 + results = glob.glob(os.path.expanduser(s)+"*") 1.711 + if len(results) == 0: 1.712 + return line 1.713 + if len(results) == 1: 1.714 + lm = results[0] 1.715 + lm = lm + (os.path.isdir(lm) and "/" or "") 1.716 + else: 1.717 + lm = results[0] 1.718 + for result in results: 1.719 + for i in range(min(len(result), len(lm))): 1.720 + if result[i] != lm[i]: 1.721 + lm = lm[:i] 1.722 + break 1.723 + if quote: lm = re.sub('([ \'"()\[\]{}$`])', '\\\\\\1', lm) 1.724 + return part + lm 1.725 + 1.726 + def command_get_bookmark(self): 1.727 + app.do_input_hook = self.do_get_bookmark 1.728 + app.start_input(_("bookmark")) 1.729 + 1.730 + def do_get_bookmark(self, ch): 1.731 + app.input_string = ch 1.732 + bookmark = self.bookmarks.get(ch) 1.733 + if bookmark: 1.734 + self.bookmarks[39] = [self.cwd, self.bufptr] 1.735 + dir, pos = bookmark 1.736 + self.chdir(dir) 1.737 + self.listdir() 1.738 + self.bufptr = pos 1.739 + self.update() 1.740 + app.status(_("ok"), 1) 1.741 + else: 1.742 + app.status(_("Not found!"), 1) 1.743 + app.stop_input() 1.744 + 1.745 + def command_set_bookmark(self): 1.746 + app.do_input_hook = self.do_set_bookmark 1.747 + app.start_input(_("set bookmark")) 1.748 + 1.749 + def do_set_bookmark(self, ch): 1.750 + app.input_string = ch 1.751 + self.bookmarks[ch] = [self.cwd, self.bufptr] 1.752 + ch and app.status(_("ok"), 1) or app.stop_input() 1.753 + 1.754 + def command_search_recursively(self): 1.755 + app.stop_input_hook = self.stop_search_recursively 1.756 + app.start_input(_("search")) 1.757 + 1.758 + def stop_search_recursively(self): 1.759 + try: re_tmp = re.compile(app.input_string, re.I) 1.760 + except re.error, e: 1.761 + app.status(e, 2) 1.762 + return 1.763 + app.status(_("Searching...")) 1.764 + results = [] 1.765 + for entry in self.buffer: 1.766 + if entry.filename == "..": 1.767 + continue 1.768 + if re_tmp.search(entry.filename): 1.769 + results.append(entry) 1.770 + elif os.path.isdir(entry.pathname): 1.771 + try: self.search_recursively(re_tmp, entry.pathname, results) 1.772 + except: pass 1.773 + if not self.search_mode: 1.774 + self.chdir(os.path.join(self.cwd,_("search results"))) 1.775 + self.search_mode = 1 1.776 + self.buffer = results 1.777 + self.bufptr = 0 1.778 + self.parent.update_title() 1.779 + self.update() 1.780 + app.restore_default_status() 1.781 + 1.782 + def search_recursively(self, re_tmp, dir, results): 1.783 + for filename in os.listdir(dir): 1.784 + pathname = os.path.join(dir, filename) 1.785 + if re_tmp.search(filename): 1.786 + if os.path.isdir(pathname): 1.787 + results.append(ListEntry(pathname, 1)) 1.788 + elif VALID_PLAYLIST(filename) or VALID_SONG(filename): 1.789 + results.append(ListEntry(pathname)) 1.790 + elif os.path.isdir(pathname): 1.791 + self.search_recursively(re_tmp, pathname, results) 1.792 + 1.793 + def get_title(self): 1.794 + self.name = _("Filelist: ") 1.795 + return ListWindow.get_title(self, re.sub("/?$", "/", self.cwd)) 1.796 + 1.797 + def listdir_maybe(self, now=0): 1.798 + if now < self.mtime_when+2: return 1.799 + self.mtime_when = now 1.800 + self.oldposition[self.cwd] = self.bufptr 1.801 + try: self.mtime == os.stat(self.cwd)[8] or self.listdir(quiet=1) 1.802 + except os.error: pass 1.803 + 1.804 + def listdir(self, quiet=0, prevdir=None): 1.805 + quiet or app.status(_("Reading directory...")) 1.806 + self.search_mode = 0 1.807 + dirs = [] 1.808 + files = [] 1.809 + try: 1.810 + self.mtime = os.stat(self.cwd)[8] 1.811 + self.mtime_when = time.time() 1.812 + filenames = os.listdir(self.cwd) 1.813 + filenames.sort() 1.814 + for filename in filenames: 1.815 + if filename[0] == ".": continue 1.816 + pathname = os.path.join(self.cwd, filename) 1.817 + if os.path.isdir(pathname): dirs.append(pathname) 1.818 + elif VALID_SONG(filename): files.append(pathname) 1.819 + elif VALID_PLAYLIST(filename): files.append(pathname) 1.820 + except os.error: pass 1.821 + dots = ListEntry(os.path.join(self.cwd, ".."), 1) 1.822 + self.buffer = [[dots], []][self.cwd == "/"] 1.823 + for i in dirs: self.buffer.append(ListEntry(i, 1)) 1.824 + for i in files: self.buffer.append(ListEntry(i)) 1.825 + if prevdir: 1.826 + for self.bufptr in range(len(self.buffer)): 1.827 + if self.buffer[self.bufptr].filename == prevdir: break 1.828 + else: self.bufptr = 0 1.829 + elif self.oldposition.has_key(self.cwd): 1.830 + self.bufptr = self.oldposition[self.cwd] 1.831 + else: self.bufptr = 0 1.832 + self.parent.update_title() 1.833 + self.update() 1.834 + quiet or app.restore_default_status() 1.835 + 1.836 + def chdir(self, dir): 1.837 + if hasattr(self, "cwd"): self.oldposition[self.cwd] = self.bufptr 1.838 + self.cwd = os.path.normpath(dir) 1.839 + try: os.chdir(self.cwd) 1.840 + except: pass 1.841 + 1.842 + def command_chdir_or_play(self): 1.843 + if not self.buffer: return 1.844 + if self.current().filename == "..": 1.845 + self.command_chparentdir() 1.846 + elif os.path.isdir(self.current().pathname): 1.847 + self.chdir(self.current().pathname) 1.848 + self.listdir() 1.849 + elif VALID_SONG(self.current().filename): 1.850 + app.play(self.current()) 1.851 + 1.852 + def command_chparentdir(self): 1.853 + if app.restricted and self.cwd == self.startdir: return 1.854 + dir = os.path.basename(self.cwd) 1.855 + self.chdir(os.path.dirname(self.cwd)) 1.856 + self.listdir(prevdir=dir) 1.857 + 1.858 + def command_goto(self): 1.859 + if app.restricted: return 1.860 + app.stop_input_hook = self.stop_goto 1.861 + app.complete_input_hook = self.complete_generic 1.862 + app.start_input(_("goto")) 1.863 + 1.864 + def stop_goto(self): 1.865 + dir = os.path.expanduser(app.input_string) 1.866 + if dir[0] != '/': dir = os.path.join(self.cwd, dir) 1.867 + if not os.path.isdir(dir): 1.868 + app.status(_("Not a directory!"), 1) 1.869 + return 1.870 + self.chdir(dir) 1.871 + self.listdir() 1.872 + 1.873 + def command_add_recursively(self): 1.874 + l = self.get_tagged() 1.875 + if not l: 1.876 + app.win_playlist.add(self.current().pathname) 1.877 + self.cursor_move(1) 1.878 + return 1.879 + app.status(_("Adding tagged files"), 1) 1.880 + for entry in l: 1.881 + app.win_playlist.add(entry.pathname, quiet=1) 1.882 + entry.set_tagged(0) 1.883 + self.update() 1.884 + 1.885 +# ------------------------------------------ 1.886 +class PlaylistWindow(TagListWindow): 1.887 + def __init__(self, parent): 1.888 + TagListWindow.__init__(self, parent) 1.889 + self.pathname = None 1.890 + self.repeat = 0 1.891 + self.random = 0 1.892 + self.random_prev = [] 1.893 + self.random_next = [] 1.894 + self.random_left = [] 1.895 + self.stop = 0 1.896 + self.keymap.bind(['\n', curses.KEY_ENTER], 1.897 + self.command_play, ()) 1.898 + self.keymap.bind('d', self.command_delete, ()) 1.899 + self.keymap.bind('D', self.command_delete_all, ()) 1.900 + self.keymap.bind('m', self.command_move, (1,)) 1.901 + self.keymap.bind('M', self.command_move, (0,)) 1.902 + self.keymap.bind('s', self.command_shuffle, ()) 1.903 + self.keymap.bind('S', self.command_sort, ()) 1.904 + self.keymap.bind('r', self.command_toggle_repeat, ()) 1.905 + self.keymap.bind('R', self.command_toggle_random, ()) 1.906 + self.keymap.bind('X', self.command_toggle_stop, ()) 1.907 + self.keymap.bind('w', self.command_save_playlist, ()) 1.908 + self.keymap.bind('@', self.command_jump_to_active, ()) 1.909 + 1.910 + def command_change_viewpoint(self, klass=PlaylistEntry): 1.911 + if not globals().get("ID3"): 1.912 + try: 1.913 + global ID3, ogg, codecs 1.914 + import ID3, ogg.vorbis, codecs 1.915 + klass.vps.append([_("metadata"), klass.vp_metadata]) 1.916 + except ImportError: pass 1.917 + TagListWindow.command_change_viewpoint(self, klass) 1.918 + 1.919 + def get_title(self): 1.920 + space_out = lambda value, s: value and s or " "*len(s) 1.921 + self.name = _("Playlist %s %s %s") % ( 1.922 + space_out(self.repeat, _("[repeat]")), 1.923 + space_out(self.random, _("[random]")), 1.924 + space_out(self.stop, _("[stop]"))) 1.925 + return ListWindow.get_title(self) 1.926 + 1.927 + def append(self, item): 1.928 + self.buffer.append(item) 1.929 + if self.random: self.random_left.append(item) 1.930 + 1.931 + def add_dir(self, dir): 1.932 + filenames = os.listdir(dir) 1.933 + filenames.sort() 1.934 + subdirs = [] 1.935 + for filename in filenames: 1.936 + pathname = os.path.join(dir, filename) 1.937 + if VALID_SONG(filename): 1.938 + self.append(PlaylistEntry(pathname)) 1.939 + if os.path.isdir(pathname): 1.940 + subdirs.append(pathname) 1.941 + map(self.add_dir, subdirs) 1.942 + 1.943 + def add_m3u(self, line): 1.944 + if re.match("^(#.*)?$", line): return 1.945 + if re.match("^(/|http://)", line): 1.946 + self.append(PlaylistEntry(self.fix_url(line))) 1.947 + else: 1.948 + dirname = os.path.dirname(self.pathname) 1.949 + self.append(PlaylistEntry(os.path.join(dirname, line))) 1.950 + 1.951 + def add_pls(self, line): 1.952 + # todo - support title & length 1.953 + m = re.match("File(\d+)=(.*)", line) 1.954 + if m: self.append(PlaylistEntry(self.fix_url(m.group(2)))) 1.955 + 1.956 + def add_playlist(self, pathname): 1.957 + self.pathname = pathname 1.958 + if re.search("\.m3u$", pathname, re.I): f = self.add_m3u 1.959 + if re.search("\.pls$", pathname, re.I): f = self.add_pls 1.960 + file = open(pathname) 1.961 + map(f, map(string.strip, file.readlines())) 1.962 + file.close() 1.963 + 1.964 + def add(self, pathname, quiet=0): 1.965 + try: 1.966 + if os.path.isdir(pathname): 1.967 + app.status(_("Working...")) 1.968 + self.add_dir(pathname) 1.969 + elif VALID_PLAYLIST(pathname): 1.970 + self.add_playlist(pathname) 1.971 + else: 1.972 + pathname = self.fix_url(pathname) 1.973 + self.append(PlaylistEntry(pathname)) 1.974 + # todo - refactor 1.975 + filename = os.path.basename(pathname) or pathname 1.976 + quiet or app.status(_("Added: %s") % filename, 1) 1.977 + except Exception, e: 1.978 + app.status(e, 2) 1.979 + 1.980 + def fix_url(self, url): 1.981 + return re.sub("(http://[^/]+)/?(.*)", "\\1/\\2", url) 1.982 + 1.983 + def putstr(self, entry, *pos): 1.984 + if entry.is_active(): self.attron(curses.A_BOLD) 1.985 + apply(ListWindow.putstr, (self, entry) + pos) 1.986 + if entry.is_active(): self.attroff(curses.A_BOLD) 1.987 + 1.988 + def change_active_entry(self, direction): 1.989 + if not self.buffer: return 1.990 + old = self.get_active_entry() 1.991 + new = None 1.992 + if self.random: 1.993 + if direction > 0: 1.994 + if self.random_next: new = self.random_next.pop() 1.995 + elif self.random_left: pass 1.996 + elif self.repeat: self.random_left = self.buffer[:] 1.997 + else: return 1.998 + if not new: 1.999 + import random 1.1000 + new = random.choice(self.random_left) 1.1001 + self.random_left.remove(new) 1.1002 + try: self.random_prev.remove(new) 1.1003 + except ValueError: pass 1.1004 + self.random_prev.append(new) 1.1005 + else: 1.1006 + if len(self.random_prev) > 1: 1.1007 + self.random_next.append(self.random_prev.pop()) 1.1008 + new = self.random_prev[-1] 1.1009 + else: return 1.1010 + old and old.set_active(0) 1.1011 + elif old: 1.1012 + index = self.buffer.index(old)+direction 1.1013 + if not (0 <= index < len(self.buffer) or self.repeat): return 1.1014 + old.set_active(0) 1.1015 + new = self.buffer[index % len(self.buffer)] 1.1016 + else: 1.1017 + new = self.buffer[0] 1.1018 + new.set_active(1) 1.1019 + self.update() 1.1020 + return new 1.1021 + 1.1022 + def get_active_entry(self): 1.1023 + for entry in self.buffer: 1.1024 + if entry.is_active(): return entry 1.1025 + 1.1026 + def command_jump_to_active(self): 1.1027 + entry = self.get_active_entry() 1.1028 + if not entry: return 1.1029 + self.bufptr = self.buffer.index(entry) 1.1030 + self.update() 1.1031 + 1.1032 + def command_play(self): 1.1033 + if not self.buffer: return 1.1034 + entry = self.get_active_entry() 1.1035 + entry and entry.set_active(0) 1.1036 + entry = self.current() 1.1037 + entry.set_active(1) 1.1038 + self.update() 1.1039 + app.play(entry) 1.1040 + 1.1041 + def command_delete(self): 1.1042 + if not self.buffer: return 1.1043 + current_entry, n = self.current(), len(self.buffer) 1.1044 + self.buffer = self.not_tagged(self.buffer) 1.1045 + if n > len(self.buffer): 1.1046 + try: self.bufptr = self.buffer.index(current_entry) 1.1047 + except ValueError: pass 1.1048 + else: 1.1049 + current_entry.set_tagged(1) 1.1050 + del self.buffer[self.bufptr] 1.1051 + if self.random: 1.1052 + self.random_prev = self.not_tagged(self.random_prev) 1.1053 + self.random_next = self.not_tagged(self.random_next) 1.1054 + self.random_left = self.not_tagged(self.random_left) 1.1055 + self.update() 1.1056 + 1.1057 + def command_delete_all(self): 1.1058 + self.buffer = [] 1.1059 + self.random_prev = [] 1.1060 + self.random_next = [] 1.1061 + self.random_left = [] 1.1062 + app.status(_("Deleted playlist"), 1) 1.1063 + self.update() 1.1064 + 1.1065 + def command_move(self, after): 1.1066 + if not self.buffer: return 1.1067 + current_entry, l = self.current(), self.get_tagged() 1.1068 + if not l or current_entry.is_tagged(): return 1.1069 + self.buffer = self.not_tagged(self.buffer) 1.1070 + self.bufptr = self.buffer.index(current_entry)+after 1.1071 + self.buffer[self.bufptr:self.bufptr] = l 1.1072 + self.update() 1.1073 + 1.1074 + def command_shuffle(self): 1.1075 + import random 1.1076 + l = [] 1.1077 + n = len(self.buffer) 1.1078 + while n > 0: 1.1079 + n = n-1 1.1080 + r = random.randint(0, n) 1.1081 + l.append(self.buffer[r]) 1.1082 + del self.buffer[r] 1.1083 + self.buffer = l 1.1084 + self.bufptr = 0 1.1085 + self.update() 1.1086 + app.status(_("Shuffled playlist... Oops?"), 1) 1.1087 + 1.1088 + def command_sort(self): 1.1089 + app.status(_("Working...")) 1.1090 + self.buffer.sort(lambda x, y: x.vp() > y.vp() or -1) 1.1091 + self.bufptr = 0 1.1092 + self.update() 1.1093 + app.status(_("Sorted playlist"), 1) 1.1094 + 1.1095 + def command_toggle_repeat(self): 1.1096 + self.toggle("repeat", _("Repeat: %s")) 1.1097 + 1.1098 + def command_toggle_random(self): 1.1099 + self.toggle("random", _("Random: %s")) 1.1100 + self.random_prev = [] 1.1101 + self.random_next = [] 1.1102 + self.random_left = self.buffer[:] 1.1103 + 1.1104 + def command_toggle_stop(self): 1.1105 + self.toggle("stop", _("Stop playlist: %s")) 1.1106 + 1.1107 + def toggle(self, attr, format): 1.1108 + setattr(self, attr, not getattr(self, attr)) 1.1109 + app.status(format % (getattr(self, attr) and _("on") or _("off")), 1) 1.1110 + self.parent.update_title() 1.1111 + 1.1112 + def command_save_playlist(self): 1.1113 + if app.restricted: return 1.1114 + default = self.pathname or "%s/" % app.win_filelist.cwd 1.1115 + app.stop_input_hook = self.stop_save_playlist 1.1116 + app.start_input(_("Save playlist"), default) 1.1117 + 1.1118 + def stop_save_playlist(self): 1.1119 + pathname = app.input_string 1.1120 + if pathname[0] != '/': 1.1121 + pathname = os.path.join(app.win_filelist.cwd, pathname) 1.1122 + if not re.search("\.m3u$", pathname, re.I): 1.1123 + pathname = "%s%s" % (pathname, ".m3u") 1.1124 + try: 1.1125 + file = open(pathname, "w") 1.1126 + for entry in self.buffer: 1.1127 + file.write("%s\n" % entry.pathname) 1.1128 + file.close() 1.1129 + self.pathname = pathname 1.1130 + app.status(_("ok"), 1) 1.1131 + except IOError, e: 1.1132 + app.status(e, 2) 1.1133 + 1.1134 +# ------------------------------------------ 1.1135 +def get_tag(pathname): 1.1136 + if re.compile("^http://").match(pathname) or not os.path.exists(pathname): 1.1137 + return pathname 1.1138 + tags = {} 1.1139 + # FIXME: use magic instead of file extensions to identify OGGs and MP3s 1.1140 + if re.compile(".*\.ogg$", re.I).match(pathname): 1.1141 + try: 1.1142 + vf = ogg.vorbis.VorbisFile(pathname) 1.1143 + vc = vf.comment() 1.1144 + tags = vc.as_dict() 1.1145 + except NameError: pass 1.1146 + except (IOError, UnicodeError): return os.path.basename(pathname) 1.1147 + elif re.compile(".*\.mp3$", re.I).match(pathname): 1.1148 + try: 1.1149 + vc = ID3.ID3(pathname, as_tuple=1) 1.1150 + tags = vc.as_dict() 1.1151 + except NameError: pass 1.1152 + except (IOError, ID3.InvalidTagError): return os.path.basename(pathname) 1.1153 + else: 1.1154 + return os.path.basename(pathname) 1.1155 + 1.1156 + artist = tags.get("ARTIST", [""])[0] 1.1157 + title = tags.get("TITLE", [""])[0] 1.1158 + tag = os.path.basename(pathname) 1.1159 + try: 1.1160 + if artist and title: 1.1161 + tag = codecs.latin_1_encode(artist)[0] + " - " + codecs.latin_1_encode(title)[0] 1.1162 + elif artist: 1.1163 + tag = artist 1.1164 + elif title: 1.1165 + tag = title 1.1166 + return codecs.latin_1_encode(tag)[0] 1.1167 + except (NameError, UnicodeError): return tag 1.1168 + 1.1169 +# ------------------------------------------ 1.1170 +class Player: 1.1171 + def __init__(self, commandline, files, fps=1): 1.1172 + self.commandline = commandline 1.1173 + self.re_files = re.compile(files, re.I) 1.1174 + self.fps = fps 1.1175 + self.stdin_r, self.stdin_w = os.pipe() 1.1176 + self.stdout_r, self.stdout_w = os.pipe() 1.1177 + self.stderr_r, self.stderr_w = os.pipe() 1.1178 + self.entry = None 1.1179 + self.stopped = 0 1.1180 + self.paused = 0 1.1181 + self.time_setup = None 1.1182 + self.buf = '' 1.1183 + self.tid = None 1.1184 + 1.1185 + def setup(self, entry, offset): 1.1186 + self.argv = string.split(self.commandline) 1.1187 + self.argv[0] = which(self.argv[0]) 1.1188 + for i in range(len(self.argv)): 1.1189 + if self.argv[i] == "%s": self.argv[i] = entry.pathname 1.1190 + if self.argv[i] == "%d": self.argv[i] = str(offset*self.fps) 1.1191 + self.entry = entry 1.1192 + if offset == 0: 1.1193 + app.progress(0) 1.1194 + self.offset = 0 1.1195 + self.length = 0 1.1196 + self.values = [0, 0] 1.1197 + self.time_setup = time.time() 1.1198 + return self.argv[0] 1.1199 + 1.1200 + def play(self): 1.1201 + self.pid = os.fork() 1.1202 + if self.pid == 0: 1.1203 + os.dup2(self.stdin_w, sys.stdin.fileno()) 1.1204 + os.dup2(self.stdout_w, sys.stdout.fileno()) 1.1205 + os.dup2(self.stderr_w, sys.stderr.fileno()) 1.1206 + os.setpgrp() 1.1207 + try: os.execv(self.argv[0], self.argv) 1.1208 + except: os._exit(1) 1.1209 + self.stopped = 0 1.1210 + self.paused = 0 1.1211 + self.step = 0 1.1212 + self.update_status() 1.1213 + 1.1214 + def stop(self, quiet=0): 1.1215 + self.paused and self.toggle_pause(quiet) 1.1216 + try: 1.1217 + while 1: 1.1218 + try: os.kill(-self.pid, signal.SIGINT) 1.1219 + except os.error: pass 1.1220 + os.waitpid(self.pid, os.WNOHANG) 1.1221 + except Exception: pass 1.1222 + self.stopped = 1 1.1223 + quiet or self.update_status() 1.1224 + 1.1225 + def toggle_pause(self, quiet=0): 1.1226 + try: os.kill(-self.pid, [signal.SIGSTOP, signal.SIGCONT][self.paused]) 1.1227 + except os.error: return 1.1228 + self.paused = not self.paused 1.1229 + quiet or self.update_status() 1.1230 + 1.1231 + def parse_progress(self): 1.1232 + if self.stopped or self.step: self.tid = None 1.1233 + else: 1.1234 + self.parse_buf() 1.1235 + self.tid = app.timeout.add(1.0, self.parse_progress) 1.1236 + 1.1237 + def read_fd(self, fd): 1.1238 + self.buf = os.read(fd, 512) 1.1239 + self.tid or self.parse_progress() 1.1240 + 1.1241 + def poll(self): 1.1242 + try: os.waitpid(self.pid, os.WNOHANG) 1.1243 + except: 1.1244 + # something broken? try again 1.1245 + if self.time_setup and (time.time() - self.time_setup) < 2.0: 1.1246 + self.play() 1.1247 + return 0 1.1248 + app.set_default_status("") 1.1249 + app.counter([0,0]) 1.1250 + app.progress(0) 1.1251 + return 1 1.1252 + 1.1253 + def seek(self, offset, relative): 1.1254 + if relative: 1.1255 + d = offset * self.length * 0.002 1.1256 + self.step = self.step * (self.step * d > 0) + d 1.1257 + self.offset = min(self.length, max(0, self.offset+self.step)) 1.1258 + else: 1.1259 + self.step = 1 1.1260 + self.offset = (offset < 0) and self.length+offset or offset 1.1261 + self.show_position() 1.1262 + 1.1263 + def set_position(self, offset, length, values): 1.1264 + self.offset = offset 1.1265 + self.length = length 1.1266 + self.values = values 1.1267 + self.show_position() 1.1268 + 1.1269 + def show_position(self): 1.1270 + app.counter(self.values) 1.1271 + app.progress(self.length and (float(self.offset) / self.length)) 1.1272 + 1.1273 + def update_status(self): 1.1274 + if not self.entry: 1.1275 + app.set_default_status("") 1.1276 + elif self.stopped: 1.1277 + app.set_default_status(_("Stopped: %s") % self.entry.vp()) 1.1278 + elif self.paused: 1.1279 + app.set_default_status(_("Paused: %s") % self.entry.vp()) 1.1280 + else: 1.1281 + app.set_default_status(_("Playing: %s") % self.entry.vp()) 1.1282 + 1.1283 +# ------------------------------------------ 1.1284 +class FrameOffsetPlayer(Player): 1.1285 + re_progress = re.compile("Time.*\s(\d+):(\d+).*\[(\d+):(\d+)") 1.1286 + 1.1287 + def parse_buf(self): 1.1288 + match = self.re_progress.search(self.buf) 1.1289 + if match: 1.1290 + m1, s1, m2, s2 = map(string.atoi, match.groups()) 1.1291 + head, tail = m1*60+s1, m2*60+s2 1.1292 + self.set_position(head, head+tail, [head, tail]) 1.1293 + 1.1294 +# ------------------------------------------ 1.1295 +class TimeOffsetPlayer(Player): 1.1296 + re_progress = re.compile("(\d+):(\d+):(\d+)") 1.1297 + 1.1298 + def parse_buf(self): 1.1299 + match = self.re_progress.search(self.buf) 1.1300 + if match: 1.1301 + h, m, s = map(string.atoi, match.groups()) 1.1302 + tail = h*3600+m*60+s 1.1303 + head = max(self.length, tail) - tail 1.1304 + self.set_position(head, head+tail, [head, tail]) 1.1305 + 1.1306 +# ------------------------------------------ 1.1307 +class NoOffsetPlayer(Player): 1.1308 + 1.1309 + def parse_buf(self): 1.1310 + head = self.offset+1 1.1311 + self.set_position(head, 0, [head, head]) 1.1312 + 1.1313 + def seek(self, *dummy): 1.1314 + return 1 1.1315 + 1.1316 +# ------------------------------------------ 1.1317 +class Timeout: 1.1318 + def __init__(self): 1.1319 + self.next = 0 1.1320 + self.dict = {} 1.1321 + 1.1322 + def add(self, timeout, func, args=()): 1.1323 + tid = self.next = self.next + 1 1.1324 + self.dict[tid] = (func, args, time.time() + timeout) 1.1325 + return tid 1.1326 + 1.1327 + def remove(self, tid): 1.1328 + del self.dict[tid] 1.1329 + 1.1330 + def check(self, now): 1.1331 + for tid, (func, args, timeout) in self.dict.items(): 1.1332 + if now >= timeout: 1.1333 + self.remove(tid) 1.1334 + apply(func, args) 1.1335 + return len(self.dict) and 0.2 or None 1.1336 + 1.1337 +# ------------------------------------------ 1.1338 +class FIFOControl: 1.1339 + def __init__(self): 1.1340 + try: self.fd = open(CONTROL_FIFO, "rb+", 0) 1.1341 + except: self.fd = None 1.1342 + self.commands = {"pause" : app.toggle_pause, 1.1343 + "next" : app.next_song, 1.1344 + "prev" : app.prev_song, 1.1345 + "forward" : self.forward, 1.1346 + "backward" : self.backward, 1.1347 + "play" : app.toggle_stop, 1.1348 + "stop" : app.toggle_stop, 1.1349 + "volup" : app.inc_volume, 1.1350 + "voldown" : app.dec_volume, 1.1351 + "quit" : app.quit} 1.1352 + 1.1353 + def handle_command(self): 1.1354 + command = string.strip(self.fd.readline()) 1.1355 + if command in self.commands.keys(): 1.1356 + self.commands[command]() 1.1357 + 1.1358 + def forward(self): 1.1359 + app.seek(1, 1) 1.1360 + 1.1361 + def backward(self): 1.1362 + app.seek(-1, 1) 1.1363 + 1.1364 +# ------------------------------------------ 1.1365 +class Application: 1.1366 + def __init__(self): 1.1367 + self.keymapstack = KeymapStack() 1.1368 + self.input_mode = 0 1.1369 + self.input_prompt = "" 1.1370 + self.input_string = "" 1.1371 + self.do_input_hook = None 1.1372 + self.stop_input_hook = None 1.1373 + self.complete_input_hook = None 1.1374 + self.channels = [] 1.1375 + self.restricted = 0 1.1376 + self.input_keymap = Keymap() 1.1377 + self.input_keymap.bind(list(Window.chars), self.do_input) 1.1378 + self.input_keymap.bind(curses.KEY_BACKSPACE, self.do_input, (8,)) 1.1379 + self.input_keymap.bind([21, 23], self.do_input) 1.1380 + self.input_keymap.bind(['\a', 27], self.cancel_input, ()) 1.1381 + self.input_keymap.bind(['\n', curses.KEY_ENTER], 1.1382 + self.stop_input, ()) 1.1383 + 1.1384 + def setup(self): 1.1385 + if tty: 1.1386 + self.tcattr = tty.tcgetattr(sys.stdin.fileno()) 1.1387 + tcattr = tty.tcgetattr(sys.stdin.fileno()) 1.1388 + tcattr[0] = tcattr[0] & ~(tty.IXON) 1.1389 + tty.tcsetattr(sys.stdin.fileno(), tty.TCSANOW, tcattr) 1.1390 + self.w = curses.initscr() 1.1391 + curses.cbreak() 1.1392 + curses.noecho() 1.1393 + try: curses.meta(1) 1.1394 + except: pass 1.1395 + self.cursor(0) 1.1396 + signal.signal(signal.SIGCHLD, signal.SIG_IGN) 1.1397 + signal.signal(signal.SIGHUP, self.handler_quit) 1.1398 + signal.signal(signal.SIGINT, self.handler_quit) 1.1399 + signal.signal(signal.SIGTERM, self.handler_quit) 1.1400 + signal.signal(signal.SIGWINCH, self.handler_resize) 1.1401 + self.win_root = RootWindow(None) 1.1402 + self.win_root.update() 1.1403 + self.win_tab = self.win_root.win_tab 1.1404 + self.win_filelist = self.win_root.win_tab.win_filelist 1.1405 + self.win_playlist = self.win_root.win_tab.win_playlist 1.1406 + self.win_status = self.win_root.win_status 1.1407 + self.status = self.win_status.status 1.1408 + self.set_default_status = self.win_status.set_default_status 1.1409 + self.restore_default_status = self.win_status.restore_default_status 1.1410 + self.counter = self.win_root.win_counter.counter 1.1411 + self.progress = self.win_root.win_progress.progress 1.1412 + self.player = PLAYERS[0] 1.1413 + self.timeout = Timeout() 1.1414 + self.play_tid = None 1.1415 + self.kludge = 0 1.1416 + self.win_filelist.listdir() 1.1417 + self.control = FIFOControl() 1.1418 + 1.1419 + def cleanup(self): 1.1420 + try: curses.endwin() 1.1421 + except curses.error: return 1.1422 + XTERM and sys.stderr.write("\033]0;%s\a" % "xterm") 1.1423 + tty and tty.tcsetattr(sys.stdin.fileno(), tty.TCSADRAIN, self.tcattr) 1.1424 + print 1.1425 + 1.1426 + def run(self): 1.1427 + while 1: 1.1428 + now = time.time() 1.1429 + timeout = self.timeout.check(now) 1.1430 + self.win_filelist.listdir_maybe(now) 1.1431 + if not self.player.stopped: 1.1432 + timeout = 0.5 1.1433 + if self.kludge and self.player.poll(): 1.1434 + self.player.stopped = 1 # end of playlist hack 1.1435 + if not self.win_playlist.stop: 1.1436 + entry = self.win_playlist.change_active_entry(1) 1.1437 + entry and self.play(entry) 1.1438 + R = [sys.stdin, self.player.stdout_r, self.player.stderr_r] 1.1439 + self.control.fd and R.append(self.control.fd) 1.1440 + try: r, w, e = select.select(R, [], [], timeout) 1.1441 + except select.error: continue 1.1442 + self.kludge = 1 1.1443 + # user 1.1444 + if sys.stdin in r: 1.1445 + c = self.win_root.getch() 1.1446 + self.keymapstack.process(c) 1.1447 + # player 1.1448 + if self.player.stderr_r in r: 1.1449 + self.player.read_fd(self.player.stderr_r) 1.1450 + # player 1.1451 + if self.player.stdout_r in r: 1.1452 + self.player.read_fd(self.player.stdout_r) 1.1453 + # remote 1.1454 + if self.control.fd in r: 1.1455 + self.control.handle_command() 1.1456 + 1.1457 + def play(self, entry, offset = 0): 1.1458 + self.kludge = 0 1.1459 + self.play_tid = None 1.1460 + if entry is None or offset is None: return 1.1461 + self.player.stop(quiet=1) 1.1462 + for self.player in PLAYERS: 1.1463 + if self.player.re_files.search(entry.pathname): 1.1464 + if self.player.setup(entry, offset): break 1.1465 + else: 1.1466 + app.status(_("Player not found!"), 1) 1.1467 + self.player.stopped = 0 # keep going 1.1468 + return 1.1469 + self.player.play() 1.1470 + 1.1471 + def delayed_play(self, entry, offset): 1.1472 + if self.play_tid: self.timeout.remove(self.play_tid) 1.1473 + self.play_tid = self.timeout.add(0.5, self.play, (entry, offset)) 1.1474 + 1.1475 + def next_song(self): 1.1476 + self.delayed_play(self.win_playlist.change_active_entry(1), 0) 1.1477 + 1.1478 + def prev_song(self): 1.1479 + self.delayed_play(self.win_playlist.change_active_entry(-1), 0) 1.1480 + 1.1481 + def seek(self, offset, relative): 1.1482 + if not self.player.entry: return 1.1483 + self.player.seek(offset, relative) 1.1484 + self.delayed_play(self.player.entry, self.player.offset) 1.1485 + 1.1486 + def toggle_pause(self): 1.1487 + if not self.player.entry: return 1.1488 + if not self.player.stopped: self.player.toggle_pause() 1.1489 + 1.1490 + def toggle_stop(self): 1.1491 + if not self.player.entry: return 1.1492 + if not self.player.stopped: self.player.stop() 1.1493 + else: self.play(self.player.entry, self.player.offset) 1.1494 + 1.1495 + def inc_volume(self): 1.1496 + self.mixer("cue", 1) 1.1497 + 1.1498 + def dec_volume(self): 1.1499 + self.mixer("cue", -1) 1.1500 + 1.1501 + def key_volume(self, ch): 1.1502 + self.mixer("set", (ch & 0x0f)*10) 1.1503 + 1.1504 + def mixer(self, cmd=None, arg=None): 1.1505 + try: self._mixer(cmd, arg) 1.1506 + except Exception, e: app.status(e, 2) 1.1507 + 1.1508 + def _mixer(self, cmd, arg): 1.1509 + try: 1.1510 + import ossaudiodev 1.1511 + mixer = ossaudiodev.openmixer() 1.1512 + get, set = mixer.get, mixer.set 1.1513 + self.channels = self.channels or \ 1.1514 + [['MASTER', ossaudiodev.SOUND_MIXER_VOLUME], 1.1515 + ['PCM', ossaudiodev.SOUND_MIXER_PCM]] 1.1516 + except ImportError: 1.1517 + import oss 1.1518 + mixer = oss.open_mixer() 1.1519 + get, set = mixer.read_channel, mixer.write_channel 1.1520 + self.channels = self.channels or \ 1.1521 + [['MASTER', oss.SOUND_MIXER_VOLUME], 1.1522 + ['PCM', oss.SOUND_MIXER_PCM]] 1.1523 + if cmd is "toggle": self.channels.insert(0, self.channels.pop()) 1.1524 + name, channel = self.channels[0] 1.1525 + if cmd is "cue": arg = min(100, max(0, get(channel)[0] + arg)) 1.1526 + if cmd in ["set", "cue"]: set(channel, (arg, arg)) 1.1527 + app.status(_("%s volume %s%%") % (name, get(channel)[0]), 1) 1.1528 + mixer.close() 1.1529 + 1.1530 + def show_input(self): 1.1531 + n = len(self.input_prompt)+1 1.1532 + s = cut(self.input_string, self.win_status.cols-n, left=1) 1.1533 + app.status("%s%s " % (self.input_prompt, s)) 1.1534 + 1.1535 + def start_input(self, prompt="", data="", colon=1): 1.1536 + self.input_mode = 1 1.1537 + self.cursor(1) 1.1538 + app.keymapstack.push(self.input_keymap) 1.1539 + self.input_prompt = prompt + (colon and ": " or "") 1.1540 + self.input_string = data 1.1541 + self.show_input() 1.1542 + 1.1543 + def do_input(self, *args): 1.1544 + if self.do_input_hook: 1.1545 + return apply(self.do_input_hook, args) 1.1546 + ch = args and args[0] or None 1.1547 + if ch in [8, 127]: # backspace 1.1548 + self.input_string = self.input_string[:-1] 1.1549 + elif ch == 9 and self.complete_input_hook: 1.1550 + self.input_string = self.complete_input_hook(self.input_string) 1.1551 + elif ch == 21: # C-u 1.1552 + self.input_string = "" 1.1553 + elif ch == 23: # C-w 1.1554 + self.input_string = re.sub("((.* )?)\w.*", "\\1", self.input_string) 1.1555 + elif ch: 1.1556 + self.input_string = "%s%c" % (self.input_string, ch) 1.1557 + self.show_input() 1.1558 + 1.1559 + def stop_input(self, *args): 1.1560 + self.input_mode = 0 1.1561 + self.cursor(0) 1.1562 + app.keymapstack.pop() 1.1563 + if not self.input_string: 1.1564 + app.status(_("cancel"), 1) 1.1565 + elif self.stop_input_hook: 1.1566 + apply(self.stop_input_hook, args) 1.1567 + self.do_input_hook = None 1.1568 + self.stop_input_hook = None 1.1569 + self.complete_input_hook = None 1.1570 + 1.1571 + def cancel_input(self): 1.1572 + self.input_string = "" 1.1573 + self.stop_input() 1.1574 + 1.1575 + def cursor(self, visibility): 1.1576 + try: curses.curs_set(visibility) 1.1577 + except: pass 1.1578 + 1.1579 + def quit(self): 1.1580 + self.player.stop(quiet=1) 1.1581 + sys.exit(0) 1.1582 + 1.1583 + def handler_resize(self, sig, frame): 1.1584 + # curses trickery 1.1585 + while 1: 1.1586 + try: curses.endwin(); break 1.1587 + except: time.sleep(1) 1.1588 + self.w.refresh() 1.1589 + self.win_root.resize() 1.1590 + self.win_root.update() 1.1591 + 1.1592 + def handler_quit(self, sig, frame): 1.1593 + self.quit() 1.1594 + 1.1595 +# ------------------------------------------ 1.1596 +def main(): 1.1597 + try: 1.1598 + opts, args = getopt.getopt(sys.argv[1:], "nrRv") 1.1599 + except: 1.1600 + usage = _("Usage: %s [-nrRv] [ file | dir | playlist ] ...\n") 1.1601 + sys.stderr.write(usage % sys.argv[0]) 1.1602 + sys.exit(1) 1.1603 + 1.1604 + global app 1.1605 + app = Application() 1.1606 + 1.1607 + playlist = [] 1.1608 + if not sys.stdin.isatty(): 1.1609 + playlist = map(string.strip, sys.stdin.readlines()) 1.1610 + os.close(0) 1.1611 + os.open("/dev/tty", 0) 1.1612 + try: 1.1613 + app.setup() 1.1614 + for opt, optarg in opts: 1.1615 + if opt == "-n": app.restricted = 1 1.1616 + if opt == "-r": app.win_playlist.command_toggle_repeat() 1.1617 + if opt == "-R": app.win_playlist.command_toggle_random() 1.1618 + if opt == "-v": app.mixer("toggle") 1.1619 + if args or playlist: 1.1620 + for i in args or playlist: 1.1621 + app.win_playlist.add(os.path.abspath(i)) 1.1622 + app.win_tab.change_window() 1.1623 + app.run() 1.1624 + except SystemExit: 1.1625 + app.cleanup() 1.1626 + except Exception: 1.1627 + app.cleanup() 1.1628 + import traceback 1.1629 + traceback.print_exc() 1.1630 + 1.1631 +# ------------------------------------------ 1.1632 +PLAYERS = [ 1.1633 + FrameOffsetPlayer("ogg123 -q -v -k %d %s", "\.ogg$"), 1.1634 + FrameOffsetPlayer("splay -f -k %d %s", "(^http://|\.mp[123]$)", 38.28), 1.1635 + FrameOffsetPlayer("mpg123 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28), 1.1636 + FrameOffsetPlayer("mpg321 -q -v -k %d %s", "(^http://|\.mp[123]$)", 38.28), 1.1637 + TimeOffsetPlayer("madplay -v --display-time=remaining -s %d %s", "\.mp[123]$"), 1.1638 + NoOffsetPlayer("mikmod -q -p0 %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm)$"), 1.1639 + NoOffsetPlayer("xmp -q %s", "\.(mod|xm|fm|s3m|med|col|669|it|mtm|stm)$"), 1.1640 + NoOffsetPlayer("play %s", "\.(aiff|au|cdr|mp3|ogg|wav)$"), 1.1641 + NoOffsetPlayer("speexdec %s", "\.spx$") 1.1642 + ] 1.1643 + 1.1644 +def VALID_SONG(name): 1.1645 + for player in PLAYERS: 1.1646 + if player.re_files.search(name): 1.1647 + return 1 1.1648 + 1.1649 +def VALID_PLAYLIST(name): 1.1650 + if re.search("\.(m3u|pls)$", name, re.I): 1.1651 + return 1 1.1652 + 1.1653 +for rc in [os.path.expanduser("~/.cplayrc"), "/etc/cplayrc"]: 1.1654 + try: execfile(rc); break 1.1655 + except IOError: pass 1.1656 + 1.1657 +# ------------------------------------------ 1.1658 +if __name__ == "__main__": main()