./decibel-audio-player-1.06/ 0000755 0001750 0001750 00000000000 11456551414 015765 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/ 0000755 0001750 0001750 00000000000 11456551413 016553 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/decibel-audio-player.py 0000755 0001750 0001750 00000023657 11456551413 023125 0 ustar ingelres ingelres #!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import dbus, optparse
from tools import consts
# Command line
optparser = optparse.OptionParser(usage='Usage: %prog [options] [FILE(s)]')
optparser.add_option('-p', '--playbin', action='store_true', default=False, help='use the playbin GStreamer component instead of playbin2')
optparser.add_option('--no-glossy-cover', action='store_true', default=False, help='disable the gloss effect applied to covers')
optparser.add_option('--multiple-instances', action='store_true', default=False, help='start a new instance even if one is already running')
(optOptions, optArgs) = optparser.parse_args()
# Check whether DAP is already running?
if not optOptions.multiple_instances:
shouldStop = False
dbusSession = None
try:
dbusSession = dbus.SessionBus()
activeServices = dbusSession.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus').ListNames()
if consts.dbusService in activeServices:
shouldStop = True
# Fill the current instance with the given tracks, if any
if len(optArgs) != 0:
dbus.Interface(dbusSession.get_object(consts.dbusService, '/TrackList'), consts.dbusInterface).SetTracks(optArgs, True)
except:
pass
if dbusSession is not None:
dbusSession.close()
if shouldStop:
import sys
sys.exit(1)
# Start a new instance
import gettext, gobject, gtk, locale
from tools import loadGladeFile, log, prefs
DEFAULT_VIEW_MODE = consts.VIEW_MODE_FULL
DEFAULT_PANED_POS = 300
DEFAULT_WIN_WIDTH = 750
DEFAULT_WIN_HEIGHT = 470
DEFAULT_MAXIMIZED_STATE = False
def realStartup():
"""
Perform all the initialization stuff which is not mandatory to display the window
This function should be called within the GTK main loop, once the window has been displayed
"""
import atexit, dbus.mainloop.glib, modules, signal
def onDelete(win, event):
""" Use our own quit sequence, that will itself destroy the window """
window.hide()
modules.postQuitMsg()
return True
def onResize(win, rect):
""" Save the new size of the window """
# The first label gets more or less a third of the window's width
wTree.get_widget('hbox-status1').set_size_request(rect.width / 3 + 15, -1)
# Save size and maximized state
if win.window is not None and not win.window.get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED:
prefs.set(__name__, 'win-width', rect.width)
prefs.set(__name__, 'win-height', rect.height)
if prefs.get(__name__, 'view-mode', DEFAULT_VIEW_MODE)in (consts.VIEW_MODE_FULL, consts.VIEW_MODE_PLAYLIST):
prefs.set(__name__, 'full-win-height', rect.height)
def onAbout(item):
""" Show the about dialog box """
import gui.about
gui.about.show(window)
def onHelp(item):
""" Show help page in the web browser """
import webbrowser
webbrowser.open(consts.urlHelp)
def onState(win, evt):
""" Save the new state of the window """
prefs.set(__name__, 'win-is-maximized', bool(evt.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
def atExit():
""" Final function, called just before exiting the Python interpreter """
prefs.save()
log.logger.info('Stopped')
# D-Bus
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
# Register a few handlers
atexit.register(atExit)
signal.signal(signal.SIGINT, lambda sig, frame: onDelete(window, None))
signal.signal(signal.SIGTERM, lambda sig, frame: onDelete(window, None))
# GTK handlers
window.connect('delete-event', onDelete)
window.connect('size-allocate', onResize)
window.connect('window-state-event', onState)
paned.connect('size-allocate', lambda win, rect: prefs.set(__name__, 'paned-pos', paned.get_position()))
wTree.get_widget('menu-mode-mini').connect('activate', onViewMode, consts.VIEW_MODE_MINI)
wTree.get_widget('menu-mode-full').connect('activate', onViewMode, consts.VIEW_MODE_FULL)
wTree.get_widget('menu-mode-playlist').connect('activate', onViewMode, consts.VIEW_MODE_PLAYLIST)
wTree.get_widget('menu-quit').connect('activate', lambda item: onDelete(window, None))
wTree.get_widget('menu-about').connect('activate', onAbout)
wTree.get_widget('menu-help').connect('activate', onHelp)
wTree.get_widget('menu-preferences').connect('activate', lambda item: modules.showPreferences())
# Now we can start all modules
gobject.idle_add(modules.postMsg, consts.MSG_EVT_APP_STARTED)
# Immediately show the preferences the first time the application is started
if prefs.get(__name__, 'first-time', True):
prefs.set(__name__, 'first-time', False)
gobject.idle_add(modules.showPreferences)
def onViewMode(item, mode):
""" Wrapper for setViewMode(): Don't do anything if the mode the same as the current one """
if item.get_active() and mode != prefs.get(__name__, 'view-mode', DEFAULT_VIEW_MODE):
setViewMode(mode, True)
def setViewMode(mode, resize):
""" Change the view mode to the given one """
lastMode = prefs.get(__name__, 'view-mode', DEFAULT_VIEW_MODE)
prefs.set(__name__, 'view-mode', mode)
(winWidth, winHeight) = window.get_size()
if mode == consts.VIEW_MODE_FULL:
paned.get_child1().show()
wTree.get_widget('statusbar').show()
wTree.get_widget('box-btn-tracklist').show()
wTree.get_widget('scrolled-tracklist').show()
wTree.get_widget('box-trkinfo').show()
if resize:
if lastMode != consts.VIEW_MODE_FULL: winWidth = winWidth + paned.get_position()
if lastMode == consts.VIEW_MODE_MINI: winHeight = prefs.get(__name__, 'full-win-height', DEFAULT_WIN_HEIGHT)
window.resize(winWidth, winHeight)
return
paned.get_child1().hide()
if resize and lastMode == consts.VIEW_MODE_FULL:
winWidth = winWidth - paned.get_position()
window.resize(winWidth, winHeight)
if mode == consts.VIEW_MODE_PLAYLIST:
wTree.get_widget('statusbar').show()
wTree.get_widget('box-btn-tracklist').hide()
wTree.get_widget('scrolled-tracklist').show()
wTree.get_widget('box-trkinfo').show()
if resize and lastMode == consts.VIEW_MODE_MINI:
window.resize(winWidth, prefs.get(__name__, 'full-win-height', DEFAULT_WIN_HEIGHT))
return
wTree.get_widget('statusbar').hide()
wTree.get_widget('box-btn-tracklist').hide()
wTree.get_widget('scrolled-tracklist').hide()
if mode == consts.VIEW_MODE_MINI: wTree.get_widget('box-trkinfo').show()
else: wTree.get_widget('box-trkinfo').hide()
if resize: window.resize(winWidth, 1)
# --== Entry point ==--
log.logger.info('Started')
# Localization
locale.setlocale(locale.LC_ALL, '')
gettext.textdomain(consts.appNameShort)
gettext.bindtextdomain(consts.appNameShort, consts.dirLocale)
gtk.glade.textdomain(consts.appNameShort)
gtk.glade.bindtextdomain(consts.appNameShort, consts.dirLocale)
# Command line
prefs.setCmdLine((optOptions, optArgs))
# PyGTK initialization
gobject.threads_init()
gtk.window_set_default_icon_list(gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon16),
gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon24),
gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon32),
gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon48),
gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon64),
gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon128))
# Create the GUI
wTree = loadGladeFile('MainWindow.glade')
paned = wTree.get_widget('pan-main')
window = wTree.get_widget('win-main')
prefs.setWidgetsTree(wTree)
# RGBA support
try:
colormap = window.get_screen().get_rgba_colormap()
if colormap:
gtk.widget_set_default_colormap(colormap)
except:
log.logger.info('No RGBA support (requires PyGTK 2.10+)')
# Show all widgets and restore the window size BEFORE hiding some of them when restoring the view mode
# Resizing must be done before showing the window to make sure that the WM correctly places the window
if prefs.get(__name__, 'win-is-maximized', DEFAULT_MAXIMIZED_STATE):
window.maximize()
window.resize(prefs.get(__name__, 'win-width', DEFAULT_WIN_WIDTH), prefs.get(__name__, 'win-height', DEFAULT_WIN_HEIGHT))
window.show_all()
# Restore last view mode
viewMode = prefs.get(__name__, 'view-mode', DEFAULT_VIEW_MODE)
if viewMode == consts.VIEW_MODE_FULL:
wTree.get_widget('menu-mode-full').set_active(True)
else:
if viewMode == consts.VIEW_MODE_MINI: wTree.get_widget('menu-mode-mini').set_active(True)
else: wTree.get_widget('menu-mode-playlist').set_active(True)
setViewMode(viewMode, False)
# Restore sizes once more
window.resize(prefs.get(__name__, 'win-width', DEFAULT_WIN_WIDTH), prefs.get(__name__, 'win-height', DEFAULT_WIN_HEIGHT))
paned.set_position(prefs.get(__name__, 'paned-pos', DEFAULT_PANED_POS))
# Initialization done, let's continue the show
gobject.idle_add(realStartup)
gtk.main()
./decibel-audio-player-1.06/src/remote.py 0000755 0001750 0001750 00000007700 11456551413 020427 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import dbus, os.path, sys
(
PLAY,
PAUSE,
NEXT,
PREVIOUS,
STOP,
SET,
ADD,
CLEAR,
SHUFFLE,
VOLUME,
) = range(10)
(CMD_ARGS, CMD_HELP, CMD_NAME) = range(3)
commands = {
'play': ( '', 'Start playing the current track', PLAY ),
'pause': ( '', 'Pause or continue playing the current track', PAUSE ),
'next': ( '', 'Jump to the next track', NEXT ),
'prev': ( '', 'Jump to the previous track', PREVIOUS),
'stop': ( '', 'Stop playback', STOP ),
'pl-set': ( 'file1 file2...', 'Set the playlist to the given files', SET ),
'pl-add': ( 'file1 file2...', 'Append the given files to the playlist', ADD ),
'pl-clr': ( '', 'Clear the playlist', CLEAR ),
'shuffle': ( '', 'Shuffle the playlist', SHUFFLE ),
'volume': ( 'value (0 -- 100)', 'Set the volume', VOLUME ),
}
# Check the command line
cmdLineOk = False
if len(sys.argv) > 1:
cmdName = sys.argv[1]
if cmdName not in commands: print '%s is not a valid command\n' % cmdName
elif len(sys.argv) == 2 and commands[cmdName][CMD_ARGS] != '': print '%s needs some arguments\n' % cmdName
elif len(sys.argv) > 2 and commands[cmdName][CMD_ARGS] == '': print '%s does not take any argument\n' % cmdName
else: cmdLineOk = True
if not cmdLineOk:
print 'Usage: %s command [arg1 arg2...]\n' % os.path.basename(sys.argv[0])
print 'Command | Arguments | Description'
print '-------------------------------------------------------------------------'
for cmd, data in sorted(commands.iteritems()):
print '%s| %s| %s' % (cmd.ljust(9), data[CMD_ARGS].ljust(17), data[CMD_HELP])
sys.exit(1)
# Connect to D-BUS
session = dbus.SessionBus()
activeServices = session.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus').ListNames()
if 'org.mpris.dap' not in activeServices:
print 'Decibel Audio Player is not running, or D-Bus support is not available'
sys.exit(2)
cmd = commands[cmdName][CMD_NAME]
player = dbus.Interface(session.get_object('org.mpris.dap', '/Player'), 'org.freedesktop.MediaPlayer')
tracklist = dbus.Interface(session.get_object('org.mpris.dap', '/TrackList'), 'org.freedesktop.MediaPlayer')
if cmd == SET: tracklist.SetTracks(sys.argv[2:], True)
elif cmd == ADD:
print 'Hello'
tracklist.AddTracks(sys.argv[2:], False)
elif cmd == PLAY: player.Play()
elif cmd == NEXT: player.Next()
elif cmd == STOP: player.Stop()
elif cmd == PAUSE: player.Pause()
elif cmd == CLEAR: tracklist.Clear()
elif cmd == VOLUME: player.VolumeSet(int(sys.argv[2]))
elif cmd == SHUFFLE: tracklist.SetRandom(True)
elif cmd == PREVIOUS: player.Prev()
./decibel-audio-player-1.06/src/modules/ 0000755 0001750 0001750 00000000000 11456551414 020224 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/modules/Twitter.py 0000644 0001750 0001750 00000011441 11456551413 022240 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gui, modules, traceback
from tools import consts, prefs
from gettext import gettext as _
from tools.log import logger
MOD_INFO = ('Twitter', 'Twitter', _('Update the status of your Twitter account'), [], False, True, consts.MODCAT_INTERNET)
DEFAULT_STATUS_MSG = '♫ Listening to {album} by {artist} ♫'
class Twitter(modules.ThreadedModule):
""" This module updates the status of a Twitter account based on the current track """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
}
modules.ThreadedModule.__init__(self, handlers)
def getAuthInfo(self):
""" Retrieve the login/password of the user """
from gui import authentication
auth = authentication.getAuthInfo('Twitter', _('your Twitter account'))
if auth is None: self.login, self.passwd = None, None
else: self.login, self.passwd = auth
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
self.login = None
self.passwd = None
self.cfgWindow = None
self.lastStatus = ''
def onNewTrack(self, track):
""" A new track has started """
import base64, urllib, urllib2
status = track.format(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG))
if status == self.lastStatus:
return
self.gtkExecute(self.getAuthInfo)
if self.passwd is None:
return
authToken = base64.b64encode(self.login + ':' + self.passwd)
self.passwd = None
self.lastStatus = status
request = urllib2.Request('http://twitter.com/statuses/update.xml')
request.headers['Authorization'] = 'Basic ' + authToken
request.data = urllib.urlencode({'status': status})
try:
urllib2.urlopen(request)
except:
logger.error('[%s] Unable to set Twitter status\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration window """
if self.cfgWindow is None:
self.cfgWindow = gui.window.Window('Twitter.glade', 'vbox1', __name__, _(MOD_INFO[modules.MODINFO_NAME]), 440, 141)
# GTK handlers
self.cfgWindow.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide())
self.cfgWindow.getWidget('btn-help').connect('clicked', self.onBtnHelp)
if not self.cfgWindow.isVisible():
self.cfgWindow.getWidget('txt-status').set_text(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG))
self.cfgWindow.getWidget('btn-ok').grab_focus()
self.cfgWindow.show()
def onBtnOk(self, btn):
""" Save new preferences """
prefs.set(__name__, 'status-msg', self.cfgWindow.getWidget('txt-status').get_text())
self.cfgWindow.hide()
def onBtnHelp(self, btn):
""" Display a small help message box """
# Do this import only when we really need it
import media
helpDlg = gui.help.HelpDlg(_(MOD_INFO[modules.MODINFO_NAME]))
helpDlg.addSection(_('Description'),
_('This module posts a message to your Twitter account according to what '
'you are listening to.'))
helpDlg.addSection(_('Customizing the Status'),
_('You can set the status to any text you want. Before setting it, the module replaces all fields of '
'the form {field} by their corresponding value. Available fields are:')
+ '\n\n' + media.track.getFormatSpecialFields(False))
helpDlg.show(self.cfgWindow)
./decibel-audio-player-1.06/src/modules/FileExplorer.py 0000644 0001750 0001750 00000060644 11456551413 023207 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, media, modules, os, tools
from tools import consts, prefs, icons
from media import playlist
from gettext import gettext as _
from os.path import isdir, isfile
from gobject import idle_add, TYPE_STRING, TYPE_INT
MOD_INFO = ('File Explorer', _('File Explorer'), _('Browse your file system'), [], True, True, consts.MODCAT_EXPLORER)
MOD_L10N = MOD_INFO[modules.MODINFO_L10N]
# Default preferences
PREFS_DEFAULT_MEDIA_FOLDERS = {_('Home'): consts.dirBaseUsr, _('Root'): '/'} # List of media folders that are used as roots for the file explorer
PREFS_DEFAULT_ADD_BY_FILENAME = False # True if files should be added to the playlist by their filename
PREFS_DEFAULT_SHOW_HIDDEN_FILES = False # True if hidden files should be shown
# The format of a row in the treeview
(
ROW_PIXBUF, # Item icon
ROW_NAME, # Item name
ROW_TYPE, # The type of the item (e.g., directory, file)
ROW_FULLPATH # The full path to the item
) = range(4)
# The possible types for a node of the tree
(
TYPE_DIR, # A directory
TYPE_FILE, # A media file
TYPE_NONE # A fake item, used to display a '+' in front of a directory when needed
) = range(3)
class FileExplorer(modules.Module):
""" This explorer lets the user browse the disk from a given root directory (e.g., ~/, /) """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onAppQuit,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_EXPLORER_CHANGED: self.onExplorerChanged,
}
modules.Module.__init__(self, handlers)
def createTree(self):
""" Create the tree used to display the file system """
from gui import extTreeview
columns = (('', [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (gtk.CellRendererText(), TYPE_STRING)], True),
(None, [(None, TYPE_INT)], False),
(None, [(None, TYPE_STRING)], False))
self.tree = extTreeview.ExtTreeView(columns, True)
self.scrolled.add(self.tree)
self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]])
self.tree.connect('drag-data-get', self.onDragDataGet)
self.tree.connect('key-press-event', self.onKeyPressed)
self.tree.connect('exttreeview-button-pressed', self.onMouseButton)
self.tree.connect('exttreeview-row-collapsed', self.onRowCollapsed)
self.tree.connect('exttreeview-row-expanded', self.onRowExpanded)
def getTreeDump(self, path=None):
""" Recursively dump the given tree starting at path (None for the root of the tree) """
list = []
for child in self.tree.iterChildren(path):
row = self.tree.getRow(child)
if self.tree.getNbChildren(child) == 0: grandChildren = None
elif self.tree.row_expanded(child): grandChildren = self.getTreeDump(child)
else: grandChildren = []
list.append([(row[ROW_NAME], row[ROW_TYPE], row[ROW_FULLPATH]), grandChildren])
return list
def restoreTreeDump(self, dump, parent=None):
""" Recursively restore the dump under the given parent (None for the root of the tree) """
for item in dump:
(name, type, path) = item[0]
if type == TYPE_FILE:
self.tree.appendRow((icons.mediaFileMenuIcon(), name, TYPE_FILE, path), parent)
else:
newNode = self.tree.appendRow((icons.dirMenuIcon(), name, TYPE_DIR, path), parent)
if item[1] is not None:
fakeChild = self.tree.appendRow((icons.dirMenuIcon(), '', TYPE_NONE, ''), newNode)
if len(item[1]) != 0:
# We must expand the row before adding the real children, but this works only if there is already at least one child
self.tree.expandRow(newNode)
self.restoreTreeDump(item[1], newNode)
self.tree.removeRow(fakeChild)
def saveTreeState(self):
""" Return a dictionary representing the current state of the tree """
if self.currRoot is not None:
self.treeState[self.currRoot] = {
'tree-state': self.getTreeDump(),
'selected-paths': self.tree.getSelectedPaths(),
'vscrollbar-pos': self.scrolled.get_vscrollbar().get_value(),
'hscrollbar-pos': self.scrolled.get_hscrollbar().get_value(),
}
def sortKey(self, row):
""" Key function used to compare two rows of the tree """
return row[ROW_NAME].lower()
def setShowHiddenFiles(self, showHiddenFiles):
""" Show/hide hidden files """
if showHiddenFiles != self.showHiddenFiles:
# Update the configuration window if needed
if self.cfgWin is not None and self.cfgWin.isVisible():
self.cfgWin.getWidget('chk-hidden').set_active(showHiddenFiles)
self.showHiddenFiles = showHiddenFiles
self.refresh()
def play(self, replace, path=None):
"""
Replace/extend the tracklist
If 'path' is None, use the current selection
"""
if path is None: tracks = media.getTracks([row[ROW_FULLPATH] for row in self.tree.getSelectedRows()], self.addByFilename, not self.showHiddenFiles)
else: tracks = media.getTracks([self.tree.getRow(path)[ROW_FULLPATH]], self.addByFilename, not self.showHiddenFiles)
if replace: modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': True})
else: modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': False})
def renameFolder(self, oldName, newName):
""" Rename a folder """
self.folders[newName] = self.folders[oldName]
del self.folders[oldName]
if oldName in self.treeState:
self.treeState[newName] = self.treeState[oldName]
del self.treeState[oldName]
modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName})
# --== Tree management ==--
def startLoading(self, row):
""" Tell the user that the contents of row is being loaded """
name = self.tree.getItem(row, ROW_NAME)
self.tree.setItem(row, ROW_NAME, '%s %s' % (name, _('loading...')))
def stopLoading(self, row):
""" Tell the user that the contents of row has been loaded"""
name = self.tree.getItem(row, ROW_NAME)
index = name.find('<')
if index != -1:
self.tree.setItem(row, ROW_NAME, name[:index-2])
def getDirContents(self, directory):
""" Return a tuple of sorted rows (directories, playlists, mediaFiles) for the given directory """
playlists = []
mediaFiles = []
directories = []
for (file, path) in tools.listDir(directory, self.showHiddenFiles):
if isdir(path):
directories.append((icons.dirMenuIcon(), tools.htmlEscape(unicode(file, errors='replace')), TYPE_DIR, path))
elif isfile(path):
if media.isSupported(file):
mediaFiles.append((icons.mediaFileMenuIcon(), tools.htmlEscape(unicode(file, errors='replace')), TYPE_FILE, path))
elif playlist.isSupported(file):
playlists.append((icons.mediaFileMenuIcon(), tools.htmlEscape(unicode(file, errors='replace')), TYPE_FILE, path))
playlists.sort(key=self.sortKey)
mediaFiles.sort(key=self.sortKey)
directories.sort(key=self.sortKey)
return (directories, playlists, mediaFiles)
def exploreDir(self, parent, directory, fakeChild=None):
"""
List the contents of the given directory and append it to the tree as a child of parent
If fakeChild is not None, remove it when the real contents has been loaded
"""
directories, playlists, mediaFiles = self.getDirContents(directory)
self.tree.appendRows(directories, parent)
self.tree.appendRows(playlists, parent)
self.tree.appendRows(mediaFiles, parent)
if fakeChild is not None:
self.tree.removeRow(fakeChild)
idle_add(self.updateDirNodes(parent).next)
def updateDirNodes(self, parent):
""" This generator updates the directory nodes, based on whether they should be expandable """
for child in self.tree.iterChildren(parent):
# Only directories need to be updated and since they all come first, we can stop as soon as we find something else
if self.tree.getItem(child, ROW_TYPE) != TYPE_DIR:
break
# Make sure it's readable
directory = self.tree.getItem(child, ROW_FULLPATH)
hasContent = False
if os.access(directory, os.R_OK | os.X_OK):
for (file, path) in tools.listDir(directory, self.showHiddenFiles):
if isdir(path) or (isfile(path) and (media.isSupported(file) or playlist.isSupported(file))):
hasContent = True
break
# Append/remove children if needed
if hasContent and self.tree.getNbChildren(child) == 0: self.tree.appendRow((icons.dirMenuIcon(), '', TYPE_NONE, ''), child)
elif not hasContent and self.tree.getNbChildren(child) > 0: self.tree.removeAllChildren(child)
yield True
if parent is not None:
self.stopLoading(parent)
yield False
def refresh(self, treePath=None):
""" Refresh the tree, starting from treePath """
if treePath is None: directory = self.folders[self.currRoot]
else: directory = self.tree.getItem(treePath, ROW_FULLPATH)
directories, playlists, mediaFiles = self.getDirContents(directory)
disk = directories + playlists + mediaFiles
diskIndex = 0
childIndex = 0
childLeftIntentionally = False
while diskIndex < len(disk):
rowPath = self.tree.getChild(treePath, childIndex)
# Did we reach the end of the tree?
if rowPath is None:
break
file = disk[diskIndex]
cmpResult = cmp(self.sortKey(self.tree.getRow(rowPath)), self.sortKey(file))
if cmpResult < 0:
# We can't remove the only child left, to prevent the node from being closed automatically
if self.tree.getNbChildren(treePath) == 1:
childLeftIntentionally = True
break
self.tree.removeRow(rowPath)
else:
if cmpResult > 0:
self.tree.insertRowBefore(file, treePath, rowPath)
diskIndex += 1
childIndex += 1
# If there are tree rows left, all the corresponding files are no longer there
if not childLeftIntentionally:
while childIndex < self.tree.getNbChildren(treePath):
self.tree.removeRow(self.tree.getChild(treePath, childIndex))
# Disk files left?
while diskIndex < len(disk):
self.tree.appendRow(disk[diskIndex], treePath)
diskIndex += 1
# Deprecated child left? (must be done after the addition of left disk files)
if childLeftIntentionally:
self.tree.removeRow(self.tree.getChild(treePath, 0))
# Update nodes' appearance
if len(directories) != 0:
idle_add(self.updateDirNodes(treePath).next)
# Recursively refresh expanded rows
for child in self.tree.iterChildren(treePath):
if self.tree.row_expanded(child):
idle_add(self.refresh, child)
# --== GTK handlers ==--
def onMouseButton(self, tree, event, path):
""" A mouse button has been pressed """
if event.button == 3:
self.onShowPopupMenu(tree, event.button, event.time, path)
elif path is not None:
if event.button == 2:
self.play(False, path)
elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
if tree.getItem(path, ROW_PIXBUF) != icons.dirMenuIcon(): self.play(True)
elif tree.row_expanded(path): tree.collapse_row(path)
else: tree.expand_row(path, False)
def onShowPopupMenu(self, tree, button, time, path):
""" Show a popup menu """
popup = gtk.Menu()
# Play selection
play = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
popup.append(play)
if path is None: play.set_sensitive(False)
else: play.connect('activate', lambda widget: self.play(True))
popup.append(gtk.SeparatorMenuItem())
# Collapse all nodes
collapse = gtk.ImageMenuItem(_('Collapse all'))
collapse.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU))
collapse.connect('activate', lambda widget: tree.collapse_all())
popup.append(collapse)
# Refresh the view
refresh = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
refresh.connect('activate', lambda widget: self.refresh())
popup.append(refresh)
popup.append(gtk.SeparatorMenuItem())
# Show hidden files
hidden = gtk.CheckMenuItem(_('Show hidden files'))
hidden.set_active(self.showHiddenFiles)
hidden.connect('toggled', lambda item: self.setShowHiddenFiles(item.get_active()))
popup.append(hidden)
popup.show_all()
popup.popup(None, None, None, button, time)
def onKeyPressed(self, tree, event):
""" A key has been pressed """
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'F5': self.refresh()
elif keyname == 'plus': tree.expandRows()
elif keyname == 'Left': tree.collapseRows()
elif keyname == 'Right': tree.expandRows()
elif keyname == 'minus': tree.collapseRows()
elif keyname == 'space': tree.switchRows()
elif keyname == 'Return': self.play(True)
def onRowExpanded(self, tree, path):
""" Replace the fake child by the real children """
self.startLoading(path)
idle_add(self.exploreDir, path, tree.getItem(path, ROW_FULLPATH), tree.getChild(path, 0))
def onRowCollapsed(self, tree, path):
""" Replace all children by a fake child """
tree.removeAllChildren(path)
tree.appendRow((icons.dirMenuIcon(), '', TYPE_NONE, ''), path)
def onDragDataGet(self, tree, context, selection, info, time):
""" Provide information about the data being dragged """
allTracks = media.getTracks([row[ROW_FULLPATH] for row in self.tree.getSelectedRows()], self.addByFilename, not self.showHiddenFiles)
selection.set(consts.DND_TARGETS[consts.DND_DAP_TRACKS][0], 8, '\n'.join([track.serialize() for track in allTracks]))
# --== Message handlers ==--
def onAppStarted(self):
""" The module has been loaded """
self.tree = None
self.cfgWin = None
self.folders = prefs.get(__name__, 'media-folders', PREFS_DEFAULT_MEDIA_FOLDERS)
self.scrolled = gtk.ScrolledWindow()
self.currRoot = None
self.treeState = prefs.get(__name__, 'saved-states', {})
self.addByFilename = prefs.get(__name__, 'add-by-filename', PREFS_DEFAULT_ADD_BY_FILENAME)
self.showHiddenFiles = prefs.get(__name__, 'show-hidden-files', PREFS_DEFAULT_SHOW_HIDDEN_FILES)
self.scrolled.set_shadow_type(gtk.SHADOW_IN)
self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.scrolled.show()
for name in self.folders:
modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled})
def onAppQuit(self):
""" The module is going to be unloaded """
self.saveTreeState()
prefs.set(__name__, 'saved-states', self.treeState)
prefs.set(__name__, 'media-folders', self.folders)
prefs.set(__name__, 'add-by-filename', self.addByFilename)
prefs.set(__name__, 'show-hidden-files', self.showHiddenFiles)
def onExplorerChanged(self, modName, expName):
""" A new explorer has been selected """
if modName == MOD_L10N and self.currRoot != expName:
# Create the tree if needed
if self.tree is None:
self.createTree()
# Save the state of the current root
if self.currRoot is not None:
self.saveTreeState()
self.tree.clear()
self.currRoot = expName
# Restore the state of the new root
if expName in self.treeState:
self.tree.handler_block_by_func(self.onRowExpanded)
self.restoreTreeDump(self.treeState[expName]['tree-state'])
self.tree.handler_unblock_by_func(self.onRowExpanded)
idle_add(self.scrolled.get_vscrollbar().set_value, self.treeState[expName]['vscrollbar-pos'])
idle_add(self.scrolled.get_hscrollbar().set_value, self.treeState[expName]['hscrollbar-pos'])
idle_add(self.tree.selectPaths, self.treeState[expName]['selected-paths'])
idle_add(self.refresh)
else:
self.exploreDir(None, self.folders[self.currRoot])
if len(self.tree) != 0:
self.tree.scroll_to_cell(0)
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration dialog """
if self.cfgWin is None:
from gui import extListview, window
self.cfgWin = window.Window('FileExplorer.glade', 'vbox1', __name__, MOD_L10N, 370, 400)
# Create the list of folders
txtRdr = gtk.CellRendererText()
pixRdr = gtk.CellRendererPixbuf()
columns = ((None, [(txtRdr, TYPE_STRING)], 0, False, False),
('', [(pixRdr, gtk.gdk.Pixbuf), (txtRdr, TYPE_STRING)], 2, False, True))
self.cfgList = extListview.ExtListView(columns, sortable=False, useMarkup=True, canShowHideColumns=False)
self.cfgList.set_headers_visible(False)
self.cfgWin.getWidget('scrolledwindow1').add(self.cfgList)
# Connect handlers
self.cfgList.connect('key-press-event', self.onCfgKeyPressed)
self.cfgList.get_selection().connect('changed', self.onCfgSelectionChanged)
self.cfgWin.getWidget('btn-add').connect('clicked', self.onAddFolder)
self.cfgWin.getWidget('btn-remove').connect('clicked', lambda btn: self.onRemoveSelectedFolder(self.cfgList))
self.cfgWin.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWin.getWidget('btn-rename').connect('clicked', self.onRenameFolder)
self.cfgWin.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWin.hide())
self.cfgWin.getWidget('btn-help').connect('clicked', self.onHelp)
if not self.cfgWin.isVisible():
self.populateFolderList()
self.cfgWin.getWidget('chk-hidden').set_active(self.showHiddenFiles)
self.cfgWin.getWidget('chk-add-by-filename').set_active(self.addByFilename)
self.cfgWin.getWidget('btn-ok').grab_focus()
self.cfgWin.show()
def populateFolderList(self):
""" Populate the list of known folders """
self.cfgList.replaceContent([(name, icons.dirBtnIcon(), '%s\n%s' % (tools.htmlEscape(name), tools.htmlEscape(path)))
for name, path in sorted(self.folders.iteritems())])
def onAddFolder(self, btn):
""" Let the user add a new folder to the list """
from gui import selectPath
result = selectPath.SelectPath(MOD_L10N, self.cfgWin, self.folders.keys()).run()
if result is not None:
name, path = result
self.folders[name] = path
self.populateFolderList()
modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled})
def onRemoveSelectedFolder(self, list):
""" Remove the selected media folder """
import gui
if list.getSelectedRowsCount() == 1:
remark = _('You will be able to add this root folder again later on if you wish so.')
question = _('Remove the selected entry?')
else:
remark = _('You will be able to add these root folders again later on if you wish so.')
question = _('Remove all selected entries?')
if gui.questionMsgBox(self.cfgWin, question, '%s %s' % (_('Your media files will not be deleted.'), remark)) == gtk.RESPONSE_YES:
for row in self.cfgList.getSelectedRows():
name = row[0]
modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': name})
del self.folders[name]
# Remove the tree, if any, from the scrolled window
if self.currRoot == name:
self.currRoot = None
# Remove the saved state of the tree, if any
if name in self.treeState:
del self.treeState[name]
self.cfgList.removeSelectedRows()
def onRenameFolder(self, btn):
""" Let the user rename a folder """
from gui import selectPath
name = self.cfgList.getSelectedRows()[0][0]
forbidden = [rootName for rootName in self.folders if rootName != name]
pathSelector = selectPath.SelectPath(MOD_L10N, self.cfgWin, forbidden)
pathSelector.setPathSelectionEnabled(False)
result = pathSelector.run(name, self.folders[name])
if result is not None and result[0] != name:
self.renameFolder(name, result[0])
self.populateFolderList()
def onCfgKeyPressed(self, list, event):
""" Remove the selection if possible """
if gtk.gdk.keyval_name(event.keyval) == 'Delete':
self.onRemoveSelectedFolder(list)
def onCfgSelectionChanged(self, selection):
""" The selection has changed """
self.cfgWin.getWidget('btn-remove').set_sensitive(selection.count_selected_rows() != 0)
self.cfgWin.getWidget('btn-rename').set_sensitive(selection.count_selected_rows() == 1)
def onBtnOk(self, btn):
""" The user has clicked on the OK button """
self.cfgWin.hide()
self.setShowHiddenFiles(self.cfgWin.getWidget('chk-hidden').get_active())
self.addByFilename = self.cfgWin.getWidget('chk-add-by-filename').get_active()
def onHelp(self, btn):
""" Display a small help message box """
import gui
helpDlg = gui.help.HelpDlg(MOD_L10N)
helpDlg.addSection(_('Description'),
_('This module allows you to browse the files on your drives.'))
helpDlg.addSection(_('Usage'),
_('At least one root folder must be added to browse your files. This folder then becomes the root of the '
'file explorer tree in the main window.'))
helpDlg.show(self.cfgWin)
./decibel-audio-player-1.06/src/modules/CtrlPanel.py 0000644 0001750 0001750 00000016430 11456551413 022465 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, modules
from tools import consts, prefs, sec2str
from gettext import gettext as _
MOD_INFO = ('Control Panel', 'Control Panel', '', [], True, False, consts.MODCAT_NONE)
PREFS_DEFAULT_VOLUME = 0.65
class CtrlPanel(modules.Module):
""" This module manages the control panel with the buttons and the slider """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_APP_QUIT: self.onAppQuit,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_TRACK_MOVED: self.onCurrentTrackMoved,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_NEW_TRACKLIST: self.onNewTracklist,
consts.MSG_EVT_VOLUME_CHANGED: self.onVolumeChanged,
consts.MSG_EVT_TRACK_POSITION: self.onNewTrackPosition,
}
modules.Module.__init__(self, handlers)
# --== Message handler ==--
def onAppStarted(self):
""" Real initialization function, called when this module has been loaded """
self.currTrackLength = 0
self.sclBeingDragged = False
# Widgets
wTree = prefs.getWidgetsTree()
self.btnStop = wTree.get_widget('btn-stop')
self.btnPlay = wTree.get_widget('btn-play')
self.btnNext = wTree.get_widget('btn-next')
self.btnPrev = wTree.get_widget('btn-previous')
self.sclSeek = wTree.get_widget('scl-position')
self.btnVolume = wTree.get_widget('btn-volume')
self.lblElapsed = wTree.get_widget('lbl-elapsedTime')
self.lblRemaining = wTree.get_widget('lbl-remainingTime')
# Restore the volume
volume = prefs.get(__name__, 'volume', PREFS_DEFAULT_VOLUME)
self.btnVolume.set_value(volume)
modules.postMsg(consts.MSG_CMD_SET_VOLUME, {'value': volume})
# GTK handlers
self.btnStop.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_STOP))
self.btnNext.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_NEXT))
self.btnPrev.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_PREVIOUS))
self.btnPlay.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE))
self.sclSeek.connect('change-value', self.onSeekChangingValue)
self.sclSeek.connect('value-changed', self.onSeekValueChanged)
self.btnVolume.connect('value-changed', self.onVolumeValueChanged)
def onAppQuit(self):
""" The application is about to terminate """
prefs.set(__name__, 'volume', self.btnVolume.get_value())
def onNewTrack(self, track):
""" A new track is being played """
self.btnStop.set_sensitive(True)
self.btnPlay.set_sensitive(True)
self.btnPlay.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON))
self.btnPlay.set_tooltip_text(_('Pause the current track'))
self.currTrackLength = track.getLength()
self.sclSeek.show()
self.lblElapsed.show()
self.lblRemaining.show()
self.onNewTrackPosition(0)
# Must be done last
if self.currTrackLength != 0:
self.sclSeek.set_range(0, self.currTrackLength)
def onStopped(self):
""" The playback has been stopped """
self.btnStop.set_sensitive(False)
self.btnNext.set_sensitive(False)
self.btnPrev.set_sensitive(False)
self.btnPlay.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON))
self.btnPlay.set_tooltip_text(_('Play the first selected track of the playlist'))
self.sclSeek.hide()
self.lblElapsed.hide()
self.lblRemaining.hide()
def onNewTrackPosition(self, seconds):
""" The track position has changed """
if not self.sclBeingDragged:
self.lblElapsed.set_label(sec2str(seconds))
if seconds >= self.currTrackLength:
seconds = self.currTrackLength
self.lblRemaining.set_label(sec2str(self.currTrackLength - seconds))
# Make sure the handler will not be called
self.sclSeek.handler_block_by_func(self.onSeekValueChanged)
self.sclSeek.set_value(seconds)
self.sclSeek.handler_unblock_by_func(self.onSeekValueChanged)
def onVolumeChanged(self, value):
""" The volume has been changed """
self.btnVolume.handler_block_by_func(self.onVolumeValueChanged)
self.btnVolume.set_value(value)
self.btnVolume.handler_unblock_by_func(self.onVolumeValueChanged)
def onCurrentTrackMoved(self, hasPrevious, hasNext):
""" Update previous and next buttons """
self.btnNext.set_sensitive(hasNext)
self.btnPrev.set_sensitive(hasPrevious)
def onPaused(self):
""" The playback has been paused """
self.btnPlay.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON))
self.btnPlay.set_tooltip_text(_('Continue playing the current track'))
def onUnpaused(self):
""" The playback has been unpaused """
self.btnPlay.set_image(gtk.image_new_from_stock(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON))
self.btnPlay.set_tooltip_text(_('Pause the current track'))
def onNewTracklist(self, tracks, playtime):
""" A new tracklist has been set """
self.btnPlay.set_sensitive(playtime != 0)
# --== GTK handlers ==--
def onSeekValueChanged(self, range):
""" The user has moved the seek slider """
modules.postMsg(consts.MSG_CMD_SEEK, {'seconds': int(range.get_value())})
self.sclBeingDragged = False
def onSeekChangingValue(self, range, scroll, value):
""" The user is moving the seek slider """
self.sclBeingDragged = True
if value >= self.currTrackLength: value = self.currTrackLength
else: value = int(value)
self.lblElapsed.set_label(sec2str(value))
self.lblRemaining.set_label(sec2str(self.currTrackLength - value))
def onVolumeValueChanged(self, button, value):
""" The user has moved the volume slider """
modules.postMsg(consts.MSG_CMD_SET_VOLUME, {'value': value})
./decibel-audio-player-1.06/src/modules/__init__.py 0000644 0001750 0001750 00000032107 11456551413 022337 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, gui, gui.preferences, os, sys, threading, traceback
from tools import consts, log, prefs
from gettext import gettext as _
# Information exported by a module
(
MODINFO_NAME, # Name of the module, must be unique
MODINFO_L10N, # Name translated into the current locale
MODINFO_DESC, # Description of the module, translated into the current locale
MODINFO_DEPS, # A list of special Python dependencies (e.g., pynotify)
MODINFO_MANDATORY, # True if the module cannot be disabled
MODINFO_CONFIGURABLE, # True if the module can be configured
MODINFO_CATEGORY, # Category the module belongs to
) = range(7)
# Values associated with a module
(
MOD_PMODULE, # The actual Python module object
MOD_CLASSNAME, # The classname of the module
MOD_INSTANCE, # Instance, None if not currently enabled
MOD_INFO # A tuple exported by the module, see above definition
) = range(4)
class LoadException(Exception):
""" Raised when a module could not be loaded """
def __init__(self, errMsg):
""" Constructor """
self.errMsg = errMsg
def __str__(self):
""" String representation """
return self.errMsg
def __checkDeps(deps):
""" Given a list of Python modules, return a list of the modules that are unavailable """
unmetDeps = []
for module in deps:
try: __import__(module)
except: unmetDeps.append(module)
return unmetDeps
def load(name):
""" Load the given module, may raise LoadException """
mModulesLock.acquire()
module = mModules[name]
mModulesLock.release()
# Check dependencies
unmetDeps = __checkDeps(module[MOD_INFO][MODINFO_DEPS])
if len(unmetDeps) != 0:
errMsg = _('The following Python modules are not available:')
errMsg += '\n * '
errMsg += '\n * '.join(unmetDeps)
errMsg += '\n\n'
errMsg += _('You must install them if you want to enable this module.')
raise LoadException, errMsg
# Instantiate the module
try:
module[MOD_INSTANCE] = getattr(module[MOD_PMODULE], module[MOD_CLASSNAME])()
module[MOD_INSTANCE].start()
mHandlersLock.acquire()
if module[MOD_INSTANCE] in mHandlers[consts.MSG_EVT_MOD_LOADED]:
module[MOD_INSTANCE].postMsg(consts.MSG_EVT_MOD_LOADED)
mHandlersLock.release()
log.logger.info('Module loaded: %s' % module[MOD_CLASSNAME])
mEnabledModules.append(name)
prefs.set(__name__, 'enabled_modules', mEnabledModules)
except:
raise LoadException, traceback.format_exc()
def unload(name):
""" Unload the given module """
mModulesLock.acquire()
module = mModules[name]
instance = module[MOD_INSTANCE]
module[MOD_INSTANCE] = None
mModulesLock.release()
if instance is not None:
mHandlersLock.acquire()
instance.postMsg(consts.MSG_EVT_MOD_UNLOADED)
for handlers in [handler for handler in mHandlers.itervalues() if instance in handler]:
handlers.remove(instance)
mHandlersLock.release()
mEnabledModules.remove(name)
log.logger.info('Module unloaded: %s' % module[MOD_CLASSNAME])
prefs.set(__name__, 'enabled_modules', mEnabledModules)
def getModules():
""" Return a copy of all known modules """
mModulesLock.acquire()
copy = mModules.items()
mModulesLock.release()
return copy
def register(module, msgList):
""" Register the given module for all messages in the given list/tuple """
mHandlersLock.acquire()
for msg in msgList:
mHandlers[msg].add(module)
mHandlersLock.release()
def showPreferences():
""" Show the preferences dialog box """
gobject.idle_add(gui.preferences.show)
def __postMsg(msg, params={}):
""" This is the 'real' postMsg function, which must be executed in the GTK main loop """
mHandlersLock.acquire()
for module in mHandlers[msg]:
module.postMsg(msg, params)
mHandlersLock.release()
def postMsg(msg, params={}):
""" Post a message to the queue of modules that registered for this type of message """
# We need to ensure that posting messages will be done by the GTK main loop
# Otherwise, the code of threaded modules could be executed in the caller's thread, which could cause problems when calling GTK functions
gobject.idle_add(__postMsg, msg, params)
def __postQuitMsg():
""" This is the 'real' postQuitMsg function, which must be executed in the GTK main loop """
__postMsg(consts.MSG_EVT_APP_QUIT)
for modData in mModules.itervalues():
if modData[MOD_INSTANCE] is not None:
modData[MOD_INSTANCE].join()
# Don't exit the application right now, let modules do their job before
gobject.idle_add(gtk.main_quit)
def postQuitMsg():
""" Post a MSG_EVT_APP_QUIT in each module's queue and exit the application """
# As with postMsg(), we need to ensure that the code will be executed by the GTK main loop
gobject.idle_add(__postQuitMsg)
mMenuItems = {}
mSeparator = None
mAccelGroup = None
def __addMenuItem(label, callback, accelerator):
""" This is the 'real' addMenuItem function, which must be executed in the GTK main loop """
global mAccelGroup, mSeparator
menu = prefs.getWidgetsTree().get_widget('menu-edit')
# Remove all menu items
if len(mMenuItems) != 0:
menu.remove(mSeparator)
for menuitem in mMenuItems.itervalues():
menu.remove(menuitem)
# Create a new menu item for the module
menuitem = gtk.MenuItem(label)
menuitem.connect('activate', callback)
menuitem.show()
mMenuItems[label] = menuitem
# Add an accelerator if needed
if accelerator is not None:
if mAccelGroup is None:
mAccelGroup = gtk.AccelGroup()
prefs.getWidgetsTree().get_widget('win-main').add_accel_group(mAccelGroup)
key, mod = gtk.accelerator_parse(accelerator)
menuitem.add_accelerator('activate', mAccelGroup, key, mod, gtk.ACCEL_VISIBLE)
# Create the separator?
if mSeparator is None:
mSeparator = gtk.SeparatorMenuItem()
mSeparator.show()
# Re-add items alphabetically, including the new one
menu.insert(mSeparator, 0)
for item in sorted(mMenuItems.items(), key = lambda item: item[0], reverse = True):
menu.insert(item[1], 0)
def addMenuItem(label, callback, accelerator=None):
""" Add a menu item to the 'modules' menu """
gobject.idle_add(__addMenuItem, label, callback, accelerator)
def __delMenuItem(label):
""" This is the 'real' delMenuItem function, which must be executed in the GTK main loop """
# Make sure the menu item is there
if label not in mMenuItems:
return
menu = prefs.getWidgetsTree().get_widget('menu-edit')
# Remove all current menu items
menu.remove(mSeparator)
for menuitem in mMenuItems.itervalues():
menu.remove(menuitem)
# Delete the given menu item
del mMenuItems[label]
# Re-add items if needed
if len(mMenuItems) != 0:
menu.insert(mSeparator, 0)
for item in sorted(mMenuItems.items(), key = lambda item: item[0], reverse = True):
menu.insert(item[1], 0)
def delMenuItem(label):
""" Delete a menu item from the 'modules' menu """
gobject.idle_add(__delMenuItem, label)
# --== Base classes for modules ==--
class ModuleBase:
""" This class makes sure that all modules have some mandatory functions """
def join(self):
pass
def start(self):
pass
def configure(self, parent):
pass
def handleMsg(self, msg, params):
pass
def restartRequired(self):
gobject.idle_add(gui.infoMsgBox, None, _('Restart required'), _('You must restart the application for this modification to take effect.'))
class Module(ModuleBase):
""" This is the base class for non-threaded modules """
def __init__(self, handlers):
self.handlers = handlers
register(self, handlers.keys())
def postMsg(self, msg, params={}):
gobject.idle_add(self.__dispatch, msg, params)
def __dispatch(self, msg, params):
self.handlers[msg](**params)
class ThreadedModule(threading.Thread, ModuleBase):
""" This is the base class for threaded modules """
def __init__(self, handlers):
""" Constructor """
import Queue
# Attributes
self.queue = Queue.Queue(0) # List of queued messages
self.gtkResult = None # Value returned by the function executed in the GTK loop
self.gtkSemaphore = threading.Semaphore(0) # Used to execute some code in the GTK loop
# Initialization
threading.Thread.__init__(self)
# Add QUIT and UNLOADED messages if needed
# These messages are required to exit the thread's loop
if consts.MSG_EVT_APP_QUIT not in handlers: handlers[consts.MSG_EVT_APP_QUIT] = lambda: None
if consts.MSG_EVT_MOD_UNLOADED not in handlers: handlers[consts.MSG_EVT_MOD_UNLOADED] = lambda: None
self.handlers = handlers
register(self, handlers.keys())
def __gtkExecute(self, func):
""" Private function, must be executed in the GTK main loop """
self.gtkResult = func()
self.gtkSemaphore.release()
def gtkExecute(self, func):
""" Execute func in the GTK main loop, and block the execution of the thread until done """
gobject.idle_add(self.__gtkExecute, func)
self.gtkSemaphore.acquire()
return self.gtkResult
def threadExecute(self, func, *args):
"""
Schedule func(*args) to be called by the thread
This is used to avoid func to be executed in the GTK main loop
"""
self.postMsg(consts.MSG_CMD_THREAD_EXECUTE, (func, args))
def postMsg(self, msg, params={}):
""" Enqueue a message in this threads's message queue """
self.queue.put((msg, params))
def run(self):
""" Wait for messages and handle them """
msg = None
while msg != consts.MSG_EVT_APP_QUIT and msg != consts.MSG_EVT_MOD_UNLOADED:
(msg, params) = self.queue.get(True)
if msg == consts.MSG_CMD_THREAD_EXECUTE:
(func, args) = params
func(*args)
else:
self.handlers[msg](**params)
# --== Entry point ==--
mModDir = os.path.dirname(__file__) # Where modules are located
mModules = {} # All known modules associated to an 'active' boolean
mHandlers = dict([(msg, set()) for msg in xrange(consts.MSG_END_VALUE)]) # For each message, store the set of registered modules
mModulesLock = threading.Lock() # Protects the modules list from concurrent access
mHandlersLock = threading.Lock() # Protects the handlers list from concurrent access
mEnabledModules = prefs.get(__name__, 'enabled_modules', []) # List of modules currently enabled
# Find modules, instantiate those that are mandatory or that have been previously enabled by the user
sys.path.append(mModDir)
for file in [os.path.splitext(file)[0] for file in os.listdir(mModDir) if file.endswith('.py') and file != '__init__.py']:
try:
pModule = __import__(file)
modInfo = getattr(pModule, 'MOD_INFO')
# Should it be instanciated?
instance = None
if modInfo[MODINFO_MANDATORY] or modInfo[MODINFO_NAME] in mEnabledModules:
if len(__checkDeps(modInfo[MODINFO_DEPS])) == 0:
instance = getattr(pModule, file)()
instance.start()
log.logger.info('Module loaded: %s' % file)
else:
log.logger.error('Unable to load module %s because of missing dependencies' % file)
# Add it to the dictionary
mModules[modInfo[MODINFO_NAME]] = [pModule, file, instance, modInfo]
except:
log.logger.error('Unable to load module %s\n\n%s' % (file, traceback.format_exc()))
# Remove enabled modules that are no longer available
mEnabledModules[:] = [module for module in mEnabledModules if module in mModules]
prefs.set(__name__, 'enabled_modules', mEnabledModules)
./decibel-audio-player-1.06/src/modules/TrackPanel.py 0000644 0001750 0001750 00000014522 11456551413 022625 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, modules, os.path, tools
from tools import consts
from gettext import gettext as _
MOD_INFO = ('Track Panel', 'Track Panel', '', [], True, False, consts.MODCAT_NONE)
class TrackPanel(modules.Module):
"""
This module manages the panel showing information on the current track.
This includes the thumbnail of the current cover, if the user has enabled the 'Covers' module.
"""
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_CMD_SET_COVER: self.onSetCover,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
}
modules.Module.__init__(self, handlers)
def __setTitle(self, title, length=None):
""" Change the title of the current track """
title = tools.htmlEscape(title)
if length is None: self.txtTitle.set_markup('%s' % title)
else: self.txtTitle.set_markup('%s [%s]' % (title, tools.sec2str(length)))
def __setImage(self, imgPath):
"""
Change the current image to imgPath.
Use the application's icon if imgPath is None.
"""
if imgPath is None: self.img.set_from_file(os.path.join(tools.consts.dirPix, 'cover-none.png'))
else: self.img.set_from_file(imgPath)
def __showCover(self, x, y):
"""
Display a popup window showing the full size cover.
The window closes automatically when clicked or when the mouse leaves it.
"""
# Don't do anything if there's already a cover
if self.coverWindow is not None:
return
frame = gtk.Frame()
image = gtk.Image()
evtBox = gtk.EventBox()
self.coverWindow = gtk.Window(gtk.WINDOW_POPUP)
# Construct the window
image.set_from_file(self.currCoverPath)
evtBox.add(image)
frame.set_shadow_type(gtk.SHADOW_IN)
frame.add(evtBox)
self.coverWindow.add(frame)
# Center the window around (x, y)
pixbuf = image.get_pixbuf()
width = pixbuf.get_width()
height = pixbuf.get_height()
self.coverWindow.move(int(x - width/2), int(y - height/2))
# Destroy the window when clicked and when the mouse leaves it
evtBox.connect('button-press-event', self.onCoverWindowDestroy)
evtBox.connect('leave-notify-event', self.onCoverWindowDestroy)
self.coverWindow.show_all()
# --== Message handlers ==--
def onAppStarted(self):
""" Real initialization function, called when this module has been loaded """
# Widgets
wTree = tools.prefs.getWidgetsTree()
evtBox = wTree.get_widget('evtbox-cover')
self.img = wTree.get_widget('img-cover')
self.txtMisc = wTree.get_widget('lbl-trkMisc')
self.txtTitle = wTree.get_widget('lbl-trkTitle')
self.imgFrame = wTree.get_widget('frm-cover')
self.currTrack = None
self.coverWindow = None
self.coverTimerId = None
self.currCoverPath = None
self.lastMousePosition = (0, 0)
# GTK handlers
evtBox.connect('leave-notify-event', self.onImgMouseLeave)
evtBox.connect('enter-notify-event', self.onImgMouseEnter)
def onNewTrack(self, track):
""" A new track is being played """
self.currTrack = track
self.__setTitle(track.getTitle(), track.getLength())
self.txtMisc.set_text(_('by %(artist)s\nfrom %(album)s' % {'artist': track.getArtist(), 'album': track.getExtendedAlbum()}))
def onStopped(self):
""" Playback has been stopped """
self.currTrack = None
self.currCoverPath = None
self.__setImage(None)
self.__setTitle(consts.appName)
self.txtMisc.set_text('...And Music For All\n')
def onSetCover(self, track, pathThumbnail, pathFullSize):
""" Set the cover that is currently displayed """
# Must check if currTrack is not None, because '==' calls the cmp() method and this fails on None
if self.currTrack is not None and track == self.currTrack:
self.currCoverPath = pathFullSize
self.__setImage(pathThumbnail)
# --== GTK handlers ==--
def onImgMouseEnter(self, evtBox, event):
""" The mouse is over the event box """
if self.currCoverPath is not None and (event.x_root, event.y_root) != self.lastMousePosition:
self.coverTimerId = gobject.timeout_add(600, self.onCoverTimerTimedOut)
def onImgMouseLeave(self, evtBox, event):
""" The mouse left the event box """
self.lastMousePosition = (0, 0)
if self.coverTimerId is not None:
gobject.source_remove(self.coverTimerId)
self.coverTimerId = None
def onCoverTimerTimedOut(self):
""" The mouse has been over the cover thumbnail during enough time """
if self.currCoverPath is not None:
self.__showCover(*tools.getCursorPosition())
return False
def onCoverWindowDestroy(self, widget, event):
""" Destroy the cover window """
if self.coverWindow is not None:
self.coverWindow.destroy()
self.coverWindow = None
self.lastMousePosition = tools.getCursorPosition()
./decibel-audio-player-1.06/src/modules/Covers.py 0000644 0001750 0001750 00000047705 11456551413 022053 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules, os, tools, traceback
from tools import consts, prefs
from gettext import gettext as _
from tools.log import logger
# Module information
MOD_INFO = ('Covers', _('Covers'), _('Show album covers'), [], False, True, consts.MODCAT_DECIBEL)
MOD_NAME = MOD_INFO[modules.MODINFO_NAME]
AS_API_KEY = 'fd8dd98d26bb3f288f3e626502f9add6' # Ingelrest François' Audioscrobbler API key
AS_TAG_START = '' # The text that is right before the URL to the cover
AS_TAG_END = '' # The text that is right after the URL to the cover
# It seems that a non standard 'user-agent' header may cause problem, so let's cheat
USER_AGENT = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008072820 Firefox/3.0.1'
# We store both the paths to the thumbnail and to the full size image
(
CVR_THUMB,
CVR_FULL,
) = range(2)
# Width/height of PIL images
(
PIL_WIDTH,
PIL_HEIGHT,
) = range(2)
# Constants for thumbnails
THUMBNAIL_WIDTH = 100 # Width allocated to thumbnails in the model
THUMBNAIL_HEIGHT = 100 # Height allocated to thumbnails in the model
THUMBNAIL_OFFSETX = 11 # X-offset to render the thumbnail in the model
THUMBNAIL_OFFSETY = 3 # Y-offset to render the thumbnail in the model
# Constants for full size covers
FULLSIZE_WIDTH = 300
FULLSIZE_HEIGHT = 300
# File formats we can read
ACCEPTED_FILE_FORMATS = {'.jpg': None, '.jpeg': None, '.png': None, '.gif': None}
# Default preferences
PREFS_DFT_DOWNLOAD_COVERS = False
PREFS_DFT_PREFER_USER_COVERS = True
PREFS_DFT_USER_COVER_FILENAMES = ['cover', 'art', 'front', '*']
PREFS_DFT_SEARCH_IN_PARENT_DIRS = False
# Images for thumbnails
THUMBNAIL_GLOSS = os.path.join(consts.dirPix, 'cover-gloss.png')
THUMBNAIL_MODEL = os.path.join(consts.dirPix, 'cover-model.png')
class Covers(modules.ThreadedModule):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
}
modules.ThreadedModule.__init__(self, handlers)
def __resizeWithRatio(self, width, height, maxWidth, maxHeight):
"""
Fit (width x height) into (maxWidth x maxHeight) while preserving the original ratio
Return a tuple (newWidth, newheight)
"""
diffWidth = width - maxWidth
diffHeight = height - maxHeight
if diffWidth <= 0 and diffHeight <= 0:
newWidth = width
newHeight = height
elif diffHeight > diffWidth:
newHeight = maxHeight
newWidth = width * maxHeight / height
else:
newWidth = maxWidth
newHeight = height * maxWidth / width
return (newWidth, newHeight)
def generateFullSizeCover(self, inFile, outFile, format):
""" Resize inFile if needed, and write it to outFile (outFile and inFile may be equal) """
import Image
try:
# Open the image
cover = Image.open(inFile)
# Fit the image into FULLSIZE_WIDTH x FULLSIZE_HEIGHT
(newWidth, newHeight) = self.__resizeWithRatio(cover.size[PIL_WIDTH], cover.size[PIL_HEIGHT], FULLSIZE_WIDTH, FULLSIZE_HEIGHT)
# Resize it
cover = cover.resize((newWidth, newHeight), Image.ANTIALIAS)
# We're done
cover.save(outFile, format)
except:
logger.error('[%s] An error occurred while generating a showable full size cover\n\n%s' % (MOD_NAME, traceback.format_exc()))
def generateThumbnail(self, inFile, outFile, format):
""" Generate a thumbnail from inFile (e.g., resize it) and write it to outFile (outFile and inFile may be equal) """
import Image
try:
# Open the image
cover = Image.open(inFile).convert('RGBA')
# Fit the image into THUMBNAIL_WIDTH x THUMBNAIL_HEIGHT
(newWidth, newHeight) = self.__resizeWithRatio(cover.size[PIL_WIDTH], cover.size[PIL_HEIGHT], THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
# We need to shift the image if it doesn't fully fill the thumbnail
if newWidth < THUMBNAIL_WIDTH: offsetX = (THUMBNAIL_WIDTH - newWidth) / 2
else: offsetX = 0
if newHeight < THUMBNAIL_HEIGHT: offsetY = (THUMBNAIL_HEIGHT - newHeight) / 2
else: offsetY = 0
# Resize the image
cover = cover.resize((newWidth, newHeight), Image.ANTIALIAS)
# Paste the resized cover into our model
model = Image.open(THUMBNAIL_MODEL).convert('RGBA')
model.paste(cover, (THUMBNAIL_OFFSETX + offsetX, THUMBNAIL_OFFSETY + offsetY), cover)
cover = model
# Don't apply the gloss effect if asked to
if not prefs.getCmdLine()[0].no_glossy_cover:
gloss = Image.open(THUMBNAIL_GLOSS).convert('RGBA')
cover.paste(gloss, (0, 0), gloss)
# We're done
cover.save(outFile, format)
except:
logger.error('[%s] An error occurred while generating a thumbnail\n\n%s' % (MOD_NAME, traceback.format_exc()))
def getUserCover(self, trackPath):
""" Return the path to a cover file in trackPath, None if no cover found """
splitPath = tools.splitPath(trackPath)
if prefs.get(__name__, 'search-in-parent-dirs', PREFS_DFT_SEARCH_IN_PARENT_DIRS): lvls = len(splitPath)
else: lvls = 1
while lvls != 0:
# Create the path we're currently looking into
currPath = os.path.join(*splitPath)
# Create a dictionary with candidates
candidates = {}
for (file, path) in tools.listDir(currPath, True):
(name, ext) = os.path.splitext(file.lower())
if ext in ACCEPTED_FILE_FORMATS:
candidates[name] = path
# Check each possible name using its index in the list as its priority
for name in prefs.get(__name__, 'user-cover-filenames', PREFS_DFT_USER_COVER_FILENAMES):
if name in candidates:
return candidates[name]
if name == '*' and len(candidates) != 0:
return candidates.values()[0]
# No cover found, let's go one level higher
lvls -= 1
splitPath = splitPath[:-1]
return None
def getFromCache(self, artist, album):
""" Return the path to the cached cover, or None if it's not cached """
cachePath = os.path.join(self.cacheRootPath, str(abs(hash(artist))))
cacheIdxPath = os.path.join(cachePath, 'INDEX')
try:
cacheIdx = tools.pickleLoad(cacheIdxPath)
cover = os.path.join(cachePath, cacheIdx[artist + album])
if os.path.exists(cover):
return cover
except:
pass
return None
def __getFromInternet(self, artist, album):
"""
Try to download the cover from the Internet
If successful, add it to the cache and return the path to it
Otherwise, return None
"""
import socket, urllib2
# Make sure to not be blocked by the request
socket.setdefaulttimeout(consts.socketTimeout)
# Request information to Last.fm
# Beware of UTF-8 characters: we need to percent-encode all characters
try:
url = 'http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=%s&artist=%s&album=%s' % (AS_API_KEY,
tools.percentEncode(artist), tools.percentEncode(album))
request = urllib2.Request(url, headers = {'User-Agent': USER_AGENT})
stream = urllib2.urlopen(request)
data = stream.read()
except urllib2.HTTPError, err:
if err.code == 400:
logger.error('[%s] No known cover for %s / %s' % (MOD_NAME, artist, album))
else:
logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
return None
except:
logger.error('[%s] Information request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
return None
# Extract the URL to the cover image
malformed = True
startIdx = data.find(AS_TAG_START)
endIdx = data.find(AS_TAG_END, startIdx)
if startIdx != -1 and endIdx != -1:
coverURL = data[startIdx+len(AS_TAG_START):endIdx]
coverFormat = os.path.splitext(coverURL)[1].lower()
if coverURL.startswith('http://') and coverFormat in ACCEPTED_FILE_FORMATS:
malformed = False
if malformed:
logger.error('[%s] Received malformed data\n\n%s' % (MOD_NAME, data))
return None
# Download the cover image
try:
request = urllib2.Request(coverURL, headers = {'User-Agent': USER_AGENT})
stream = urllib2.urlopen(request)
data = stream.read()
if len(data) < 1024:
raise Exception, 'The cover image seems incorrect (%u bytes is too small)' % len(data)
except:
logger.error('[%s] Cover image request failed\n\n%s' % (MOD_NAME, traceback.format_exc()))
return None
# So far, so good: let's cache the image
cachePath = os.path.join(self.cacheRootPath, str(abs(hash(artist))))
cacheIdxPath = os.path.join(cachePath, 'INDEX')
if not os.path.exists(cachePath):
os.mkdir(cachePath)
try: cacheIdx = tools.pickleLoad(cacheIdxPath)
except: cacheIdx = {}
nextInt = len(cacheIdx) + 1
filename = str(nextInt) + coverFormat
coverPath = os.path.join(cachePath, filename)
cacheIdx[artist + album] = filename
tools.pickleSave(cacheIdxPath, cacheIdx)
try:
output = open(coverPath, 'wb')
output.write(data)
output.close()
return coverPath
except:
logger.error('[%s] Could not save the downloaded cover\n\n%s' % (MOD_NAME, traceback.format_exc()))
return None
def getFromInternet(self, artist, album):
""" Wrapper for __getFromInternet(), manage blacklist """
# If we already tried without success, don't try again
if (artist, album) in self.coverBlacklist:
return None
# Otherwise, try to download the cover
cover = self.__getFromInternet(artist, album)
# If the download failed, blacklist the album
if cover is None:
self.coverBlacklist[(artist, album)] = None
return cover
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
self.cfgWin = None # Configuration window
self.coverMap = {} # Store covers previously requested
self.currTrack = None # The current track being played, if any
self.cacheRootPath = os.path.join(consts.dirCfg, MOD_NAME) # Local cache for Internet covers
self.coverBlacklist = {} # When a cover cannot be downloaded, avoid requesting it again
if not os.path.exists(self.cacheRootPath):
os.mkdir(self.cacheRootPath)
def onModUnloaded(self):
""" The module has been unloaded """
if self.currTrack is not None:
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': self.currTrack, 'pathThumbnail': None, 'pathFullSize': None})
# Delete covers that have been generated by this module
for covers in self.coverMap.itervalues():
if os.path.exists(covers[CVR_THUMB]):
os.remove(covers[CVR_THUMB])
if os.path.exists(covers[CVR_FULL]):
os.remove(covers[CVR_FULL])
self.coverMap = None
# Delete blacklist
self.coverBlacklist = None
def onNewTrack(self, track):
""" A new track is being played, try to retrieve the corresponding cover """
# Make sure we have enough information
if track.getArtist() == consts.UNKNOWN_ARTIST or track.getAlbum() == consts.UNKNOWN_ALBUM:
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})
return
album = track.getAlbum().lower()
artist = track.getArtist().lower()
rawCover = None
self.currTrack = track
# Let's see whether we already have the cover
if (artist, album) in self.coverMap:
covers = self.coverMap[(artist, album)]
pathFullSize = covers[CVR_FULL]
pathThumbnail = covers[CVR_THUMB]
# Make sure the files are still there
if os.path.exists(pathThumbnail) and os.path.exists(pathFullSize):
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': pathThumbnail, 'pathFullSize': pathFullSize})
return
# Should we check for a user cover?
if not prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS) \
or prefs.get(__name__, 'prefer-user-covers', PREFS_DFT_PREFER_USER_COVERS):
rawCover = self.getUserCover(os.path.dirname(track.getFilePath()))
# Is it in our cache?
if rawCover is None:
rawCover = self.getFromCache(artist, album)
# If we still don't have a cover, maybe we can try to download it
if rawCover is None:
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})
if prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS):
rawCover = self.getFromInternet(artist, album)
# If we still don't have a cover, too bad
# Otherwise, generate a thumbnail and a full size cover, and add it to our cover map
if rawCover is not None:
import tempfile
thumbnail = tempfile.mktemp() + '.png'
fullSizeCover = tempfile.mktemp() + '.png'
self.generateThumbnail(rawCover, thumbnail, 'PNG')
self.generateFullSizeCover(rawCover, fullSizeCover, 'PNG')
if os.path.exists(thumbnail) and os.path.exists(fullSizeCover):
self.coverMap[(artist, album)] = (thumbnail, fullSizeCover)
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': thumbnail, 'pathFullSize': fullSizeCover})
else:
modules.postMsg(consts.MSG_CMD_SET_COVER, {'track': track, 'pathThumbnail': None, 'pathFullSize': None})
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration window """
if self.cfgWin is None:
from gui.window import Window
self.cfgWin = Window('Covers.glade', 'vbox1', __name__, MOD_INFO[modules.MODINFO_L10N], 320, 265)
self.cfgWin.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWin.getWidget('img-lastfm').set_from_file(os.path.join(consts.dirPix, 'audioscrobbler.png'))
self.cfgWin.getWidget('btn-help').connect('clicked', self.onBtnHelp)
self.cfgWin.getWidget('chk-downloadCovers').connect('toggled', self.onDownloadCoversToggled)
self.cfgWin.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWin.hide())
if not self.cfgWin.isVisible():
downloadCovers = prefs.get(__name__, 'download-covers', PREFS_DFT_DOWNLOAD_COVERS)
preferUserCovers = prefs.get(__name__, 'prefer-user-covers', PREFS_DFT_PREFER_USER_COVERS)
userCoverFilenames = prefs.get(__name__, 'user-cover-filenames', PREFS_DFT_USER_COVER_FILENAMES)
searchInParentDirs = prefs.get(__name__, 'search-in-parent-dirs', PREFS_DFT_SEARCH_IN_PARENT_DIRS)
self.cfgWin.getWidget('btn-ok').grab_focus()
self.cfgWin.getWidget('txt-filenames').set_text(', '.join(userCoverFilenames))
self.cfgWin.getWidget('chk-downloadCovers').set_active(downloadCovers)
self.cfgWin.getWidget('chk-preferUserCovers').set_active(preferUserCovers)
self.cfgWin.getWidget('chk-preferUserCovers').set_sensitive(downloadCovers)
self.cfgWin.getWidget('chk-searchInParentDirs').set_active(searchInParentDirs)
self.cfgWin.show()
def onBtnOk(self, btn):
""" Save configuration """
downloadCovers = self.cfgWin.getWidget('chk-downloadCovers').get_active()
preferUserCovers = self.cfgWin.getWidget('chk-preferUserCovers').get_active()
searchInParentDirs = self.cfgWin.getWidget('chk-searchInParentDirs').get_active()
userCoverFilenames = [word.strip() for word in self.cfgWin.getWidget('txt-filenames').get_text().split(',')]
prefs.set(__name__, 'download-covers', downloadCovers)
prefs.set(__name__, 'prefer-user-covers', preferUserCovers)
prefs.set(__name__, 'user-cover-filenames', userCoverFilenames)
prefs.set(__name__, 'search-in-parent-dirs', searchInParentDirs)
self.cfgWin.hide()
def onDownloadCoversToggled(self, downloadCovers):
""" Toggle the "prefer user covers" checkbox according to the state of the "download covers" one """
self.cfgWin.getWidget('chk-preferUserCovers').set_sensitive(downloadCovers.get_active())
def onBtnHelp(self, btn):
""" Display a small help message box """
from gui import help
helpDlg = help.HelpDlg(MOD_INFO[modules.MODINFO_L10N])
helpDlg.addSection(_('Description'),
_('This module displays the cover of the album the current track comes from. Covers '
'may be loaded from local pictures, located in the same directory as the current '
'track, or may be downloaded from the Internet.'))
helpDlg.addSection(_('User Covers'),
_('A user cover is a picture located in the same directory as the current track. '
'When specifying filenames, you do not need to provide file extensions, supported '
'file formats (%s) are automatically used. This module can be configured to search '
'for user covers in parent directories are well.' % ', '.join(ACCEPTED_FILE_FORMATS.iterkeys())))
helpDlg.addSection(_('Internet Covers'),
_('Covers may be downloaded from the Internet, based on the tags of the current track. '
'You can ask to always prefer user covers to Internet ones. In this case, if a user '
'cover exists for the current track, it is used. If there is none, the cover is downloaded.'))
helpDlg.show(self.cfgWin)
./decibel-audio-player-1.06/src/modules/CommandLine.py 0000644 0001750 0001750 00000005222 11456551413 022764 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import media, modules, os.path, traceback
from tools import consts, log, pickleSave, pickleLoad, prefs
MOD_INFO = ('Command Line Support', 'Command Line Support', '', [], True, False, consts.MODCAT_NONE)
class CommandLine(modules.ThreadedModule):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_NEW_TRACKLIST: self.onNewTracklist,
}
modules.ThreadedModule.__init__(self, handlers)
# --== Message handlers ==--
def onAppStarted(self):
""" Try to fill the playlist by using the files given on the command line or by restoring the last playlist """
# The file 'saved-playlist.txt' uses an old format, we now use 'saved-playlist-2.txt'
(options, args) = prefs.getCmdLine()
self.savedPlaylist = os.path.join(consts.dirCfg, 'saved-playlist-2.txt')
if len(args) != 0:
log.logger.info('[%s] Filling playlist with files given on command line' % MOD_INFO[modules.MODINFO_NAME])
modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': media.getTracks(args), 'playNow': True})
else:
try:
tracks = [media.track.unserialize(serialTrack) for serialTrack in pickleLoad(self.savedPlaylist)]
modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': False})
log.logger.info('[%s] Restored playlist' % MOD_INFO[modules.MODINFO_NAME])
except:
log.logger.error('[%s] Unable to restore playlist from %s\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], self.savedPlaylist, traceback.format_exc()))
def onNewTracklist(self, tracks, playtime):
""" A new tracklist has been set """
pickleSave(self.savedPlaylist, [track.serialize() for track in tracks])
./decibel-audio-player-1.06/src/modules/Tracklist.py 0000644 0001750 0001750 00000056600 11456551413 022544 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, gui, media, modules, tools
from gui import fileChooser
from tools import consts, icons
from gettext import gettext as _
from gobject import TYPE_STRING, TYPE_INT, TYPE_PYOBJECT
from gui.extListview import ExtListView
MOD_INFO = ('Tracklist', 'Tracklist', '', [], True, False, consts.MODCAT_NONE)
# Create a unique ID for each field of a row in the list
(
ROW_ICO, # Icon drawn in front of the row
ROW_NUM, # Track number
ROW_TIT, # Track title
ROW_ART, # Track artist
ROW_ALB, # Track album
ROW_LEN, # Track length in seconds
ROW_BTR, # Bit rate
ROW_GNR, # Genre
ROW_DAT, # Date
ROW_PTH, # Path to the file
ROW_TRK # The Track object
) = range(11)
# Create a unique ID for each column that the user can see
(
COL_TRCK_NUM,
COL_TITLE,
COL_ARTIST,
COL_ALBUM,
COL_DATE,
COL_GENRE,
COL_LENGTH,
COL_PATH,
COL_BITRATE,
) = range(9)
PREFS_DEFAULT_REPEAT_STATUS = False
PREFS_DEFAULT_COLUMNS_VISIBILITY = {
COL_TRCK_NUM : True,
COL_TITLE : True,
COL_ARTIST : True,
COL_ALBUM : True,
COL_DATE : False,
COL_GENRE : False,
COL_LENGTH : True,
COL_BITRATE : False,
COL_PATH : False,
}
class Tracklist(modules.Module):
""" This module manages the tracklist """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_CMD_NEXT: self.jumpToNext,
consts.MSG_EVT_PAUSED: lambda: self.onPausedToggled(icons.pauseMenuIcon()),
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: lambda: self.onPausedToggled(icons.playMenuIcon()),
consts.MSG_CMD_PREVIOUS: self.jumpToPrevious,
consts.MSG_EVT_NEED_BUFFER: self.onBufferingNeeded,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_CMD_TOGGLE_PAUSE: self.togglePause,
consts.MSG_CMD_TRACKLIST_DEL: self.remove,
consts.MSG_CMD_TRACKLIST_ADD: self.insert,
consts.MSG_CMD_TRACKLIST_SET: self.set,
consts.MSG_CMD_TRACKLIST_CLR: lambda: self.set(None, None),
consts.MSG_EVT_TRACK_ENDED_OK: lambda: self.onTrackEnded(False),
consts.MSG_CMD_TRACKLIST_REPEAT: self.setRepeat,
consts.MSG_EVT_TRACK_ENDED_ERROR: lambda: self.onTrackEnded(True),
consts.MSG_CMD_TRACKLIST_SHUFFLE: self.shuffleTracklist,
}
modules.Module.__init__(self, handlers)
def __fmtColumnColor(self, col, cll, mdl, it):
""" When playing, tracks already played are slightly greyed out """
style = self.window.get_style()
played = self.list.hasMark() and mdl.get_path(it)[0] < self.list.getMark()
if played: cll.set_property('foreground-gdk', style.text[gtk.STATE_INSENSITIVE])
else: cll.set_property('foreground-gdk', style.text[gtk.STATE_NORMAL])
def __fmtLengthColumn(self, col, cll, mdl, it):
""" Format the column showing the length of the track (e.g., show 1:23 instead of 83) """
cll.set_property('text', tools.sec2str(mdl.get_value(it, ROW_LEN)))
self.__fmtColumnColor(col, cll, mdl, it)
def __getNextTrackIdx(self):
""" Return the index of the next track, or -1 if there is none """
if self.list.hasMark():
if self.list.getMark() < (len(self.list) - 1): return self.list.getMark() + 1
elif self.btnRepeat.get_active(): return 0
return -1
def __hasNextTrack(self):
""" Return whether there is a next track """
return self.__getNextTrackIdx() != -1
def __getPreviousTrackIdx(self):
""" Return the index of the previous track, or -1 if there is none """
if self.list.hasMark():
if self.list.getMark() > 0: return self.list.getMark() - 1
elif self.btnRepeat.get_active(): return len(self.list) - 1
return -1
def __hasPreviousTrack(self):
""" Return whether there is a previous track """
return self.__getPreviousTrackIdx() != -1
def jumpToNext(self):
""" Jump to the next track, if any """
where = self.__getNextTrackIdx()
if where != -1:
self.jumpTo(where)
def jumpToPrevious(self):
""" Jump to the previous track, if any """
where = self.__getPreviousTrackIdx()
if where != -1:
self.jumpTo(where)
def jumpTo(self, trackIdx, sendPlayMsg = True):
""" Jump to the track located at the given index """
if self.list.hasMark() and self.list.getItem(self.list.getMark(), ROW_ICO) != icons.errorMenuIcon():
self.list.setItem(self.list.getMark(), ROW_ICO, icons.nullMenuIcon())
self.list.setMark(trackIdx)
self.list.scroll_to_cell(trackIdx)
self.list.setItem(trackIdx, ROW_ICO, icons.playMenuIcon())
if sendPlayMsg:
modules.postMsg(consts.MSG_CMD_PLAY, {'uri': self.list.getItem(trackIdx, ROW_TRK).getURI()})
modules.postMsg(consts.MSG_EVT_NEW_TRACK, {'track': self.list.getRow(trackIdx)[ROW_TRK]})
modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def insert(self, tracks, playNow, position=None):
""" Insert some tracks in the tracklist, append them if position is None """
rows = [[icons.nullMenuIcon(), track.getNumber(), track.getTitle(), track.getArtist(), track.getExtendedAlbum(),
track.getLength(), track.getBitrate(), track.getGenre(), track.getDate(), track.getURI(), track] for track in tracks]
if len(rows) != 0:
self.previousTracklist = [row[ROW_TRK] for row in self.list]
for row in rows:
self.playtime += row[ROW_LEN]
self.list.insertRows(rows, position)
if playNow:
if position is not None: self.jumpTo(position)
else: self.jumpTo(len(self.previousTracklist))
def set(self, tracks, playNow):
""" Replace the tracklist, clear it if tracks is None """
self.playtime = 0
# Save playlist only locally to this function
# The insert() function would overwrite it otherwise
previousTracklist = [row[ROW_TRK] for row in self.list]
if self.list.hasMark() and ((not playNow) or (tracks is None) or (len(tracks) == 0)):
modules.postMsg(consts.MSG_CMD_STOP)
self.list.clear()
if tracks is not None and len(tracks) != 0:
self.insert(tracks, playNow)
self.previousTracklist = previousTracklist
def savePlaylist(self):
""" Save the current tracklist to a playlist """
outFile = fileChooser.save(self.window, _('Save playlist'), 'playlist.m3u')
if outFile is not None:
allFiles = [row[ROW_TRK].getFilePath() for row in self.list.iterAllRows()]
media.playlist.save(allFiles, outFile)
def remove(self, idx=None):
""" Remove the given track, or the selection if idx is None """
if idx is not None and (idx < 0 or idx >= len(self.list)):
return
hadMark = self.list.hasMark()
self.previousTracklist = [row[ROW_TRK] for row in self.list]
if idx is not None:
self.playtime -= self.list.getRow(idx)[ROW_LEN]
self.list.removeRow((idx, ))
else:
self.playtime -= sum([row[ROW_LEN] for row in self.list.iterSelectedRows()])
self.list.removeSelectedRows()
self.list.unselectAll()
if hadMark and not self.list.hasMark():
modules.postMsg(consts.MSG_CMD_STOP)
def crop(self):
""" Remove the unselected tracks """
hadMark = self.list.hasMark()
self.previousTracklist = [row[ROW_TRK] for row in self.list]
self.playtime = sum([row[ROW_LEN] for row in self.list.iterSelectedRows()])
self.list.cropSelectedRows()
if hadMark and not self.list.hasMark():
modules.postMsg(consts.MSG_CMD_STOP)
def revertTracklist(self):
""" Back to the previous tracklist """
self.set(self.previousTracklist, False)
self.previousTracklist = None
def shuffleTracklist(self):
""" Shuffle the tracks and ensure that the current track stays visible """
self.previousTracklist = [row[ROW_TRK] for row in self.list]
self.list.shuffle()
if self.list.hasMark():
self.list.scroll_to_cell(self.list.getMark())
def setRepeat(self, repeat):
""" Set/Unset the repeat function """
if self.btnRepeat.get_active() != repeat:
self.btnRepeat.clicked()
def showPopupMenu(self, list, path, button, time):
""" The index parameter may be None """
popup = gtk.Menu()
# Crop
crop = gtk.ImageMenuItem(_('Crop'))
crop.set_image(gtk.image_new_from_stock(gtk.STOCK_CUT, gtk.ICON_SIZE_MENU))
popup.append(crop)
if path is None: crop.set_sensitive(False)
else: crop.connect('activate', lambda item: self.crop())
# Remove
remove = gtk.ImageMenuItem(gtk.STOCK_REMOVE)
popup.append(remove)
if path is None: remove.set_sensitive(False)
else: remove.connect('activate', lambda item: self.remove())
popup.append(gtk.SeparatorMenuItem())
# Shuffle
shuffle = gtk.ImageMenuItem(_('Shuffle Playlist'))
shuffle.set_image(gtk.image_new_from_icon_name('stock_shuffle', gtk.ICON_SIZE_MENU))
popup.append(shuffle)
if len(list) == 0: shuffle.set_sensitive(False)
else: shuffle.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE))
# Revert
revert = gtk.ImageMenuItem(_('Revert Playlist'))
revert.set_image(gtk.image_new_from_stock(gtk.STOCK_REVERT_TO_SAVED, gtk.ICON_SIZE_MENU))
popup.append(revert)
if self.previousTracklist is None: revert.set_sensitive(False)
else: revert.connect('activate', lambda item: self.revertTracklist())
# Clear
clear = gtk.ImageMenuItem(_('Clear Playlist'))
clear.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU))
popup.append(clear)
if len(list) == 0: clear.set_sensitive(False)
else: clear.connect('activate', lambda item: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))
popup.append(gtk.SeparatorMenuItem())
# Repeat
repeat = gtk.CheckMenuItem(_('Repeat'))
repeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS))
repeat.connect('toggled', lambda item: self.btnRepeat.clicked())
popup.append(repeat)
popup.append(gtk.SeparatorMenuItem())
# Save
save = gtk.ImageMenuItem(_('Save Playlist As...'))
save.set_image(gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
popup.append(save)
if len(list) == 0: save.set_sensitive(False)
else: save.connect('activate', lambda item: self.savePlaylist())
popup.show_all()
popup.popup(None, None, None, button, time)
def togglePause(self):
""" Start playing if not already playing """
if len(self.list) != 0 and not self.list.hasMark():
if self.list.getSelectedRowsCount() != 0:
self.jumpTo(self.list.getFirstSelectedRowIndex())
else:
self.jumpTo(0)
# --== Message handlers ==--
def onAppStarted(self):
""" This is the real initialization function, called when the module has been loaded """
wTree = tools.prefs.getWidgetsTree()
self.playtime = 0
self.bufferedTrack = None
self.previousTracklist = None
# Retrieve widgets
self.window = wTree.get_widget('win-main')
self.btnClear = wTree.get_widget('btn-tracklistClear')
self.btnRepeat = wTree.get_widget('btn-tracklistRepeat')
self.btnShuffle = wTree.get_widget('btn-tracklistShuffle')
self.btnClear.set_sensitive(False)
self.btnShuffle.set_sensitive(False)
# Create the list and its columns
txtLRdr = gtk.CellRendererText()
txtRRdr = gtk.CellRendererText()
pixbufRdr = gtk.CellRendererPixbuf()
txtRRdr.set_property('xalign', 1.0)
# 'columns-visibility' may be broken, we should not use it (#311293)
visible = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY)
for (key, value) in PREFS_DEFAULT_COLUMNS_VISIBILITY.iteritems():
if key not in visible:
visible[key] = value
columns = (('#', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRRdr, TYPE_INT)], (ROW_NUM, ROW_TIT), False, visible[COL_TRCK_NUM]),
(_('Title'), [(txtLRdr, TYPE_STRING)], (ROW_TIT,), True, visible[COL_TITLE]),
(_('Artist'), [(txtLRdr, TYPE_STRING)], (ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ARTIST]),
(_('Album'), [(txtLRdr, TYPE_STRING)], (ROW_ALB, ROW_NUM, ROW_TIT), True, visible[COL_ALBUM]),
(_('Length'), [(txtRRdr, TYPE_INT)], (ROW_LEN,), False, visible[COL_LENGTH]),
(_('Bit Rate'), [(txtRRdr, TYPE_STRING)], (ROW_BTR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_BITRATE]),
(_('Genre'), [(txtLRdr, TYPE_STRING)], (ROW_GNR, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_GENRE]),
(_('Date'), [(txtLRdr, TYPE_INT)], (ROW_DAT, ROW_ART, ROW_ALB, ROW_NUM, ROW_TIT), False, visible[COL_DATE]),
(_('Path'), [(txtLRdr, TYPE_STRING)], (ROW_PTH,), False, visible[COL_PATH]),
(None, [(None, TYPE_PYOBJECT)], (None,), False, False))
self.list = ExtListView(columns, sortable=True, dndTargets=consts.DND_TARGETS.values(), useMarkup=False, canShowHideColumns=True)
self.list.get_column(1).set_cell_data_func(txtLRdr, self.__fmtColumnColor)
self.list.get_column(4).set_cell_data_func(txtRRdr, self.__fmtLengthColumn)
self.list.enableDNDReordering()
wTree.get_widget('scrolled-tracklist').add(self.list)
# GTK handlers
self.list.connect('extlistview-dnd', self.onDND)
self.list.connect('key-press-event', self.onKeyboard)
self.list.connect('extlistview-modified', self.onListModified)
self.list.connect('extlistview-button-pressed', self.onButtonPressed)
self.list.connect('extlistview-selection-changed', self.onSelectionChanged)
self.list.connect('extlistview-column-visibility-changed', self.onColumnVisibilityChanged)
self.btnClear.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_CLR))
self.btnRepeat.connect('toggled', self.onButtonRepeat)
self.btnShuffle.connect('clicked', lambda widget: modules.postMsg(consts.MSG_CMD_TRACKLIST_SHUFFLE))
# Restore preferences
self.btnRepeat.set_active(tools.prefs.get(__name__, 'repeat-status', PREFS_DEFAULT_REPEAT_STATUS))
# Set icons
wTree.get_widget('img-repeat').set_from_icon_name('stock_repeat', gtk.ICON_SIZE_BUTTON)
wTree.get_widget('img-shuffle').set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_BUTTON)
def onTrackEnded(self, withError):
""" The current track has ended, jump to the next one if any """
currIdx = self.list.getMark()
# If an error occurred with the current track, flag it as such
if withError:
self.list.setItem(currIdx, ROW_ICO, icons.errorMenuIcon())
# Find the next 'playable' track (not already flagged)
if self.btnRepeat.get_active(): nbTracks = len(self.list)
else: nbTracks = len(self.list) - 1 - currIdx
for i in xrange(nbTracks):
currIdx = (currIdx + 1) % len(self.list)
if self.list.getItem(currIdx, ROW_ICO) != icons.errorMenuIcon():
track = self.list.getItem(currIdx, ROW_TRK).getURI()
self.jumpTo(currIdx, track != self.bufferedTrack)
self.bufferedTrack = None
return
self.bufferedTrack = None
modules.postMsg(consts.MSG_CMD_STOP)
def onBufferingNeeded(self):
""" The current track is close to its end, so we try to buffer the next one to avoid gaps """
where = self.__getNextTrackIdx()
if where != -1:
self.bufferedTrack = self.list.getItem(where, ROW_TRK).getURI()
modules.postMsg(consts.MSG_CMD_BUFFER, {'uri': self.bufferedTrack})
def onStopped(self):
""" Playback has been stopped """
if self.list.hasMark():
currTrack = self.list.getMark()
if self.list.getItem(currTrack, ROW_ICO) != icons.errorMenuIcon():
self.list.setItem(currTrack, ROW_ICO, icons.nullMenuIcon())
self.list.clearMark()
def onPausedToggled(self, icon):
""" Switch between paused and unpaused """
if self.list.hasMark():
self.list.setItem(self.list.getMark(), ROW_ICO, icon)
# --== GTK handlers ==--
def onButtonRepeat(self, btn):
""" The 'repeat' button has been pressed """
tools.prefs.set(__name__, 'repeat-status', self.btnRepeat.get_active())
modules.postMsg(consts.MSG_EVT_REPEAT_CHANGED, {'repeat': self.btnRepeat.get_active()})
if self.list.hasMark():
modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def onButtonPressed(self, list, event, path):
""" Play the selected track on double click, or show a popup menu on right click """
if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None:
self.jumpTo(path[0])
elif event.button == 3:
self.showPopupMenu(list, path, event.button, event.time)
def onKeyboard(self, list, event):
""" Keyboard shortcuts """
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'Delete': self.remove()
elif keyname == 'Return': self.jumpTo(self.list.getFirstSelectedRowIndex())
elif keyname == 'space': modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
elif keyname == 'Escape': modules.postMsg(consts.MSG_CMD_STOP)
elif keyname == 'Left': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': -5})
elif keyname == 'Right': modules.postMsg(consts.MSG_CMD_STEP, {'seconds': 5})
def onListModified(self, list):
""" Some rows have been added/removed/moved """
self.btnClear.set_sensitive(len(list) != 0)
self.btnShuffle.set_sensitive(len(list) != 0)
# Update playlist length and playlist position for all tracks
for position, row in enumerate(self.list):
row[ROW_TRK].setPlaylistPos(position + 1)
row[ROW_TRK].setPlaylistLen(len(self.list))
allTracks = [row[ROW_TRK] for row in self.list]
modules.postMsg(consts.MSG_EVT_NEW_TRACKLIST, {'tracks': allTracks, 'playtime': self.playtime})
if self.list.hasMark():
modules.postMsg(consts.MSG_EVT_TRACK_MOVED, {'hasPrevious': self.__hasPreviousTrack(), 'hasNext': self.__hasNextTrack()})
def onSelectionChanged(self, list, selectedRows):
""" The selection has changed """
modules.postMsg(consts.MSG_EVT_TRACKLIST_NEW_SEL, {'tracks': [row[ROW_TRK] for row in selectedRows]})
def onColumnVisibilityChanged(self, list, colTitle, visible):
""" A column has been shown/hidden """
if colTitle == '#': colId = COL_TRCK_NUM
elif colTitle == _('Title'): colId = COL_TITLE
elif colTitle == _('Artist'): colId = COL_ARTIST
elif colTitle == _('Album'): colId = COL_ALBUM
elif colTitle == _('Length'): colId = COL_LENGTH
elif colTitle == _('Genre'): colId = COL_GENRE
elif colTitle == _('Date'): colId = COL_DATE
elif colTitle == _('Bit Rate'): colId = COL_BITRATE
else: colId = COL_PATH
visibility = tools.prefs.get(__name__, 'columns-visibility-2', PREFS_DEFAULT_COLUMNS_VISIBILITY)
visibility[colId] = visible
tools.prefs.set(__name__, 'columns-visibility-2', visibility)
def onDND(self, list, context, x, y, dragData, dndId, time):
""" External Drag'n'Drop """
import urllib
if dragData.data == '':
context.finish(False, False, time)
return
# A list of filenames, without 'file://' at the beginning
if dndId == consts.DND_DAP_URI:
tracks = media.getTracks([urllib.url2pathname(uri) for uri in dragData.data.split()])
# A list of filenames starting with 'file://'
elif dndId == consts.DND_URI:
tracks = media.getTracks([urllib.url2pathname(uri)[7:] for uri in dragData.data.split()])
# A list of tracks
elif dndId == consts.DND_DAP_TRACKS:
tracks = [media.track.unserialize(serialTrack) for serialTrack in dragData.data.split('\n')]
dropInfo = list.get_dest_row_at_pos(x, y)
# Insert the tracks, but beware of the AFTER/BEFORE mechanism used by GTK
if dropInfo is None: self.insert(tracks, False)
elif dropInfo[1] == gtk.TREE_VIEW_DROP_AFTER: self.insert(tracks, False, dropInfo[0][0] + 1)
else: self.insert(tracks, False, dropInfo[0][0])
context.finish(True, False, time)
./decibel-audio-player-1.06/src/modules/IMStatus.py 0000644 0001750 0001750 00000037273 11456551413 022322 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules, traceback
from tools import consts, prefs
from gettext import gettext as _
from tools.log import logger
MOD_INFO = ('Instant Messenger Status', _('Instant Messenger Status'), _('Update the status message of your IM client'), [], False, True, consts.MODCAT_DESKTOP)
MOD_NAME = MOD_INFO[modules.MODINFO_NAME]
# Possible actions upon stopping or quitting
(
STOP_DO_NOTHING,
STOP_SET_STATUS
) = range(2)
# Default preferences
DEFAULT_STATUS_MSG = '♫ {artist} - {album} ♫'
DEFAULT_STOP_ACTION = STOP_SET_STATUS
DEFAULT_STOP_STATUS = _('Decibel is stopped')
DEFAULT_SANITIZED_WORDS = ''
DEFAULT_UPDATE_ON_PAUSED = True
DEFAULT_UPDATE_WHEN_AWAY = False
##############################################################################
class Gaim:
def __init__(self, dbusInterface):
""" Constructor """
self.dbusInterface = dbusInterface
def listAccounts(self):
""" Return a default account """
return ['GenericAccount']
def setStatusMsg(self, account, msg):
""" Change the status message of the given account """
try:
current = self.dbusInterface.GaimSavedstatusGetCurrent()
statusType = self.dbusInterface.GaimSavedstatusGetType(current)
statusId = self.dbusInterface.GaimPrimitiveGetIdFromType(statusType)
if statusId == 'available' or prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY):
saved = self.dbusInterface.GaimSavedstatusNew('', statusType)
self.dbusInterface.GaimSavedstatusSetMessage(saved, msg)
self.dbusInterface.GaimSavedstatusActivate(saved)
except:
logger.error('[%s] Unable to set Gaim status\n\n%s' % (MOD_NAME, traceback.format_exc()))
##############################################################################
class Gajim:
def __init__(self, dbusInterface):
""" Constructor """
self.dbusInterface = dbusInterface
def listAccounts(self):
""" Return a list of existing accounts """
try:
return [account for account in self.dbusInterface.list_accounts()]
except:
logger.error('[%s] Unable to list Gajim accounts\n\n%s' % (MOD_NAME, traceback.format_exc()))
return []
def setStatusMsg(self, account, msg):
""" Change the status message of the given account """
try:
currentStatus = self.dbusInterface.get_status(account)
if currentStatus in ('online', 'chat') or prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY):
self.dbusInterface.change_status(currentStatus, msg, account)
except:
logger.error('[%s] Unable to set Gajim status\n\n%s' % (MOD_NAME, traceback.format_exc()))
##############################################################################
class Gossip:
def __init__(self, dbusInterface):
""" Constructor """
self.dbusInterface = dbusInterface
def listAccounts(self):
""" Return a default account """
return ['GenericAccount']
def setStatusMsg(self, account, msg):
""" Change the status message of the given account """
try:
currentStatus, currentMsg = self.dbusInterface.GetPresence('')
if currentStatus == 'available' or prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY):
self.dbusInterface.SetPresence(currentStatus, msg)
except:
logger.error('[%s] Unable to set Gossip status\n\n%s' % (MOD_NAME, traceback.format_exc()))
##############################################################################
class Pidgin:
def __init__(self, dbusInterface):
""" Constructor """
self.dbusInterface = dbusInterface
def listAccounts(self):
""" Return a default account """
return ['GenericAccount']
def setStatusMsg(self, account, msg):
""" Change the status message of the given account """
try:
current = self.dbusInterface.PurpleSavedstatusGetCurrent()
# This used to be needed, but seems to have been fixed in Pidgin
# statusType = dbus.UInt32(self.dbusInterface.PurpleSavedstatusGetType(current))
statusType = self.dbusInterface.PurpleSavedstatusGetType(current)
statusId = self.dbusInterface.PurplePrimitiveGetIdFromType(statusType)
if statusId == 'available' or prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY):
saved = self.dbusInterface.PurpleSavedstatusNew('', statusType)
self.dbusInterface.PurpleSavedstatusSetMessage(saved, msg)
self.dbusInterface.PurpleSavedstatusActivate(saved)
except:
logger.error('[%s] Unable to set Pidgin status\n\n%s' % (MOD_NAME, traceback.format_exc()))
##############################################################################
# Elements associated with each supported IM clients
(
IM_NAME,
IM_DBUS_SERVICE_NAME,
IM_DBUS_OBJECT_NAME,
IM_DBUS_INTERFACE_NAME,
IM_CLASS,
IM_INSTANCE,
IM_ACCOUNTS
) = range(7)
# All specific classes have been defined, so we can now populate the list of supported IM clients
CLIENTS = (
['Gajim', 'org.gajim.dbus', '/org/gajim/dbus/RemoteObject', 'org.gajim.dbus.RemoteInterface', Gajim, None, []],
['Gossip', 'org.gnome.Gossip', '/org/gnome/Gossip', 'org.gnome.Gossip', Gossip, None, []],
['Gaim', 'net.sf.gaim.GaimService', '/net/sf/gaim/GaimObject', 'net.sf.gaim.GaimInterface', Gaim, None, []],
['Pidgin', 'im.pidgin.purple.PurpleService', '/im/pidgin/purple/PurpleObject', 'im.pidgin.purple.PurpleInterfacep', Pidgin, None, []]
)
class IMStatus(modules.Module):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_APP_QUIT: self.onStopped,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onStopped,
}
modules.Module.__init__(self, handlers)
def __format(self, string, track):
""" Replace the special fields in the given string by their corresponding value and sanitize the result """
result = track.format(string)
if len(prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS)) != 0:
lowerResult = result.lower()
for word in [w.lower() for w in prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS).split('\n') if len(w) > 2]:
pos = lowerResult.find(word)
while pos != -1:
result = result[:pos+1] + ('*' * (len(word)-2)) + result[pos+len(word)-1:]
lowerResult = lowerResult[:pos+1] + ('*' * (len(word)-2)) + lowerResult[pos+len(word)-1:]
pos = lowerResult.find(word)
return result
def setStatusMsg(self, status):
""" Update the status of all accounts of all active IM clients """
for client in self.clients:
for account in client[IM_ACCOUNTS]:
client[IM_INSTANCE].setStatusMsg(account, status)
# --== Message handlers ==--
def onModLoaded(self):
""" Initialize the module """
self.track = None # Current track
self.status = '' # The currently used status
self.paused = False # True if the current track is paused
self.clients = [] # Clients currently active
self.cfgWindow = None # Configuration window
# Detect active clients
try:
import dbus
session = dbus.SessionBus()
activeServices = session.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus').ListNames()
for activeClient in [client for client in CLIENTS if client[IM_DBUS_SERVICE_NAME] in activeServices]:
obj = session.get_object(activeClient[IM_DBUS_SERVICE_NAME], activeClient[IM_DBUS_OBJECT_NAME])
interface = dbus.Interface(obj, activeClient[IM_DBUS_INTERFACE_NAME])
activeClient[IM_INSTANCE] = activeClient[IM_CLASS](interface)
activeClient[IM_ACCOUNTS] = activeClient[IM_INSTANCE].listAccounts()
logger.info('[%s] Found %s instance' % (MOD_NAME, activeClient[IM_NAME]))
self.clients.append(activeClient)
except:
logger.error('[%s] Error while initializing\n\n%s' % (MOD_NAME, traceback.format_exc()))
def onNewTrack(self, track):
""" A new track is being played """
self.track = track
self.status = self.__format(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG), track)
self.paused = False
self.setStatusMsg(self.status)
def onStopped(self):
""" The current track has been stopped """
self.track = None
self.paused = False
if prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS:
self.setStatusMsg(prefs.get(__name__, 'stop-status', DEFAULT_STOP_STATUS))
def onPaused(self):
""" The current track has been paused """
self.paused = True
if prefs.get(__name__, 'update-on-paused', DEFAULT_UPDATE_ON_PAUSED):
self.setStatusMsg(_('%(status)s [paused]') % {'status': self.status})
def onUnpaused(self):
""" The current track has been unpaused """
self.paused = False
self.setStatusMsg(self.status)
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration window """
if self.cfgWindow is None:
from gui.window import Window
self.cfgWindow = Window('IMStatus.glade', 'vbox1', __name__, _(MOD_NAME), 440, 290)
# GTK handlers
self.cfgWindow.getWidget('rad-stopDoNothing').connect('toggled', self.onRadToggled)
self.cfgWindow.getWidget('rad-stopSetStatus').connect('toggled', self.onRadToggled)
self.cfgWindow.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide())
self.cfgWindow.getWidget('btn-help').connect('clicked', self.onBtnHelp)
if not self.cfgWindow.isVisible():
self.cfgWindow.getWidget('txt-status').set_text(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG))
self.cfgWindow.getWidget('chk-updateOnPaused').set_active(prefs.get(__name__, 'update-on-paused', DEFAULT_UPDATE_ON_PAUSED))
self.cfgWindow.getWidget('chk-updateWhenAway').set_active(prefs.get(__name__, 'update-when-away', DEFAULT_UPDATE_WHEN_AWAY))
self.cfgWindow.getWidget('rad-stopDoNothing').set_active(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_DO_NOTHING)
self.cfgWindow.getWidget('rad-stopSetStatus').set_active(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS)
self.cfgWindow.getWidget('txt-stopStatus').set_sensitive(prefs.get(__name__, 'stop-action', DEFAULT_STOP_ACTION) == STOP_SET_STATUS)
self.cfgWindow.getWidget('txt-stopStatus').set_text(prefs.get(__name__, 'stop-status', DEFAULT_STOP_STATUS))
self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().set_text(prefs.get(__name__, 'sanitized-words', DEFAULT_SANITIZED_WORDS))
self.cfgWindow.getWidget('btn-ok').grab_focus()
self.cfgWindow.show()
def onRadToggled(self, btn):
""" A radio button has been toggled """
self.cfgWindow.getWidget('txt-stopStatus').set_sensitive(self.cfgWindow.getWidget('rad-stopSetStatus').get_active())
def onBtnOk(self, btn):
""" Save new preferences """
prefs.set(__name__, 'status-msg', self.cfgWindow.getWidget('txt-status').get_text())
prefs.set(__name__, 'update-on-paused', self.cfgWindow.getWidget('chk-updateOnPaused').get_active())
prefs.set(__name__, 'update-when-away', self.cfgWindow.getWidget('chk-updateWhenAway').get_active())
(start, end) = self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().get_bounds()
prefs.set(__name__, 'sanitized-words', self.cfgWindow.getWidget('txt-sanitizedWords').get_buffer().get_text(start, end).strip())
if self.cfgWindow.getWidget('rad-stopDoNothing').get_active():
prefs.set(__name__, 'stop-action', STOP_DO_NOTHING)
else:
prefs.set(__name__, 'stop-action', STOP_SET_STATUS)
prefs.set(__name__, 'stop-status', self.cfgWindow.getWidget('txt-stopStatus').get_text())
self.cfgWindow.hide()
# Update status
if self.track is not None:
self.status = self.__format(prefs.get(__name__, 'status-msg', DEFAULT_STATUS_MSG), self.track)
if self.paused: self.setStatusMsg(_('%(status)s [paused]') % {'status': self.status})
else: self.setStatusMsg(self.status)
def onBtnHelp(self, btn):
""" Display a small help message box """
import gui.help, media.track
helpDlg = gui.help.HelpDlg(_(MOD_NAME))
helpDlg.addSection(_('Description'),
_('This module detects any running instant messenger and updates your status with regards to the track '
'you are listening to. Supported messengers are:')
+ '\n\n * ' + '\n * '.join(sorted([client[IM_NAME] for client in CLIENTS])))
helpDlg.addSection(_('Customizing the Status'),
_('You can set the status to any text you want. Before setting it, the module replaces all fields of '
'the form {field} by their corresponding value. Available fields are:')
+ '\n\n' + media.track.getFormatSpecialFields(False))
helpDlg.addSection(_('Markup'),
_('You can use the Pango markup language to format the text. More information on that language is '
'available on the following web page:')
+ '\n\nhttp://www.pygtk.org/pygtk2reference/pango-markup-language.html')
helpDlg.addSection(_('Sanitization'),
_('You can define some words that to sanitize before using them to set your status. In this '
'case, the middle characters of matching words is automatically replaced with asterisks '
'(e.g., "Metallica - Live S**t Binge & Purge"). Put one word per line.'))
helpDlg.show(self.cfgWindow)
./decibel-audio-player-1.06/src/modules/ReplayGain.py 0000644 0001750 0001750 00000003322 11456551413 022630 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules
from tools import consts
from gettext import gettext as _
MOD_INFO = ('ReplayGain', _('ReplayGain'), _('Normalize volume'), [], False, False, consts.MODCAT_DECIBEL)
class ReplayGain(modules.Module):
""" This module enables the GStreamer ReplayGain element """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_MOD_LOADED: self.onRestartRequired,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_MOD_UNLOADED: self.onRestartRequired,
}
modules.Module.__init__(self, handlers)
# --== Message handlers ==--
def onAppStarted(self):
""" The application has just been started """
modules.postMsg(consts.MSG_CMD_ENABLE_RG)
def onRestartRequired(self):
""" A restart of the application is required """
self.restartRequired()
./decibel-audio-player-1.06/src/modules/StatusbarTitlebar.py 0000644 0001750 0001750 00000011557 11456551413 024245 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules, tools
from tools import consts, prefs
from gettext import ngettext, gettext as _
MOD_INFO = ('Status and Title Bars', 'Status and Title Bars', '', [], True, False, consts.MODCAT_NONE)
class StatusbarTitlebar(modules.Module):
""" This module manages both the status and the title bars """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_NEW_TRACKLIST: self.onNewTracklist,
consts.MSG_EVT_TRACKLIST_NEW_SEL: self.onNewSelection,
}
modules.Module.__init__(self, handlers)
def __updateTitlebar(self):
""" Update the title bar """
if self.currTrack is None: self.window.set_title(consts.appName)
elif self.paused: self.window.set_title('%s - %s %s' % (self.currTrack.getArtist(), self.currTrack.getTitle(), _('[paused]')))
else: self.window.set_title('%s - %s' % (self.currTrack.getArtist(), self.currTrack.getTitle()))
def __updateStatusbar(self):
""" Update the status bar """
# Tracklist
count = len(self.tracklist)
if count == 0:
self.status1.set_label('')
else:
self.status1.set_label(ngettext('One track in playlist [%(length)s]', '%(count)u tracks in playlist [%(length)s]', count) \
% {'count': count, 'length': tools.sec2str(self.playtime)})
# Selected tracks
count = len(self.selTracks)
if count == 0:
self.status2.set_label('')
else:
selection = ngettext('One track selected', '%(count)u tracks selected', count) % {'count': count}
audioType = self.selTracks[0].getType()
for track in self.selTracks[1:]:
if track.getType() != audioType:
audioType = _('various')
break
bitrate = self.selTracks[0].getBitrate()
for track in self.selTracks[1:]:
if track.getBitrate() != bitrate:
bitrate = _('various')
break
self.status2.set_label(_('%(selection)s (Type: %(type)s, Bitrate: %(bitrate)s)') % {'selection': selection, 'type': audioType, 'bitrate': bitrate})
# --== Message handlers ==--
def onAppStarted(self):
""" Real initialization function, called when this module has been loaded """
self.window = prefs.getWidgetsTree().get_widget('win-main')
self.status1 = prefs.getWidgetsTree().get_widget('lbl-status1')
self.status2 = prefs.getWidgetsTree().get_widget('lbl-status2')
# Current player status
self.paused = False
self.playtime = 0
self.tracklist = []
self.selTracks = []
self.currTrack = None
def onNewTrack(self, track):
""" A new track is being played """
self.paused = False
self.currTrack = track
self.__updateTitlebar()
def onPaused(self):
""" Playback has been paused """
self.paused = True
self.__updateTitlebar()
def onUnpaused(self):
""" Playback has been unpaused """
self.paused = False
self.__updateTitlebar()
def onStopped(self):
""" Playback has been stopped """
self.paused = False
self.currTrack = None
self.__updateTitlebar()
def onNewTracklist(self, tracks, playtime):
""" A new tracklist has been set """
self.playtime = playtime
self.tracklist = tracks
self.__updateStatusbar()
def onNewSelection(self, tracks):
""" A new set of track has been selected """
self.selTracks = tracks
self.__updateStatusbar()
./decibel-audio-player-1.06/src/modules/GSTPlayer.py 0000644 0001750 0001750 00000016633 11456551413 022420 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, modules
from time import time
from tools import consts, prefs
from media import audioplayer
MOD_INFO = ('GStreamer Player', 'GStreamer Player', '', [], True, False, consts.MODCAT_NONE)
MIN_PLAYBACK_DELAY = 1.5
class GSTPlayer(modules.Module):
""" This module is the 'real' GStreamer player """
def __init__(self):
""" Constructor """
# The player must be created during the application startup, not when the application is ready (MSG_EVT_APP_STARTED)
self.player = audioplayer.AudioPlayer(self.__onTrackEnded, not prefs.getCmdLine()[0].playbin)
handlers = {
consts.MSG_CMD_STEP: self.onStep,
consts.MSG_CMD_STOP: self.onStop,
consts.MSG_CMD_PLAY: self.onPlay,
consts.MSG_CMD_SEEK: self.onSeek,
consts.MSG_CMD_BUFFER: self.onBuffer,
consts.MSG_CMD_ENABLE_RG: self.onEnableReplayGain,
consts.MSG_CMD_ENABLE_EQZ: self.onEnableEqualizer,
consts.MSG_CMD_SET_VOLUME: self.onSetVolume,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_CMD_SET_CD_SPEED: self.onSetCDSpeed,
consts.MSG_CMD_TOGGLE_PAUSE: self.onTogglePause,
consts.MSG_CMD_SET_EQZ_LVLS: self.onSetEqualizerLevels,
}
modules.Module.__init__(self, handlers)
def updateTimerHandler(self):
""" Regularly called during playback (can be paused) """
if self.player.isPlaying():
position = self.player.getPosition()
remaining = self.player.getDuration() - position
modules.postMsg(consts.MSG_EVT_TRACK_POSITION, {'seconds': int(position / 1000000000)})
if remaining < 5000000000 and self.nextURI is None and not prefs.getCmdLine()[0].playbin:
modules.postMsg(consts.MSG_EVT_NEED_BUFFER)
return True
def __startUpdateTimer(self):
""" Start the update timer if needed """
if self.updateTimer is None:
self.updateTimer = gobject.timeout_add(1000, self.updateTimerHandler)
def __stopUpdateTimer(self):
""" Start the update timer if needed """
if self.updateTimer is not None:
gobject.source_remove(self.updateTimer)
self.updateTimer = None
def onBuffer(self, uri):
""" Buffer the next track """
if not prefs.getCmdLine()[0].playbin:
self.nextURI = uri
self.player.setNextURI(uri)
def __onTrackEnded(self, error):
""" Called to signal eos and errors """
self.nextURI = None
if error: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_ERROR)
else: modules.postMsg(consts.MSG_EVT_TRACK_ENDED_OK)
def __playbackTimerHandler(self):
""" Switch the player to playback mode, and start the update timer """
if not self.player.isPlaying():
self.player.play()
self.nextURI = None
self.lastPlayback = time()
self.playbackTimer = None
self.__startUpdateTimer()
return False
# --== Message handlers ==--
def onAppStarted(self):
""" This is the real initialization function, called when this module has been loaded """
self.nextURI = None
self.queuedSeek = None
self.updateTimer = None
self.lastPlayback = 0
self.playbackTimer = None
def onPlay(self, uri):
""" Play the given URI """
if uri != self.nextURI:
self.player.stop()
self.player.setURI(uri)
self.__stopUpdateTimer()
elapsed = time() - self.lastPlayback
# Looks like GStreamer can be pretty much fucked if we start/stop the pipeline too quickly (e.g., when clicking "next" very fast)
# We minimize the load in these extreme cases by ensuring that at least one second has elapsed since the last playback
# Note that this delay is avoided when tracks are chained, since the playback of the next track has then already started (uri == self.nextURI)
if elapsed >= MIN_PLAYBACK_DELAY:
self.__playbackTimerHandler()
else:
if self.playbackTimer is not None:
gobject.source_remove(self.playbackTimer)
self.playbackTimer = gobject.timeout_add(int((MIN_PLAYBACK_DELAY - elapsed) * 1000), self.__playbackTimerHandler)
def onStop(self):
""" Stop playing """
self.__stopUpdateTimer()
self.player.stop()
self.nextURI = None
if self.playbackTimer is not None:
gobject.source_remove(self.playbackTimer)
modules.postMsg(consts.MSG_EVT_STOPPED)
def onTogglePause(self):
""" Switch between play/pause """
if self.player.isPaused():
if self.queuedSeek is not None:
self.player.seek(self.queuedSeek*1000000000)
self.queuedSeek = None
self.player.play()
modules.postMsg(consts.MSG_EVT_UNPAUSED)
elif self.player.isPlaying():
if self.playbackTimer is not None:
gobject.source_remove(self.playbackTimer)
self.player.pause()
modules.postMsg(consts.MSG_EVT_PAUSED)
def onSeek(self, seconds):
""" Jump to the given position if playing, or buffer it if paused """
if self.player.isPaused(): self.queuedSeek = seconds
elif self.player.isPlaying(): self.player.seek(seconds*1000000000)
def onStep(self, seconds):
""" Step back or forth """
if self.player.isPlaying():
newPos = self.player.getPosition() + (seconds * 1000000000)
if newPos < 0:
self.player.seek(0)
self.updateTimerHandler()
elif newPos < self.player.getDuration():
self.player.seek(newPos)
self.updateTimerHandler()
def onSetVolume(self, value):
""" Change the volume """
self.player.setVolume(value)
modules.postMsg(consts.MSG_EVT_VOLUME_CHANGED, {'value': value})
def onEnableReplayGain(self):
""" Enable replay gain """
self.player.enableReplayGain()
def onEnableEqualizer(self):
""" Enable the equalizer """
self.player.enableEqualizer()
def onSetEqualizerLevels(self, lvls):
""" Set the equalizer levels """
self.player.setEqualizerLvls(lvls)
def onSetCDSpeed(self, speed):
""" Set the CD read speed """
self.player.setCDReadSpeed(speed)
./decibel-audio-player-1.06/src/modules/GnomeMediaKeys.py 0000644 0001750 0001750 00000006325 11456551413 023444 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import dbus, modules, traceback
from time import time
from tools import consts, log
MOD_INFO = ('Gnome Media Keys', 'Gnome Media Keys', '', [], True, False, consts.MODCAT_NONE)
# Generate a 'unique' application name so that multiple instances won't interfere with each other
APP_UID = consts.appName + str(time())
class GnomeMediaKeys(modules.Module):
""" Support for Gnome multimedia keys """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onAppQuit,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
}
modules.Module.__init__(self, handlers)
def onMediaKey(self, appName, action):
""" A media key has been pressed """
if action == 'Stop': modules.postMsg(consts.MSG_CMD_STOP)
elif action == 'Next': modules.postMsg(consts.MSG_CMD_NEXT)
elif action == 'Previous': modules.postMsg(consts.MSG_CMD_PREVIOUS)
elif action in ['Play', 'Pause']: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
# --== Message handlers ==--
def onAppStarted(self):
""" The application has started """
# Try first with the new interface (Gnome >= 2.2)
try:
service = dbus.SessionBus().get_object('org.gnome.SettingsDaemon', '/org/gnome/SettingsDaemon/MediaKeys')
self.dbusInterface = dbus.Interface(service, 'org.gnome.SettingsDaemon.MediaKeys')
self.dbusInterface.GrabMediaPlayerKeys(APP_UID, time())
self.dbusInterface.connect_to_signal('MediaPlayerKeyPressed', self.onMediaKey)
except:
# If it didn't work, try the old way
try:
service = dbus.SessionBus().get_object('org.gnome.SettingsDaemon', '/org/gnome/SettingsDaemon')
self.dbusInterface = dbus.Interface(service, 'org.gnome.SettingsDaemon')
self.dbusInterface.GrabMediaPlayerKeys(APP_UID, time())
self.dbusInterface.connect_to_signal('MediaPlayerKeyPressed', self.onMediaKey)
except:
log.logger.error('[%s] Error while initializing\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
self.dbusInterface = None
def onAppQuit(self):
""" The application is about to terminate """
if self.dbusInterface is not None:
self.dbusInterface.ReleaseMediaPlayerKeys(APP_UID)
./decibel-audio-player-1.06/src/modules/StatusIcon.py 0000644 0001750 0001750 00000023101 11456551413 022666 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, modules
from tools import consts, loadGladeFile, prefs
from gettext import gettext as _
MOD_INFO = ('Status Icon', _('Status Icon'), _('Add an icon to the notification area'), [], False, False, consts.MODCAT_DESKTOP)
class StatusIcon(modules.Module):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_TRACK_MOVED: self.onTrackMoved,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
consts.MSG_EVT_NEW_TRACKLIST: self.onNewTracklist,
consts.MSG_EVT_VOLUME_CHANGED: self.onVolumeChanged,
}
modules.Module.__init__(self, handlers)
def renderIcons(self, statusIcon, availableSize):
""" (Re) Create icons based the available tray size """
# Normal icon
if availableSize >= 48+2: self.icoNormal = gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon48)
elif availableSize >= 32+2: self.icoNormal = gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon32)
elif availableSize >= 24+2: self.icoNormal = gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon24)
else: self.icoNormal = gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon16)
# Paused icon
self.icoPause = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, self.icoNormal.get_width(), self.icoNormal.get_height())
self.icoPause.fill(0x00000000)
self.icoNormal.composite(self.icoPause, 0, 0, self.icoNormal.get_width(), self.icoNormal.get_height(), 0, 0, 1, 1, gtk.gdk.INTERP_HYPER, 100)
if self.icoNormal.get_width() == 16: pauseStock = self.mainWindow.render_icon(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU)
else: pauseStock = self.mainWindow.render_icon(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_BUTTON)
diffX = self.icoPause.get_width() - pauseStock.get_width()
diffY = self.icoPause.get_height() - pauseStock.get_height()
pauseStock.composite(self.icoPause, 0, 0, pauseStock.get_width(), pauseStock.get_height(), diffX/2, diffY/2, 1, 1, gtk.gdk.INTERP_HYPER, 255)
# Use the correct icon
if self.isPaused: statusIcon.set_from_pixbuf(self.icoPause)
else: statusIcon.set_from_pixbuf(self.icoNormal)
def toggleWinVisibility(self, statusIcon):
""" Show/hide the main window """
if not self.isMainWinVisible:
self.mainWindow.show()
self.isMainWinVisible = True
elif self.mainWindow.has_toplevel_focus():
self.mainWindow.hide()
self.isMainWinVisible = False
else:
self.mainWindow.hide()
self.mainWindow.show()
# --== Message handlers ==--
def onModLoaded(self):
""" Install the Status icon """
self.volume = 0
self.tooltip = consts.appName
self.isPaused = False
self.icoPause = None
self.popupMenu = None
self.isPlaying = False
self.icoNormal = None
self.mainWindow = prefs.getWidgetsTree().get_widget('win-main')
self.trackHasNext = False
self.trackHasPrev = False
self.emptyTracklist = True
self.isMainWinVisible = True
# The status icon does not support RGBA, so make sure to use the RGB color map when creating it
gtk.widget_push_colormap(self.mainWindow.get_screen().get_rgb_colormap())
self.statusIcon = gtk.StatusIcon()
gtk.widget_pop_colormap()
# GTK+ handlers
self.statusIcon.connect('activate', self.toggleWinVisibility)
self.statusIcon.connect('popup-menu', self.onPopupMenu)
self.statusIcon.connect('size-changed', self.renderIcons)
self.statusIcon.connect('scroll-event', self.onScroll)
self.statusIcon.connect('button-press-event', self.onButtonPressed)
# Install everything
self.statusIcon.set_tooltip(consts.appName)
self.onNewTrack(None)
self.statusIcon.set_visible(True)
def onModUnloaded(self):
""" Uninstall the Status icon """
self.statusIcon.set_visible(False)
self.statusIcon = None
if not self.isMainWinVisible:
self.mainWindow.show()
self.isMainWinVisible = True
def onNewTrack(self, track):
""" A new track is being played, None if none """
if track is None: self.tooltip = consts.appName
else: self.tooltip = '%s - %s' % (track.getArtist(), track.getTitle())
self.isPaused = False
self.isPlaying = track is not None
self.statusIcon.set_from_pixbuf(self.icoNormal)
self.statusIcon.set_tooltip(self.tooltip)
def onPaused(self):
""" The current track has been paused """
self.isPaused = True
self.statusIcon.set_from_pixbuf(self.icoPause)
self.statusIcon.set_tooltip(_('%(tooltip)s [paused]') % {'tooltip': self.tooltip})
def onUnpaused(self):
""" The current track has been unpaused """
self.isPaused = False
self.statusIcon.set_from_pixbuf(self.icoNormal)
self.statusIcon.set_tooltip(self.tooltip)
def onTrackMoved(self, hasPrevious, hasNext):
""" The position of the current track in the playlist has changed """
self.trackHasNext = hasNext
self.trackHasPrev = hasPrevious
def onVolumeChanged(self, value):
""" The volume has changed """
self.volume = value
def onNewTracklist(self, tracks, playtime):
""" A new tracklist has been defined """
if len(tracks) == 0: self.emptyTracklist = True
else: self.emptyTracklist = False
def onStopped(self):
""" The playback has been stopped """
self.onNewTrack(None)
# --== GTK handlers ==--
def onPopupMenu(self, statusIcon, button, time):
""" The user asks for the popup menu """
if self.popupMenu is None:
wTree = loadGladeFile('StatusIconMenu.glade')
self.menuPlay = wTree.get_widget('item-play')
self.menuStop = wTree.get_widget('item-stop')
self.menuNext = wTree.get_widget('item-next')
self.popupMenu = wTree.get_widget('menu-popup')
self.menuPause = wTree.get_widget('item-pause')
self.menuPrevious = wTree.get_widget('item-previous')
self.menuSeparator = wTree.get_widget('item-separator')
# Connect handlers
wTree.get_widget('item-quit').connect('activate', lambda btn: modules.postQuitMsg())
wTree.get_widget('item-preferences').connect('activate', lambda btn: modules.showPreferences())
self.menuPlay.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE))
self.menuStop.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_STOP))
self.menuNext.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_NEXT))
self.menuPrevious.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_PREVIOUS))
self.menuPause.connect('activate', lambda btn: modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE))
self.popupMenu.show_all()
# Enable only relevant menu entries
self.menuStop.set_sensitive(self.isPlaying)
self.menuNext.set_sensitive(self.isPlaying and self.trackHasNext)
self.menuPause.set_sensitive(self.isPlaying and not self.isPaused)
self.menuPrevious.set_sensitive(self.isPlaying and self.trackHasPrev)
self.menuPlay.set_sensitive((not (self.isPlaying or self.emptyTracklist)) or self.isPaused)
self.popupMenu.popup(None, None, gtk.status_icon_position_menu, button, time, statusIcon)
def onScroll(self, statusIcon, scrollEvent):
""" The mouse is scrolled on the status icon """
if scrollEvent.direction == gtk.gdk.SCROLL_UP or scrollEvent.direction == gtk.gdk.SCROLL_RIGHT:
self.volume = min(1.0, self.volume + 0.05)
else:
self.volume = max(0.0, self.volume - 0.05)
modules.postMsg(consts.MSG_CMD_SET_VOLUME, {'value': self.volume})
def onButtonPressed(self, statusIcon, buttonEvent):
""" A button is pressed on the status icon """
if buttonEvent.button == 2:
modules.postMsg(consts.MSG_CMD_TOGGLE_PAUSE)
./decibel-audio-player-1.06/src/modules/DBus.py 0000644 0001750 0001750 00000034207 11456551413 021440 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import dbus, dbus.service, gobject, media, modules, traceback
from tools import consts, log
MOD_INFO = ('D-Bus Support', 'D-Bus Support', '', [], True, False, consts.MODCAT_NONE)
# MPRIS caps constants
CAPS_CAN_GO_NEXT = 1
CAPS_CAN_GO_PREV = 2
CAPS_CAN_PAUSE = 4
CAPS_CAN_PLAY = 8
CAPS_CAN_SEEK = 16
CAPS_CAN_PROVIDE_METADATA = 32
CAPS_CAN_HAS_TRACKLIST = 64
class DBus(modules.Module):
""" Enable D-Bus support """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_TRACK_MOVED: self.onCurrentTrackMoved,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_NEW_TRACKLIST: self.onNewTracklist,
consts.MSG_EVT_VOLUME_CHANGED: self.onVolumeChanged,
consts.MSG_EVT_TRACK_POSITION: self.onNewTrackPosition,
consts.MSG_EVT_REPEAT_CHANGED: self.onRepeatChanged,
}
modules.Module.__init__(self, handlers)
def getMPRISCaps(self):
""" Return an integer sticking to the MPRIS caps definition """
caps = CAPS_CAN_HAS_TRACKLIST
if len(self.tracklist) != 0:
caps |= CAPS_CAN_PLAY
if self.currTrack is not None:
caps |= CAPS_CAN_PAUSE
caps |= CAPS_CAN_SEEK
caps |= CAPS_CAN_PROVIDE_METADATA
if self.canGoNext: caps |= CAPS_CAN_GO_NEXT
if self.canGoPrev: caps |= CAPS_CAN_GO_PREV
return caps
def getMPRISStatus(self):
""" Return a tuple sticking to the MPRIS status definition """
if self.currTrack is None: playStatus = 2
elif self.paused: playStatus = 1
else: playStatus = 0
if self.repeat: repeatStatus = 1
else: repeatStatus = 0
return (playStatus, 0, 0, repeatStatus)
# --== Message handlers ==--
def onAppStarted(self):
""" Initialize this module """
self.repeat = False
self.paused = False
self.tracklist = []
self.currTrack = None
self.canGoNext = False
self.canGoPrev = False
self.currVolume = 0
self.currPosition = 0
try:
self.sessionBus = dbus.SessionBus()
self.busName = dbus.service.BusName(consts.dbusService, bus=self.sessionBus)
# Create the three MPRIS objects
self.busObjectRoot = DBusObjectRoot(self.busName, self)
self.busObjectPlayer = DBusObjectPlayer(self.busName, self)
self.busObjectTracklist = DBusObjectTracklist(self.busName, self)
except:
self.sessionBus = None
log.logger.error('[%s] Error while initializing\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
def onNewTrack(self, track):
""" A new track is being played """
self.paused = False
self.currTrack = track
self.busObjectPlayer.CapsChange(self.getMPRISCaps())
self.busObjectPlayer.TrackChange(track.getMPRISMetadata())
self.busObjectPlayer.StatusChange(self.getMPRISStatus())
def onStopped(self):
""" Playback is stopped """
self.paused = False
self.currTrack = None
self.currPosition = 0
self.busObjectPlayer.CapsChange(self.getMPRISCaps())
self.busObjectPlayer.StatusChange(self.getMPRISStatus())
def onVolumeChanged(self, value):
""" The volume has been changed """
self.currVolume = value
def onNewTrackPosition(self, seconds):
""" New position in the current track """
self.currPosition = seconds
def onPaused(self):
""" The playback has been paused """
self.paused = True
self.busObjectPlayer.StatusChange(self.getMPRISStatus())
def onUnpaused(self):
""" The playback has been unpaused """
self.paused = False
self.busObjectPlayer.StatusChange(self.getMPRISStatus())
def onNewTracklist(self, tracks, playtime):
""" A new tracklist has been set """
self.tracklist = tracks
self.busObjectPlayer.CapsChange(self.getMPRISCaps())
self.busObjectTracklist.TrackListChange(len(tracks))
def onCurrentTrackMoved(self, hasNext, hasPrevious):
""" The position of the current track has moved in the playlist """
self.canGoNext = hasNext
self.canGoPrev = hasPrevious
self.busObjectPlayer.CapsChange(self.getMPRISCaps())
def onRepeatChanged(self, repeat):
""" Repeat has been enabled/disabled """
self.repeat = repeat
self.busObjectPlayer.StatusChange(self.getMPRISStatus())
class DBusObjectRoot(dbus.service.Object):
def __init__(self, busName, module):
""" Constructor """
dbus.service.Object.__init__(self, busName, '/')
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='s')
def Identity(self):
""" Returns a string containing the media player identification """
return '%s %s' % (consts.appName, consts.appVersion)
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Quit(self):
""" Makes the media player exit """
modules.postQuitMsg()
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='(qq)')
def MprisVersion(self):
""" Returns a struct that represents the version of the MPRIS spec being implemented """
return (1, 0)
class DBusObjectTracklist(dbus.service.Object):
def __init__(self, busName, module):
""" Constructor """
self.module = module
dbus.service.Object.__init__(self, busName, '/TrackList')
@dbus.service.method(consts.dbusInterface, in_signature='i', out_signature='a{sv}')
def GetMetadata(self, idx):
""" Gives all meta data available for element at given position in the TrackList, counting from 0 """
if idx >= 0 and idx < len(self.module.tracklist): return self.module.tracklist[idx].getMPRISMetadata()
else: return {}
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='i')
def GetCurrentTrack(self):
""" Return the position of current URI in the TrackList """
if self.module.currTrack is None: return -1
else: return self.module.currTrack.getPlaylistPos()-1
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='i')
def GetLength(self):
""" Number of elements in the TrackList """
return len(self.module.tracklist)
@dbus.service.method(consts.dbusInterface, in_signature='sb', out_signature='i')
def AddTrack(self, uri, playNow):
""" Appends an URI to the TrackList """
import urllib
decodedURI = urllib.unquote(uri)
if decodedURI.startswith('file://'):
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_ADD, {'tracks': [media.getTrackFromFile(decodedURI[7:])], 'playNow': playNow})
return 0
return 1
@dbus.service.method(consts.dbusInterface, in_signature='i', out_signature='')
def DelTrack(self, idx):
""" Removes an URI from the TrackList """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_DEL, {'idx': idx})
@dbus.service.method(consts.dbusInterface, in_signature='b', out_signature='')
def SetLoop(self, loop):
""" Toggle playlist loop """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_REPEAT, {'repeat': loop})
@dbus.service.method(consts.dbusInterface, in_signature='b', out_signature='')
def SetRandom(self, random):
""" Toggle playlist shuffle / random """
if random:
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_SHUFFLE)
@dbus.service.signal(consts.dbusInterface, signature='i')
def TrackListChange(self, length):
""" Signal is emitted when the tracklist content has changed """
pass
# These functions are not part of the MPRIS, but are useful
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Clear(self):
""" Clear the tracklist """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_CLR)
@dbus.service.method(consts.dbusInterface, in_signature='asb', out_signature='')
def AddTracks(self, uris, playNow):
""" Appends multiple URIs to the tracklist """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_ADD, {'tracks': media.getTracks([file for file in uris]), 'playNow': playNow})
@dbus.service.method(consts.dbusInterface, in_signature='asb', out_signature='')
def SetTracks(self, uris, playNow):
""" Replace the tracklist by the given URIs """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TRACKLIST_SET, {'tracks': media.getTracks([file for file in uris]), 'playNow': playNow})
class DBusObjectPlayer(dbus.service.Object):
def __init__(self, busName, module):
""" Constructor """
self.module = module
dbus.service.Object.__init__(self, busName, '/Player')
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Next(self):
""" Goes to the next element """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_NEXT)
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Prev(self):
""" Goes to the previous element """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_PREVIOUS)
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Pause(self):
""" If playing : pause. If paused : unpause """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_TOGGLE_PAUSE)
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Stop(self):
""" Stop playing """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_STOP)
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='')
def Play(self):
""" If playing : rewind to the beginning of current track, else : start playing """
if len(self.module.tracklist) != 0:
if self.module.currTrack is None: gobject.idle_add(modules.postMsg, consts.MSG_CMD_TOGGLE_PAUSE)
else: gobject.idle_add(modules.postMsg, consts.MSG_CMD_SEEK, {'seconds': 0})
@dbus.service.method(consts.dbusInterface, in_signature='b', out_signature='')
def Repeat(self, repeat):
""" Toggle the current track repeat """
# We don't support repeating only the current track
pass
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='(iiii)')
def GetStatus(self):
""" Return the status of media player as a struct of 4 ints """
return self.module.getMPRISStatus()
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='a{sv}')
def GetMetadata(self):
""" Gives all meta data available for the currently played element """
if self.module.currTrack is None: return {}
else: return self.module.currTrack.getMPRISMetadata()
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='i')
def GetCaps(self):
""" Return the media player's current capabilities """
return self.module.getMPRISCaps()
@dbus.service.method(consts.dbusInterface, in_signature='i', out_signature='')
def VolumeSet(self, volume):
""" Sets the volume (argument must be in [0;100]) """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_SET_VOLUME, {'value': volume / 100.0})
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='i')
def VolumeGet(self):
""" Returns the current volume (must be in [0;100]) """
return self.module.currVolume * 100
@dbus.service.method(consts.dbusInterface, in_signature='i', out_signature='')
def PositionSet(self, position):
""" Sets the playing position (argument must be in [0;] in milliseconds) """
gobject.idle_add(modules.postMsg, consts.MSG_CMD_SEEK, {'seconds': position / 1000})
@dbus.service.method(consts.dbusInterface, in_signature='', out_signature='i')
def PositionGet(self):
""" Returns the playing position (will be [0;] in milliseconds) """
return self.module.currPosition * 1000
@dbus.service.signal(consts.dbusInterface, signature='a{sv}')
def TrackChange(self, metadata):
""" Signal is emitted when the media player plays another track """
pass
@dbus.service.signal(consts.dbusInterface, signature='(iiii)')
def StatusChange(self, status):
""" Signal is emitted when the status of the media player change """
pass
@dbus.service.signal(consts.dbusInterface, signature='i')
def CapsChange(self, caps):
""" Signal is emitted when the media player changes capabilities """
pass
./decibel-audio-player-1.06/src/modules/Explorer.py 0000644 0001750 0001750 00000016150 11456551413 022400 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, modules
from tools import consts, icons, prefs
from gettext import gettext as _
MOD_INFO = ('Explorer', 'Explorer', '', [], True, False, consts.MODCAT_NONE)
DEFAULT_LAST_EXPLORER = ('', '') # Module name and explorer name
# The rows in the combo box
(
ROW_PIXBUF, # Icon displayed in front of the entry
ROW_NAME, # Name of the entry
ROW_MODULE, # Name of the module the entry is associated to
ROW_PAGE_NUM, # Number of the notebook page the entry is associated to
ROW_IS_HEADER, # True if the entry is an header (used to separate different modules)
) = range(5)
class Explorer(modules.Module):
""" This module manages the left part of the GUI, with all the exploration stuff """
def __init__(self):
""" Constructor """
modules.Module.__init__(self, {
consts.MSG_CMD_EXPLORER_ADD: self.onAddExplorer,
consts.MSG_CMD_EXPLORER_REMOVE: self.onRemoveExplorer,
consts.MSG_CMD_EXPLORER_RENAME: self.onRenameExplorer,
})
# Attributes
self.store = gtk.ListStore(gtk.gdk.Pixbuf, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_INT, gobject.TYPE_BOOLEAN)
self.combo = prefs.getWidgetsTree().get_widget('combo-explorer')
txtRenderer = gtk.CellRendererText()
pixRenderer = gtk.CellRendererPixbuf()
self.timeout = None
self.notebook = prefs.getWidgetsTree().get_widget('notebook-explorer')
self.allExplorers = {}
self.notebookPages = {}
self.currExplorerIdx = 0
# Setup the combo box
txtRenderer.set_property('xpad', 6)
self.combo.pack_start(pixRenderer, False)
self.combo.add_attribute(pixRenderer, 'pixbuf', ROW_PIXBUF)
self.combo.pack_start(txtRenderer, True)
self.combo.add_attribute(txtRenderer, 'markup', ROW_NAME)
self.combo.set_sensitive(False)
self.combo.set_cell_data_func(txtRenderer, self.__cellDataFunction)
self.combo.set_model(self.store)
# Setup the notebook
label = gtk.Label(_('Please select an explorer\nin the combo box below.'))
label.show()
self.notebook.append_page(label)
# GTK handlers
self.combo.connect('changed', self.onChanged)
def __cellDataFunction(self, combo, renderer, model, iter):
""" Use a different format for headers """
if model.get_value(iter, ROW_IS_HEADER): renderer.set_property('xalign', 0.5)
else: renderer.set_property('xalign', 0.0)
def __fillComboBox(self):
""" Fill the combo box """
idx = self.combo.get_active()
restoredIdx = None
self.timeout = None
previousModule = None
if idx == -1: selectedModule, selectedExplorer = prefs.get(__name__, 'last-explorer', DEFAULT_LAST_EXPLORER)
else: selectedModule, selectedExplorer = self.store[idx][ROW_MODULE], self.store[idx][ROW_NAME]
self.combo.freeze_child_notify()
self.store.clear()
for (module, explorer), (pixbuf, widget) in sorted(self.allExplorers.iteritems()):
if module != previousModule:
self.store.append((None, '%s' % module, '', -1, True))
previousModule = module
self.store.append((pixbuf, explorer, module, self.notebookPages[widget], False))
if module == selectedModule and explorer == selectedExplorer:
restoredIdx = len(self.store) - 1
if restoredIdx is None:
self.currExplorerIdx = 0
self.notebook.set_current_page(0)
else:
self.combo.set_active(restoredIdx)
self.combo.set_sensitive(len(self.store) != 0)
self.combo.thaw_child_notify()
return False
def fillComboBox(self):
"""
Wrapper function for __fillComboBox()
Call fillComboBox() after a small timeout, to avoid many (useless) consecutive calls to __fillComboBox()
"""
if self.timeout is not None:
gobject.source_remove(self.timeout)
self.timeout = gobject.timeout_add(100, self.__fillComboBox)
# --== Message handlers ==--
def onAddExplorer(self, modName, expName, icon, widget):
""" Add a new explorer to the combo box """
if widget not in self.notebookPages:
self.notebookPages[widget] = self.notebook.append_page(widget)
self.allExplorers[(modName, expName)] = (icon, widget)
self.fillComboBox()
def onRemoveExplorer(self, modName, expName):
""" Remove an existing explorer from the combo box """
del self.allExplorers[(modName, expName)]
self.fillComboBox()
def onRenameExplorer(self, modName, expName, newExpName):
""" Rename the given explorer """
if newExpName != expName:
self.allExplorers[(modName, newExpName)] = self.allExplorers[(modName, expName)]
del self.allExplorers[(modName, expName)]
# If the explorer we're renaming is currently selected, we need to rename the row
# Otherwise, fillComboBox() won't be able to keep it selected
idx = self.combo.get_active()
if idx != -1 and self.store[idx][ROW_MODULE] == modName and self.store[idx][ROW_NAME] == expName:
self.store[idx][ROW_NAME] = newExpName
prefs.set(__name__, 'last-explorer', (modName, newExpName))
self.fillComboBox()
# --== GTK handlers ==--
def onChanged(self, combo):
""" A new explorer has been selected with the combo box """
idx = combo.get_active()
if idx == -1:
self.notebook.set_current_page(0)
elif self.store[idx][ROW_IS_HEADER]:
combo.set_active(self.currExplorerIdx)
else:
self.currExplorerIdx = idx
prefs.set(__name__, 'last-explorer', (self.store[idx][ROW_MODULE], self.store[idx][ROW_NAME]))
modules.postMsg(consts.MSG_EVT_EXPLORER_CHANGED, {'modName': self.store[idx][ROW_MODULE], 'expName': self.store[idx][ROW_NAME]})
self.notebook.set_current_page(self.store[idx][ROW_PAGE_NUM])
./decibel-audio-player-1.06/src/modules/Zeitgeist.py 0000644 0001750 0001750 00000006261 11456551413 022551 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Authors: Ingelrest François (Francois.Ingelrest@gmail.com),
# Jendrik Seipp (jendrikseipp@web.de)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules, traceback
from tools import consts
from gettext import gettext as _
from tools.log import logger
MOD_INFO = ('Zeitgeist', 'Zeitgeist', _('Send track information to the Zeitgeist service'), ['zeitgeist'], False, False, consts.MODCAT_DESKTOP)
class Zeitgeist(modules.ThreadedModule):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
}
modules.ThreadedModule.__init__(self, handlers)
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
self.client = None
try:
from zeitgeist.client import ZeitgeistClient
self.client = ZeitgeistClient()
except:
logger.info('[%s] Could not create Zeitgeist client\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
def onModUnloaded(self):
""" The module has been unloaded """
self.client = None
def onNewTrack(self, track):
""" Send track information to Zeitgeist """
import mimetypes, os.path
from zeitgeist.datamodel import Event, Subject, Interpretation, Manifestation
mime, encoding = mimetypes.guess_type(track.getFilePath(), strict=False)
subject = Subject.new_for_values(
uri = os.path.dirname(track.getURI()),
text = track.getTitle() + ' - ' + track.getArtist() + ' - ' + track.getExtendedAlbum(),
mimetype = mime,
manifestation = unicode(Manifestation.FILE),
interpretation = unicode(Interpretation.AUDIO),
)
if hasattr(Interpretation, 'ACCESS_EVENT'):
eventType = Interpretation.ACCESS_EVENT
else:
eventType = Interpretation.OPEN_EVENT
event = Event.new_for_values(
actor = "application://decibel-audio-player.desktop",
subjects = [subject,],
interpretation = eventType,
)
self.client.insert_event(event)
./decibel-audio-player-1.06/src/modules/StatusFile.py 0000644 0001750 0001750 00000012110 11456551413 022653 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gui, modules, os.path
from media import track
from tools import consts, prefs
from gettext import gettext as _
MOD_INFO = ('Status File', _('Status File'), _('Generate a text file with the current status'), [], False, True, consts.MODCAT_DESKTOP)
# Default preferences
PREFS_DEFAULT_FILE = os.path.join(consts.dirCfg, 'now-playing.txt')
PREFS_DEFAULT_STATUS = 'Now playing {title}\nby {artist}'
class StatusFile(modules.ThreadedModule):
""" Allow external programs to display the current status """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_STOPPED: self.onClearFile,
consts.MSG_EVT_APP_QUIT: self.onClearFile,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_UNLOADED: self.onClearFile,
}
modules.ThreadedModule.__init__(self, handlers)
self.cfgWindow = None
def configure(self, parent):
""" Show the configuration window """
if self.cfgWindow is None:
self.cfgWindow = gui.window.Window('StatusFile.glade', 'vbox1', __name__, MOD_INFO[modules.MODINFO_L10N], 355, 345)
self.btnOk = self.cfgWindow.getWidget('btn-ok')
self.txtFile = self.cfgWindow.getWidget('txt-file')
self.txtStatus = self.cfgWindow.getWidget('txt-status')
# GTK handlers
self.btnOk.connect('clicked', self.onOk)
self.cfgWindow.getWidget('btn-help').connect('clicked', self.onHelp)
self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide())
self.cfgWindow.getWidget('btn-open').connect('clicked', self.onOpen)
# Fill current preferences
if not self.cfgWindow.isVisible():
self.txtFile.set_text(prefs.get(__name__, 'file', PREFS_DEFAULT_FILE))
self.txtStatus.get_buffer().set_text(prefs.get(__name__, 'status', PREFS_DEFAULT_STATUS))
self.btnOk.grab_focus()
self.cfgWindow.show()
def updateFile(self, track):
""" Show the notification based on the given track """
output = open(prefs.get(__name__, 'file', PREFS_DEFAULT_FILE), 'w')
if track is None: output.write('')
else: output.write(track.format(prefs.get(__name__, 'status', PREFS_DEFAULT_STATUS)))
output.close()
# --== Message handlers ==--
def onNewTrack(self, track):
""" A new track is being played """
self.updateFile(track)
def onClearFile(self):
""" Erase the contents of the file """
self.updateFile(None)
# --== GTK handlers ==--
def onOk(self, btn):
""" Save the new preferences """
if not os.path.isdir(os.path.dirname(self.txtFile.get_text())):
gui.errorMsgBox(self.cfgWindow, _('Invalid path'), _('The path to the selected file is not valid. Please choose an existing path.'))
self.txtFile.grab_focus()
else:
prefs.set(__name__, 'file', self.txtFile.get_text())
(start, end) = self.txtStatus.get_buffer().get_bounds()
prefs.set(__name__, 'status', self.txtStatus.get_buffer().get_text(start, end))
self.cfgWindow.hide()
def onOpen(self, btn):
""" Let the user choose a file """
dir = os.path.dirname(self.txtFile.get_text())
file = os.path.basename(self.txtFile.get_text())
result = gui.fileChooser.save(self.cfgWindow, _('Choose a file'), file, dir)
if result is not None:
self.txtFile.set_text(result)
def onHelp(self, btn):
""" Display a small help message box """
helpDlg = gui.help.HelpDlg(MOD_INFO[modules.MODINFO_L10N])
helpDlg.addSection(_('Description'),
_('This module generates a text file with regards to the track currently played.'))
helpDlg.addSection(_('Customizing the File'),
_('You can change the content of the file to any text you want. Before generating the file, '
'fields of the form {field} are replaced by their corresponding value. '
'Available fields are:\n\n') + track.getFormatSpecialFields(False))
helpDlg.show(self.cfgWindow)
./decibel-audio-player-1.06/src/modules/AudioCD.py 0000644 0001750 0001750 00000045312 11456551413 022052 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, gui, modules, os, tools, traceback
from gui import extTreeview
from tools import consts, icons, prefs, sec2str
from gettext import gettext as _
from tools.log import logger
from media.track.cdTrack import CDTrack
MOD_INFO = ('Audio CD', _('Audio CD'), _('Play audio discs'), ('DiscID', 'CDDB'), False, True, consts.MODCAT_EXPLORER)
MOD_L10N = MOD_INFO[modules.MODINFO_L10N]
PREFS_DFT_DEVICE = '/dev/cdrom'
PREFS_DFT_USE_CDDB = True
PREFS_DFT_USE_CACHE = True
PREFS_DFT_READ_SPEED = 1
# Format of a row in the treeview
(
ROW_PIXBUF,
ROW_LENGTH,
ROW_NAME,
ROW_TRACK
) = range(4)
# All CD-ROM read speeds
READ_SPEEDS = {
1 : 0,
2 : 1,
4 : 2,
8 : 3,
10 : 4,
12 : 5,
20 : 6,
32 : 7,
36 : 8,
40 : 9,
48 : 10,
50 : 11,
52 : 12,
56 : 13,
72 : 14,
}
# Information returned by disc_id()
DISC_FRAME1 = 2
DISC_FRAMEn = -2
DISC_LENGTH = -1
DISC_CHECKSUM = 0
DISC_NB_TRACKS = 1
class AudioCD(modules.ThreadedModule):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
consts.MSG_EVT_EXPLORER_CHANGED: self.onExplorerChanged,
}
modules.ThreadedModule.__init__(self, handlers)
def __drawAlbumLenCell(self, column, cell, model, iter):
""" Use a different background color for alphabetical headers """
if model.get_value(iter, ROW_LENGTH) is None: cell.set_property('visible', False)
else: cell.set_property('visible', True)
def getTracksFromPaths(self, tree, paths):
"""
Return a list of tracks with all the associated tags:
* From the list 'paths' if it is not None
* From the current selection if 'paths' is None
"""
if paths is None:
if tree.isRowSelected((0,)): return [tree.getItem(child, ROW_TRACK) for child in tree.iterChildren((0,))]
else: return [row[ROW_TRACK] for row in tree.getSelectedRows()]
else:
if (0,) in paths: return [tree.getItem(child, ROW_TRACK) for child in tree.iterChildren((0,))]
else: return [row[ROW_TRACK] for row in tree.getRows(paths)]
def playPaths(self, tree, paths, replace):
"""
Replace/extend the tracklist
If the list 'paths' is None, use the current selection
"""
if self.tree.getNbChildren((0,)) != 0:
tracks = self.getTracksFromPaths(tree, paths)
if replace: modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': True})
else: modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': False})
# --== Cache management ==--
def clearCache(self):
""" Clear cache content """
for file in os.listdir(self.cacheDir):
os.remove(os.path.join(self.cacheDir, file))
def isDiscInCache(self, discInfo):
""" Return whether the given disc is present in the cache """
return os.path.exists(os.path.join(self.cacheDir, str(discInfo[DISC_CHECKSUM])))
def getDiscFromCache(self, discInfo):
""" Return CDDB information from the cache, or None if that disc is not cached """
try: return tools.pickleLoad(os.path.join(self.cacheDir, str(discInfo[DISC_CHECKSUM])))
except: return None
def addDiscToCache(self, discInfo, cddb):
""" Add the given CDDB information to the cache """
if not os.path.exists(self.cacheDir):
os.mkdir(self.cacheDir)
try: tools.pickleSave(os.path.join(self.cacheDir, str(discInfo[DISC_CHECKSUM])), cddb)
except: pass
# --== Gui management, these functions must be executed in the GTK main loop ==--
def createTree(self, nbTracks):
""" Create a temporary explorer tree without disc information """
name = '%s %s' % (MOD_L10N, _('downloading data...'))
self.tree.replaceContent(((icons.cdromMenuIcon(), None, name, None),))
# Append a child for each track
self.tree.appendRows([(icons.mediaFileMenuIcon(), None, _('Track %02u') % (i+1), None) for i in xrange(nbTracks)], (0,))
self.tree.expand_all()
def updateTree(self, discInfo):
""" Update the tree using disc information from the cache, if any """
cddb = self.getDiscFromCache(discInfo)
# Create fake CDDB information if needed
if cddb is None:
cddb = {'DTITLE': '%s / %s' % (consts.UNKNOWN_ARTIST, consts.UNKNOWN_ALBUM)}
for i in xrange(discInfo[DISC_NB_TRACKS]):
cddb['TTITLE%u' % i] = consts.UNKNOWN_TITLE
# Compute the length of each track
trackLen = [int(round((discInfo[DISC_FRAME1 + i + 1] - discInfo[DISC_FRAME1 + i]) / 75.0)) for i in xrange(discInfo[DISC_NB_TRACKS] - 1)]
trackLen.append(discInfo[DISC_LENGTH] - int(round(discInfo[DISC_FRAMEn] / 75.0)))
# Update the root of the tree
disc = cddb['DTITLE'].strip().decode('iso-8859-15', 'replace')
artist, album = disc.split(' / ')
self.tree.setItem((0,), ROW_NAME, '%s' % tools.htmlEscape(disc))
self.tree.setItem((0,), ROW_LENGTH, '[%s]' % sec2str(sum(trackLen)))
# Update the explorer name
modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': self.expName, 'newExpName': disc})
self.expName = disc
# Optional information
try: date = int(cddb['DYEAR'].strip().decode('iso-8859-15', 'replace'))
except: date = None
try: genre = cddb['DGENRE'].strip().decode('iso-8859-15', 'replace')
except: genre = None
# Update each track
for i, child in enumerate(self.tree.iterChildren((0,))):
title = cddb['TTITLE%u' % i].strip().decode('iso-8859-15', 'replace')
# Create the corresponding Track object
track = CDTrack(str(i+1))
track.setTitle(title)
track.setAlbum(album)
track.setArtist(artist)
track.setLength(trackLen[i])
track.setNumber(i+1)
# Optional information
if date is not None: track.setDate(date)
if genre is not None: track.setGenre(genre)
# Fill the tree
self.tree.setItem(child, ROW_NAME, '%02u. %s' % (i + 1, tools.htmlEscape(title)))
self.tree.setItem(child, ROW_TRACK, track)
# --== Disc management ==--
def cddbRequest(self, discInfo):
""" Return disc information from online CDDB, None if request fails """
import CDDB, socket
# Make sure to not be blocked by the request
socket.setdefaulttimeout(consts.socketTimeout)
try:
(status, info) = CDDB.query(discInfo)
if status == 200: disc = info # Success
elif status == 210: disc = info[0] # Exact multiple matches
elif status == 211: disc = info[0] # Inexact multiple matches
else: raise Exception, 'Unknown disc (phase 1 returned %u)' % status
(status, info) = CDDB.read(disc['category'], disc['disc_id'])
if status == 210: return info
else: raise Exception, 'Unknown disc (phase 2 returned %u)' % status
except:
logger.error('[%s] CDDB request failed\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
return None
def loadDisc(self):
""" Read disc information and create the explorer tree accordingly """
import DiscID
try:
discInfo = DiscID.disc_id(DiscID.open(prefs.get(__name__, 'device', PREFS_DFT_DEVICE)))
except Exception, err:
if err[0] == 123:
self.tree.replaceContent([(icons.cdromMenuIcon(), None, _('No disc found'), None)])
modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': self.expName, 'newExpName': MOD_L10N})
self.expName = MOD_L10N
else:
logger.error('[%s] Unable to read device\n\n%s' % (MOD_INFO[modules.MODINFO_NAME], traceback.format_exc()))
return
# Create a temporary tree, download CDDB information if needed, and update the tree
gobject.idle_add(self.createTree, discInfo[DISC_NB_TRACKS])
if not self.isDiscInCache(discInfo) and prefs.get(__name__, 'use-cddb', PREFS_DFT_USE_CDDB):
cddb = self.cddbRequest(discInfo)
if cddb is not None:
self.addDiscToCache(discInfo, cddb)
gobject.idle_add(self.updateTree, discInfo)
def reloadDisc(self):
""" Reload the disc """
# Make sure the reload is done in the thread's code and not in the GTK main loop
self.threadExecute(self.loadDisc)
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
txtRdrLen = gtk.CellRendererText()
columns = (('', [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf), (txtRdrLen, gobject.TYPE_STRING), (gtk.CellRendererText(), gobject.TYPE_STRING)], True),
(None, [(None, gobject.TYPE_PYOBJECT)], False))
self.tree = extTreeview.ExtTreeView(columns, True)
self.popup = None
self.cfgWin = None
self.expName = MOD_L10N
self.scrolled = gtk.ScrolledWindow()
self.cacheDir = os.path.join(consts.dirCfg, MOD_INFO[modules.MODINFO_NAME])
# The album length is written in a smaller font, with a lighter color
txtRdrLen.set_property('scale', 0.85)
txtRdrLen.set_property('foreground-gdk', self.tree.get_style().text[gtk.STATE_INSENSITIVE])
# Explorer
self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]])
self.scrolled.add(self.tree)
self.scrolled.set_shadow_type(gtk.SHADOW_IN)
self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.scrolled.show()
# GTK handlers
self.tree.connect('drag-data-get', self.onDragDataGet)
self.tree.connect('key-press-event', self.onKeyPressed)
self.tree.connect('exttreeview-button-pressed', self.onButtonPressed)
modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': self.expName, 'icon': icons.cdromMenuIcon(), 'widget': self.scrolled})
# Hide the album length when not drawing the root node
self.tree.get_column(0).set_cell_data_func(txtRdrLen, self.__drawAlbumLenCell)
# CD-ROM drive read speed
modules.postMsg(consts.MSG_CMD_SET_CD_SPEED, {'speed': prefs.get(__name__, 'read-speed', PREFS_DFT_READ_SPEED)})
def onModUnloaded(self):
""" The module is going to be unloaded """
modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': self.expName})
if not prefs.get(__name__, 'use-cache', PREFS_DFT_USE_CACHE):
self.clearCache()
def onExplorerChanged(self, modName, expName):
""" A new explorer has been selected """
if modName == MOD_L10N:
self.loadDisc()
# --== GTK handlers ==--
def onDragDataGet(self, tree, context, selection, info, time):
""" Provide information about the data being dragged """
serializedTracks = '\n'.join([track.serialize() for track in self.getTracksFromPaths(tree, None)])
selection.set(consts.DND_TARGETS[consts.DND_DAP_TRACKS][0], 8, serializedTracks)
def onShowPopupMenu(self, tree, button, time, path):
""" Show a popup menu """
popup = gtk.Menu()
# Play selection
play = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
play.set_sensitive(tree.getNbChildren((0,)) != 0)
play.connect('activate', lambda widget: self.playPaths(tree, None, True))
popup.append(play)
# Refresh the view
refresh = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
refresh.connect('activate', lambda widget: self.reloadDisc())
popup.append(refresh)
popup.show_all()
popup.popup(None, None, None, button, time)
def onButtonPressed(self, tree, event, path):
""" A mouse button has been pressed """
if event.button == 3:
self.onShowPopupMenu(tree, event.button, event.time, path)
elif path is not None:
if event.button == 2:
self.playPaths(tree, [path], False)
elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
self.playPaths(tree, None, True)
def onKeyPressed(self, tree, event):
""" A key has been pressed """
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'F5': self.reloadDisc()
elif keyname == 'plus': tree.expandRows()
elif keyname == 'Left': tree.collapseRows()
elif keyname == 'Right': tree.expandRows()
elif keyname == 'minus': tree.collapseRows()
elif keyname == 'space': tree.switchRows()
elif keyname == 'Return': self.playPaths(tree, None, True)
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration window """
if self.cfgWin is None:
self.cfgWin = gui.window.Window('AudioCD.glade', 'vbox1', __name__, MOD_L10N, 335, 270)
self.cfgWin.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWin.getWidget('btn-help').connect('clicked', self.onBtnHelp)
self.cfgWin.getWidget('chk-useCDDB').connect('toggled', self.onUseCDDBToggled)
self.cfgWin.getWidget('btn-clearCache').connect('clicked', self.onBtnClearCache)
self.cfgWin.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWin.hide())
# Set up the combo box
combo = self.cfgWin.getWidget('combo-read-speed')
txtRenderer = gtk.CellRendererText()
combo.pack_start(txtRenderer, True)
combo.add_attribute(txtRenderer, 'text', 0)
combo.set_sensitive(True)
txtRenderer.set_property('xpad', 6)
# Setup the liststore
store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_INT)
combo.set_model(store)
for speed in sorted(READ_SPEEDS.iterkeys()):
store.append(('%ux' % speed, speed))
if not self.cfgWin.isVisible():
self.cfgWin.getWidget('btn-ok').grab_focus()
self.cfgWin.getWidget('txt-device').set_text(prefs.get(__name__, 'device', PREFS_DFT_DEVICE))
self.cfgWin.getWidget('chk-useCDDB').set_active(prefs.get(__name__, 'use-cddb', PREFS_DFT_USE_CDDB))
self.cfgWin.getWidget('chk-useCache').set_sensitive(prefs.get(__name__, 'use-cddb', PREFS_DFT_USE_CDDB))
self.cfgWin.getWidget('chk-useCache').set_active(prefs.get(__name__, 'use-cache', PREFS_DFT_USE_CACHE))
self.cfgWin.getWidget('combo-read-speed').set_active(READ_SPEEDS[prefs.get(__name__, 'read-speed', PREFS_DFT_READ_SPEED)])
self.cfgWin.show()
def onUseCDDBToggled(self, useCDDB):
""" Toggle the "use cache" checkbox according to the state of the "use CDDB" one """
self.cfgWin.getWidget('chk-useCache').set_sensitive(useCDDB.get_active())
def onBtnClearCache(self, btn):
""" Clear CDDB cache """
text = _('This will remove all disc information stored on your hard drive.')
question = _('Clear CDDB cache?')
if gui.questionMsgBox(self.cfgWin, question, text) == gtk.RESPONSE_YES:
self.clearCache()
def onBtnOk(self, btn):
""" Check that entered information is correct before saving everything """
device = self.cfgWin.getWidget('txt-device').get_text()
useCDDB = self.cfgWin.getWidget('chk-useCDDB').get_active()
useCache = useCDDB and self.cfgWin.getWidget('chk-useCache').get_active()
readSpeed = self.cfgWin.getWidget('combo-read-speed').get_model()[self.cfgWin.getWidget('combo-read-speed').get_active()][1]
if not os.path.exists(device):
error = _('Invalid path')
errorMsg = _('The path to the CD-ROM device is not valid. Please choose an existing path.')
gui.errorMsgBox(self.cfgWin, error, errorMsg)
self.cfgWin.getWidget('txt-device').grab_focus()
else:
prefs.set(__name__, 'device', device)
prefs.set(__name__, 'use-cddb', useCDDB)
prefs.set(__name__, 'use-cache', useCache)
prefs.set(__name__, 'read-speed', readSpeed)
self.cfgWin.hide()
# CD-ROM drive read speed
modules.postMsg(consts.MSG_CMD_SET_CD_SPEED, {'speed': readSpeed})
def onBtnHelp(self, btn):
""" Display a small help message box """
helpDlg = gui.help.HelpDlg(MOD_L10N)
helpDlg.addSection(_('Description'),
_('This module lets you play audio discs from your CD-ROM device.'))
helpDlg.addSection(_('Compact Disc Data Base (CDDB)'),
_('Disc information, such as artist and album title, may be automatically downloaded '
'from an online database if you wish so. This information may also be saved on your '
'hard drive to avoid downloading it again the next time you play the same disc.'))
helpDlg.show(self.cfgWin)
./decibel-audio-player-1.06/src/modules/AudioScrobbler.py 0000644 0001750 0001750 00000032063 11456551413 023500 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import modules, os.path, tools, traceback
from time import time
from tools import consts
from gettext import gettext as _
from tools.log import logger
MOD_INFO = ('AudioScrobbler', 'AudioScrobbler', _('Keep your Last.fm profile up to date'), [], False, False, consts.MODCAT_INTERNET)
CLI_ID = 'dbl'
CLI_VER = '0.4'
MOD_NAME = MOD_INFO[modules.MODINFO_NAME]
PROTO_VER = '1.2'
AS_SERVER = 'post.audioscrobbler.com'
CACHE_FILE = 'audioscrobbler-cache.txt'
MAX_SUBMISSION = 4
# Session
(
SESSION_ID,
NOW_PLAYING_URL,
SUBMISSION_URL
) = range(3)
# Current track
(
TRK_STARTED_TIMESTAMP, # When the track has been started
TRK_UNPAUSED_TIMESTAMP, # When the track has been unpaused (if it ever happened)
TRK_PLAY_TIME, # The total play time of this track
TRK_INFO # Information about the track
) = range(4)
class AudioScrobbler(modules.ThreadedModule):
""" This module implements the Audioscrobbler protocol v1.2 (http://www.audioscrobbler.net/development/protocol/) """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_PAUSED: self.onPaused,
consts.MSG_EVT_STOPPED: self.onStopped,
consts.MSG_EVT_UNPAUSED: self.onUnpaused,
consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
}
modules.ThreadedModule.__init__(self, handlers)
def getAuthInfo(self):
""" Retrieve the login/password of the user """
from gui import authentication
if self.login is None: auth = authentication.getAuthInfo('last.fm', _('your Last.fm account'))
else: auth = authentication.getAuthInfo('last.fm', _('your Last.fm account'), self.login, True)
if auth is None: self.login, self.passwd = None, None
else: self.login, self.passwd = auth
def handshake(self):
""" Authenticate the user to the submission servers, return True if OK """
import hashlib, socket, urllib2
socket.setdefaulttimeout(consts.socketTimeout)
now = int(time())
self.session[:] = [None, None, None]
# Postpone or cancel this handshake?
if self.isBanned or (now - self.lastHandshake) < self.handshakeDelay:
return False
# Asking for login information must be done in the GTK main loop, because a dialog box might be displayed if needed
self.gtkExecute(self.getAuthInfo)
if self.passwd is None:
return False
# Compute the authentication token
md5Pwd = hashlib.md5()
md5Token = hashlib.md5()
md5Pwd.update(self.passwd)
md5Token.update('%s%u' % (md5Pwd.hexdigest(), now))
# Try to forget authentication info ASAP
token = md5Token.hexdigest()
self.passwd = None
request = 'http://%s/?hs=true&p=%s&c=%s&v=%s&u=%s&t=%d&a=%s' % (AS_SERVER, PROTO_VER, CLI_ID, CLI_VER, self.login, now, token)
self.lastHandshake = now
try:
hardFailure = False
reply = urllib2.urlopen(request).read().strip().split('\n')
if reply[0] == 'OK':
self.session[:] = reply[1:]
self.handshakeDelay = 0
self.nbHardFailures = 0
logger.info('[%s] Logged into Audioscrobbler server' % MOD_NAME)
elif reply[0] == 'BANNED':
logger.error('[%s] This version of %s has been banned from the server' % (MOD_NAME, consts.appName))
self.isBanned = True
elif reply[0] == 'BADAUTH':
logger.error('[%s] Bad authentication information' % MOD_NAME)
return self.handshake()
elif reply[0] == 'BADTIME':
logger.error('[%s] Server reported that the current system time is not correct, please correct it' % MOD_NAME)
self.isBanned = True
else:
hardFailure = True
logger.error('[%s] Hard failure during handshake' % MOD_NAME)
except:
hardFailure = True
logger.error('[%s] Unable to perform handshake\n\n%s' % (MOD_NAME, traceback.format_exc()))
if hardFailure:
if self.handshakeDelay == 0: self.handshakeDelay = 1*60 # Start at 1mn
elif self.handshakeDelay >= 64*60: self.handshakeDelay = 120*60 # Max 120mn
else: self.handshakeDelay *= 2 # Double the delay
self.login = None
return self.session[SESSION_ID] is not None
def nowPlayingNotification(self, track, firstTry = True):
""" The Now-Playing notification is a lightweight mechanism for notifying the Audioscrobbler server that a track has started playing """
import urllib2
if (self.session[SESSION_ID] is None and not self.handshake()) or not track.hasArtist() or not track.hasTitle():
return
params = (
( 's', self.session[SESSION_ID] ),
( 'a', tools.percentEncode(track.getSafeArtist()) ),
( 't', tools.percentEncode(track.getSafeTitle()) ),
( 'b', tools.percentEncode(track.getSafeAlbum()) ),
( 'l', track.getSafeLength() ),
( 'n', track.getSafeNumber() ),
( 'm', track.getSafeMBTrackId() )
)
try:
data = '&'.join(['%s=%s' % (key, val) for (key, val) in params])
reply = urllib2.urlopen(self.session[NOW_PLAYING_URL], data).read().strip().split('\n')
if reply[0] == 'BADSESSION' and firstTry:
self.session[:] = [None, None, None]
self.nowPlayingNotification(track, False)
except:
logger.error('[%s] Unable to perform now-playing notification\n\n%s' % (MOD_NAME, traceback.format_exc()))
def submit(self, firstTry=True):
""" Submit cached tracks, return True if OK """
import urllib2
if (self.session[SESSION_ID] is None and not self.handshake()) or len(self.cache) == 0:
return False
try:
hardFailure = False
cachedTracks = self.getFromCache(MAX_SUBMISSION)
data = 's=%s&%s' % (self.session[SESSION_ID], '&'.join(cachedTracks))
reply = urllib2.urlopen(self.session[SUBMISSION_URL], data).read().strip().split('\n')
if reply[0] == 'OK':
self.removeFromCache(len(cachedTracks))
return True
elif reply[0] == 'BADSESSION' and firstTry:
self.session[:] = [None, None, None]
return self.submit(False)
else:
logger.error('[%s] Unable to perform submission\n\n%s' % (MOD_NAME, reply[0]))
hardFailure = True
except:
hardFailure = True
logger.error('[%s] Unable to perform submission\n\n%s' % (MOD_NAME, traceback.format_exc()))
if hardFailure:
if self.nbHardFailures < 2: self.nbHardFailures += 1
else: self.handshake()
else:
self.nbHardFailures = 0
return False
def onTrackEnded(self, trySubmission):
""" The playback of the current track has stopped """
if self.currTrack is not None:
self.currTrack[TRK_PLAY_TIME] += (int(time()) - self.currTrack[TRK_UNPAUSED_TIMESTAMP])
self.addToCache()
self.currTrack = None
# Try to submit the whole cache?
if trySubmission:
submitOk = self.submit()
while submitOk and len(self.cache) != 0:
submitOk = self.submit()
# --== Cache management ==--
def saveCache(self):
""" Save the cache to the disk """
file = os.path.join(consts.dirCfg, CACHE_FILE)
output = open(file, 'w')
output.writelines('\n'.join(self.cache))
output.close()
def addToCache(self):
""" Add the current track to the cache, if any, and that all conditions are OK """
if self.currTrack is None: return
else: track = self.currTrack[TRK_INFO]
if not (track.hasArtist() and track.hasTitle() and track.hasLength()):
return
if not (track.getLength() >= 30 and (self.currTrack[TRK_PLAY_TIME] >= 240 or self.currTrack[TRK_PLAY_TIME] >= track.getLength()/2)):
return
params = (
( 'a[*]', tools.percentEncode(track.getSafeArtist()) ),
( 't[*]', tools.percentEncode(track.getSafeTitle()) ),
( 'i[*]', str(self.currTrack[TRK_STARTED_TIMESTAMP]) ),
( 'o[*]', 'P' ),
( 'r[*]', '' ),
( 'l[*]', track.getSafeLength() ),
( 'b[*]', tools.percentEncode(track.getSafeAlbum()) ),
( 'n[*]', track.getSafeNumber() ),
( 'm[*]', track.getSafeMBTrackId() )
)
self.cache.append('&'.join(['%s=%s' % (key, val) for (key, val) in params]))
def getFromCache(self, howMany):
""" Return the oldest howMany tracks from the cache, replace the star with the correct index """
if howMany > len(self.cache):
howMany = len(self.cache)
# Remove '\0' bytes in the data, if any
# AudioScrobbler servers reject data when it contains such a byte
return [self.cache[i].replace('%0', '').replace('[*]', '[%d]' % i) for i in xrange(howMany)]
def removeFromCache(self, howMany):
""" Remove the oldest howMany tracks from the cache """
self.cache[:] = self.cache[howMany:]
def getCacheSize(self):
""" Return the number cached tracks """
return len(self.cache)
# --== Message handlers ==--
def onModLoaded(self):
""" Initialize this module """
# Attributes
self.login = None
self.passwd = None
self.paused = False
self.session = [None, None, None]
self.isBanned = False
self.currTrack = None
self.lastHandshake = 0
self.nbHardFailures = 0
self.handshakeDelay = 0
# Load cache from the disk
try:
input = open(os.path.join(consts.dirCfg, CACHE_FILE))
self.cache = [strippedTrack for strippedTrack in [track.strip() for track in input.readlines()] if len(strippedTrack) != 0]
input.close()
except:
self.cache = []
def onNewTrack(self, track):
""" A new track has started """
timestamp = int(time())
self.onTrackEnded(True)
self.nowPlayingNotification(track)
self.currTrack = [timestamp, timestamp, 0, track]
def onStopped(self):
""" Playback has been stopped """
if self.paused:
self.currTrack[TRK_UNPAUSED_TIMESTAMP] = int(time())
self.paused = False
self.onTrackEnded(True)
def onPaused(self):
""" Playback has been paused """
self.currTrack[TRK_PLAY_TIME] += (int(time()) - self.currTrack[TRK_UNPAUSED_TIMESTAMP])
self.paused = True
def onUnpaused(self):
""" Playback has been unpaused """
self.currTrack[TRK_UNPAUSED_TIMESTAMP] = int(time())
self.paused = False
def onModUnloaded(self):
""" The module has been unloaded """
if self.paused:
self.currTrack[TRK_UNPAUSED_TIMESTAMP] = int(time())
self.paused = False
self.onTrackEnded(False)
self.saveCache()
if self.getCacheSize() != 0:
logger.info('[%s] %u track(s) left in cache' % (MOD_NAME, self.getCacheSize()))
./decibel-audio-player-1.06/src/modules/DesktopNotification.py 0000644 0001750 0001750 00000021621 11456551413 024557 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, modules, os.path
from tools import consts, prefs
from gettext import gettext as _
from tools.log import logger
MOD_INFO = ('Desktop Notification', _('Desktop Notification'), _('Display a desktop notification on track change'), ['pynotify'], False, True, consts.MODCAT_DESKTOP)
# Default preferences
PREFS_DEFAULT_BODY = 'by {artist} on {album} ({playlist_pos} / {playlist_len})'
PREFS_DEFAULT_TITLE = '{title} [{duration_str}]'
PREFS_DEFAULT_TIMEOUT = 10
PREFS_DEFAULT_SKIP_TRACK = False
class DesktopNotification(modules.Module):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_STOPPED: self.hideNotification,
consts.MSG_EVT_APP_QUIT: self.hideNotification,
consts.MSG_EVT_NEW_TRACK: self.onNewTrack,
consts.MSG_CMD_SET_COVER: self.onSetCover,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_TRACK_MOVED: self.onCurrentTrackMoved,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.hideNotification,
}
modules.Module.__init__(self, handlers)
def hideNotification(self):
""" Hide the notification """
self.currTrack = None
self.currCover = None
if self.timeout is not None:
gobject.source_remove(self.timeout)
self.timeout = None
if self.notif is not None:
self.notif.close()
def __createNotification(self, title, body, icon):
""" Create the Notification object """
import pynotify
if not pynotify.init(consts.appNameShort):
logger.error('[%s] Initialization of pynotify failed' % MOD_INFO[modules.MODINFO_NAME])
self.notif = pynotify.Notification(title, body, icon)
self.notif.set_urgency(pynotify.URGENCY_LOW)
self.notif.set_timeout(prefs.get(__name__, 'timeout', PREFS_DEFAULT_TIMEOUT) * 1000)
if prefs.get(__name__, 'skip-track', PREFS_DEFAULT_SKIP_TRACK):
self.notif.add_action('stop', _('Skip track'), self.onSkipTrack)
def showNotification(self):
""" Show the notification based on the current track """
self.timeout = None
# Can this happen?
if self.currTrack is None:
return False
# Contents
body = self.currTrack.formatHTMLSafe(prefs.get(__name__, 'body', PREFS_DEFAULT_BODY))
title = self.currTrack.format(prefs.get(__name__, 'title', PREFS_DEFAULT_TITLE))
# Icon
if self.currCover is None: img = os.path.join(consts.dirPix, 'decibel-audio-player-64.png')
else: img = self.currCover
if os.path.isfile(img): icon = 'file://' + img
else: icon = gtk.STOCK_DIALOG_INFO
# Create / Update the notification and show it
if self.notif is None: self.__createNotification(title, body, icon)
else: self.notif.update(title, body, icon)
self.notif.show()
return False
def onSkipTrack(self, notification, action):
""" The user wants to skip the current track """
if self.hasNext: modules.postMsg(consts.MSG_CMD_NEXT)
else: modules.postMsg(consts.MSG_CMD_STOP)
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
self.notif = None
self.cfgWin = None
self.hasNext = False
self.timeout = None
self.currTrack = None
self.currCover = None
def onNewTrack(self, track):
""" A new track is being played """
self.currCover = None
self.currTrack = track
if self.timeout is not None:
gobject.source_remove(self.timeout)
# Wait a bit for the cover to be set (if any)
self.timeout = gobject.timeout_add(500, self.showNotification)
def onSetCover(self, track, pathThumbnail, pathFullSize):
""" The cover for the given track """
# We must check first whether currTrack is not None, because '==' calls the cmp() method and this fails on None
if self.currTrack is not None and track == self.currTrack:
self.currCover = pathThumbnail
def onCurrentTrackMoved(self, hasNext, hasPrevious):
""" The position of the current track has changed """
self.hasNext = hasNext
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration window """
if self.cfgWin is None:
import gui, pynotify
# Create the window
self.cfgWin = gui.window.Window('DesktopNotification.glade', 'vbox1', __name__, MOD_INFO[modules.MODINFO_L10N], 355, 345)
self.cfgWin.getWidget('btn-ok').connect('clicked', self.onBtnOk)
self.cfgWin.getWidget('btn-help').connect('clicked', self.onBtnHelp)
self.cfgWin.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWin.hide())
# Disable the 'Skip track' button if the server doesn't support buttons in notifications
if 'actions' not in pynotify.get_server_caps():
self.cfgWin.getWidget('chk-skipTrack').set_sensitive(False)
if not self.cfgWin.isVisible():
self.cfgWin.getWidget('txt-title').set_text(prefs.get(__name__, 'title', PREFS_DEFAULT_TITLE))
self.cfgWin.getWidget('spn-duration').set_value(prefs.get(__name__, 'timeout', PREFS_DEFAULT_TIMEOUT))
self.cfgWin.getWidget('txt-body').get_buffer().set_text(prefs.get(__name__, 'body', PREFS_DEFAULT_BODY))
self.cfgWin.getWidget('chk-skipTrack').set_active(prefs.get(__name__, 'skip-track', PREFS_DEFAULT_SKIP_TRACK))
self.cfgWin.getWidget('btn-ok').grab_focus()
self.cfgWin.show()
def onBtnOk(self, btn):
""" Save new preferences """
# Skipping tracks
newSkipTrack = self.cfgWin.getWidget('chk-skipTrack').get_active()
oldSkipTrack = prefs.get(__name__, 'skip-track', PREFS_DEFAULT_SKIP_TRACK)
prefs.set(__name__, 'skip-track', newSkipTrack)
if oldSkipTrack != newSkipTrack and self.notif is not None:
if newSkipTrack: self.notif.add_action('stop', _('Skip track'), self.onSkipTrack)
else: self.notif.clear_actions()
# Timeout
newTimeout = int(self.cfgWin.getWidget('spn-duration').get_value())
oldTimeout = prefs.get(__name__, 'timeout', PREFS_DEFAULT_TIMEOUT)
prefs.set(__name__, 'timeout', newTimeout)
if oldTimeout != newTimeout and self.notif is not None:
self.notif.set_timeout(newTimeout * 1000)
# Other preferences
prefs.set(__name__, 'title', self.cfgWin.getWidget('txt-title').get_text())
(start, end) = self.cfgWin.getWidget('txt-body').get_buffer().get_bounds()
prefs.set(__name__, 'body', self.cfgWin.getWidget('txt-body').get_buffer().get_text(start, end))
self.cfgWin.hide()
def onBtnHelp(self, btn):
""" Display a small help message box """
import gui, media
helpDlg = gui.help.HelpDlg(MOD_INFO[modules.MODINFO_L10N])
helpDlg.addSection(_('Description'),
_('This module displays a small popup window on your desktop when a new track starts.'))
helpDlg.addSection(_('Customizing the Notification'),
_('You can change the title and the body of the notification to any text you want. Before displaying '
'the popup window, fields of the form {field} are replaced by their corresponding value. '
'Available fields are:\n\n') + media.track.getFormatSpecialFields(False))
helpDlg.addSection(_('Markup'),
_('You can use the Pango markup language to format the text. More information on that language is '
'available on the following web page:') + '\n\nhttp://www.pygtk.org/pygtk2reference/pango-markup-language.html')
helpDlg.show(self.cfgWin)
./decibel-audio-player-1.06/src/modules/Equalizer.py 0000644 0001750 0001750 00000025063 11456551413 022544 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gobject, gtk, gui, modules
from gui import fileChooser
from tools import consts, prefs
from gettext import gettext as _
MOD_INFO = ('Equalizer', _('Equalizer'), _('Tune the level of the frequency bands'), [], False, True, consts.MODCAT_DECIBEL)
# Entries of the presets combo box
(
ROW_PRESET_IS_SEPARATOR,
ROW_PRESET_NAME,
ROW_PRESET_VALUES,
) = range(3)
class Equalizer(modules.Module):
""" This module lets the user tune the level of 10 frequency bands """
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onAppStarted,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
}
modules.Module.__init__(self, handlers)
def modInit(self):
""" Initialize the module """
self.lvls = prefs.get(__name__, 'levels', [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
self.preset = prefs.get(__name__, 'preset', _('Flat'))
self.cfgWindow = None
modules.addMenuItem(_('Equalizer'), self.configure, 'E')
# --== Message handlers ==--
def onModLoaded(self):
""" The module has been loaded """
self.modInit()
self.restartRequired()
def onModUnloaded(self):
""" The module has been unloaded """
modules.delMenuItem(_('Equalizer'))
self.restartRequired()
def onAppStarted(self):
""" The application has started """
self.modInit()
modules.postMsg(consts.MSG_CMD_ENABLE_EQZ)
modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration dialog """
if self.cfgWindow is None:
import gui.window
self.cfgWindow = gui.window.Window('Equalizer.glade', 'vbox1', __name__, MOD_INFO[modules.MODINFO_L10N], 580, 300)
self.timer = None
self.combo = self.cfgWindow.getWidget('combo-presets')
self.scales = []
self.comboStore = gtk.ListStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_PYOBJECT)
self.targetLvls = []
# Setup the scales
for i in xrange(10):
self.scales.append(self.cfgWindow.getWidget('vscale' + str(i)))
self.scales[i].set_value(self.lvls[i])
self.scales[i].connect('value-changed', self.onScaleValueChanged, i)
# Setup the combo box
txtRenderer = gtk.CellRendererText()
self.combo.pack_start(txtRenderer, True)
self.combo.add_attribute(txtRenderer, 'text', ROW_PRESET_NAME)
self.combo.set_model(self.comboStore)
self.combo.set_row_separator_func(lambda model, iter: model.get_value(iter, ROW_PRESET_IS_SEPARATOR))
# Add presets to the combo box
self.comboStore.append((False, 'Classic V', ( 7, 5, 0, -5, -8, -7, -4, -1, 3, 5)))
self.comboStore.append((False, 'Classical', ( 0, 0, 0, 0, 0, 0, 0, -2, -5, -6)))
self.comboStore.append((False, 'Dance' , ( 6, 5, 4, 3, 1, 0, -3, -5, -5, 0)))
self.comboStore.append((False, 'Flat' , ( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)))
self.comboStore.append((False, 'Live' , (-4, -2, 0, 2, 3, 3, 3, 3, 2, 0)))
self.comboStore.append((False, 'Metal' , ( 3, 4, 5, 1, -2, 0, 1, 1, -1, -1)))
self.comboStore.append((False, 'Pop' , ( 3, 6, 3, -2, -4, -3, 0, 2, 3, 5)))
self.comboStore.append((False, 'Reggae' , ( 1, 1, 1, 0, -3, 0, 3, 4, 2, 1)))
self.comboStore.append((False, 'Rock' , ( 5, 4, 2, -2, -3, -3, 2, 4, 5, 5)))
self.comboStore.append((False, 'Techno' , ( 4, 4, 3, 2, 0, -4, -2, 0, 3, 4)))
# Select the right entry
if self.preset is None:
self.comboStore.insert(0, (False, _('Custom'), None))
self.comboStore.insert(1, (True, '', None))
self.combo.set_active(0)
else:
for i, preset in enumerate(self.comboStore):
if preset[ROW_PRESET_NAME] == self.preset:
self.combo.set_active(i)
break
# Events
self.cfgWindow.getWidget('btn-save').connect('clicked', self.onBtnSave)
self.cfgWindow.getWidget('btn-open').connect('clicked', self.onBtnOpen)
self.cfgWindow.getWidget('btn-close').connect('clicked', lambda btn: self.cfgWindow.hide())
self.combo.connect('changed', self.onPresetChanged)
if not self.cfgWindow.isVisible():
self.cfgWindow.getWidget('btn-close').grab_focus()
self.cfgWindow.show()
def onPresetChanged(self, combo):
""" A preset has been selected """
idx = combo.get_active()
if idx != -1:
iter = self.comboStore.get_iter(idx)
preset = self.comboStore.get_value(iter, ROW_PRESET_NAME)
self.jumpToTargetLvls(self.comboStore.get_value(iter, ROW_PRESET_VALUES))
# Remove the 'Custom' entry if needed
if self.preset is None:
self.comboStore.remove(self.comboStore.get_iter((0, )))
self.comboStore.remove(self.comboStore.get_iter((0, )))
self.preset = preset
prefs.set(__name__, 'preset', self.preset)
def onBtnSave(self, btn):
""" Save the current levels to a file"""
outFile = fileChooser.save(self.cfgWindow, _('Save levels'), 'levels.dat')
if outFile is not None:
output = open(outFile, 'wt')
for i in xrange(10):
output.write(str(self.lvls[i]) + '\n')
output.close()
def onBtnOpen(self, btn):
""" Load the levels from a file"""
inFile = fileChooser.openFile(self.cfgWindow, _('Load levels'))
if inFile is not None:
input = open(inFile, 'rt')
lines = input.readlines()
input.close()
lvls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
isInvalid = True
if len(lines) == 10:
isInvalid = False
for i in xrange(10):
elts = lines[i].split()
try:
if len(elts) == 1:
lvls[i] = float(elts[0])
if lvls[i] >= -24 and lvls[i] <= 12:
continue
except:
pass
isInvalid = True
break
if isInvalid:
gui.errorMsgBox(self.cfgWindow, _('Could not load the file'), _('The format of the file is incorrect.'))
else:
self.jumpToTargetLvls(lvls)
# Add a 'custom' entry to the presets if needed
if self.preset is not None:
self.preset = None
prefs.set(__name__, 'preset', self.preset)
self.combo.handler_block_by_func(self.onPresetChanged)
self.comboStore.insert(0, (False, _('Custom'), None))
self.comboStore.insert(1, (True, '', None))
self.combo.set_active(0)
self.combo.handler_unblock_by_func(self.onPresetChanged)
def onScaleValueChanged(self, scale, idx):
""" The user has moved one of the scales """
# Add a 'custom' entry to the presets if needed
if self.preset is not None:
self.preset = None
prefs.set(__name__, 'preset', self.preset)
self.combo.handler_block_by_func(self.onPresetChanged)
self.comboStore.insert(0, (False, _('Custom'), None))
self.comboStore.insert(1, (True, '', None))
self.combo.set_active(0)
self.combo.handler_unblock_by_func(self.onPresetChanged)
self.lvls[idx] = scale.get_value()
prefs.set(__name__, 'levels', self.lvls)
modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
def jumpToTargetLvls(self, targetLvls):
""" Move the scales until they reach some target levels """
if self.timer is not None:
gobject.source_remove(self.timer)
self.timer = gobject.timeout_add(20, self.timerFunc)
self.targetLvls = targetLvls
for i in xrange(10):
self.scales[i].handler_block_by_func(self.onScaleValueChanged)
def timerFunc(self):
""" Move a bit the scales to their target value """
isFinished = True
# Move the scales a bit
for i in xrange(10):
currLvl = self.scales[i].get_value()
targetLvl = self.targetLvls[i]
difference = targetLvl - currLvl
if abs(difference) <= 0.25:
newLvl = targetLvl
else:
newLvl = currLvl + (difference / 8.0)
isFinished = False
self.lvls[i] = newLvl
self.scales[i].set_value(newLvl)
# Set the equalizer to the new levels
modules.postMsg(consts.MSG_CMD_SET_EQZ_LVLS, {'lvls': self.lvls})
if isFinished:
self.timer = None
prefs.set(__name__, 'levels', self.lvls)
# Make sure labels are up to date (sometimes they aren't when we're done with the animation)
# Also unblock the handlers
for i in xrange(10):
self.scales[i].queue_draw()
self.scales[i].handler_unblock_by_func(self.onScaleValueChanged)
return False
return True
./decibel-audio-player-1.06/src/modules/Library.py 0000644 0001750 0001750 00000121424 11456551413 022205 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, media, modules, os, tools
from tools import consts, htmlEscape, icons, prefs, pickleLoad, pickleSave
from gettext import ngettext, gettext as _
from os.path import isdir, isfile
from gobject import idle_add, TYPE_STRING, TYPE_INT, TYPE_PYOBJECT
from tools.log import logger
from media.track.fileTrack import FileTrack
MOD_INFO = ('Library', _('Library'), _('Organize your music by tags'), [], False, True, consts.MODCAT_EXPLORER)
MOD_L10N = MOD_INFO[modules.MODINFO_L10N]
# Information associated with libraries
(
LIB_PATH, # Physical location of media files
LIB_NB_ARTISTS, # Number of artists
LIB_NB_ALBUMS, # Number of albums
LIB_NB_TRACKS # Number of tracks
) = range(4)
# Information associated with artists
(
ART_NAME, # Its name
ART_INDEX, # Name of the directory: avoid the use of the artist name as a filename (may contain invalid characters)
ART_NB_ALBUMS # How many albums
) = range(3)
# Information associated with albums
(
ALB_NAME, # Its name
ALB_INDEX, # Name of the file: avoid the use of the artist name as a filename (may contain invalid characters)
ALB_NB_TRACKS, # Number of tracks
ALB_LENGTH # Complete duration (include all tracks)
) = range(4)
# Possible types for a node of the tree
(
TYPE_ARTIST, # Artist
TYPE_ALBUM, # Album
TYPE_TRACK, # Single track
TYPE_HEADER, # Alphabetical header
TYPE_FAVORITES_BANNER, # Favorites banner (when showing only favorites)
TYPE_NONE # Used for fake children
) = range(6)
# The format of a row in the treeview
(
ROW_PIXBUF, # Item icon
ROW_ALBUM_LEN, # Length of the album (invisible when not an album)
ROW_NAME, # Item name
ROW_TYPE, # The type of the item (e.g., directory, file)
ROW_FULLPATH, # The full path to the item
ROW_DATA # Arbitrary data that depend on the type of the row
) = range(6)
# Constants
VERSION = 3 # Used to check compatibility
ROOT_PATH = os.path.join(consts.dirCfg, 'Library') # Path where libraries are stored
FAKE_CHILD = (None, None, '', TYPE_NONE, '', None) # We use a lazy tree
PREFS_DEFAULT_PREFIXES = {'the ': None} # Prefixes are put at the end of artists' names
PREFS_DEFAULT_LIBRARIES = {} # No libraries at first
PREFS_DEFAULT_TREE_STATE = {} # No state at first
PREFS_DEFAULT_SHOW_ONLY_FAVORITES = False
class Library(modules.Module):
def __init__(self):
""" Constructor """
handlers = {
consts.MSG_EVT_APP_QUIT: self.onModUnloaded,
consts.MSG_EVT_MOD_LOADED: self.onModLoaded,
consts.MSG_EVT_APP_STARTED: self.onModLoaded,
consts.MSG_EVT_MOD_UNLOADED: self.onModUnloaded,
consts.MSG_EVT_EXPLORER_CHANGED: self.onExplorerChanged,
}
modules.Module.__init__(self, handlers)
def __createTree(self):
""" Create the main tree, add it to the scrolled window """
from gui.extTreeview import ExtTreeView
txtRdr = gtk.CellRendererText()
pixbufRdr = gtk.CellRendererPixbuf()
txtRdrAlbumLen = gtk.CellRendererText()
columns = (('', [(pixbufRdr, gtk.gdk.Pixbuf), (txtRdrAlbumLen, TYPE_STRING), (txtRdr, TYPE_STRING)], False),
(None, [(None, TYPE_INT)], False),
(None, [(None, TYPE_STRING)], False),
(None, [(None, TYPE_PYOBJECT)], False))
self.tree = ExtTreeView(columns, True)
# The first text column (ROW_ALBUM_LEN) is not the one to search for
# set_search_column(ROW_NAME) should work, but it doesn't...
self.tree.set_search_equal_func(self.__searchFunc)
self.tree.get_column(0).set_cell_data_func(txtRdr, self.__drawCell)
self.tree.get_column(0).set_cell_data_func(pixbufRdr, self.__drawCell)
self.tree.get_column(0).set_cell_data_func(txtRdrAlbumLen, self.__drawAlbumLenCell)
# The album length is written in a smaller font, with a lighter color
txtRdrAlbumLen.set_property('scale', 0.85)
txtRdrAlbumLen.set_property('foreground-gdk', self.tree.get_style().text[gtk.STATE_INSENSITIVE])
self.tree.setDNDSources([consts.DND_TARGETS[consts.DND_DAP_TRACKS]])
# GTK handlers
self.tree.connect('drag-data-get', self.onDragDataGet)
self.tree.connect('key-press-event', self.onKeyPressed)
self.tree.connect('exttreeview-row-expanded', self.onRowExpanded)
self.tree.connect('exttreeview-row-collapsed', self.onRowCollapsed)
self.tree.connect('exttreeview-button-pressed', self.onButtonPressed)
# Add the tree to the scrolled window
self.scrolled.add(self.tree)
def __searchFunc(self, model, column, key, iter):
""" Check whether the given key matches the current candidate (iter) """
return model.get_value(iter, ROW_NAME)[:len(key)].lower() != key.lower()
def __drawCell(self, column, cell, model, iter):
""" Use a different background color for alphabetical headers """
if model.get_value(iter, ROW_TYPE) == TYPE_HEADER: cell.set_property('cell-background-gdk', self.tree.style.bg[gtk.STATE_PRELIGHT])
else: cell.set_property('cell-background', None)
def __drawAlbumLenCell(self, column, cell, model, iter):
""" Use a different background color for alphabetical headers """
if model.get_value(iter, ROW_ALBUM_LEN) is None: cell.set_property('visible', False)
else: cell.set_property('visible', True)
def __createEmptyLibrary(self, name):
""" Create bootstrap files for a new library """
import shutil
# Make sure that the root directory of all libraries exists
if not isdir(ROOT_PATH):
os.mkdir(ROOT_PATH)
# Start from an empty library
libPath = os.path.join(ROOT_PATH, name)
if isdir(libPath):
shutil.rmtree(libPath)
os.mkdir(libPath)
pickleSave(os.path.join(libPath, 'files'), {})
def refreshLibrary(self, parent, libName, path, creation=False):
""" Refresh the given library, must be called through idle_add() """
import collections, shutil
from gui import progressDlg
# First show a progress dialog
if creation: header = _('Creating library')
else: header = _('Refreshing library')
progress = progressDlg.ProgressDlg(parent, header, _('The directory is scanned for media files. This can take some time.\nPlease wait.'))
yield True
libPath = os.path.join(ROOT_PATH, libName) # Location of the library
# If the version number has changed or does not exist, don't reuse any existing file and start from scratch
if not os.path.exists(os.path.join(libPath, 'VERSION_%u' % VERSION)):
self.__createEmptyLibrary(libName)
db = {} # The dictionnary used to create the library
queue = collections.deque((path,)) # Faster structure for appending/removing elements
mediaFiles = [] # All media files found
newLibrary = {} # Reflect the current file structure of the library
oldLibrary = pickleLoad(os.path.join(libPath, 'files')) # Previous file structure of the same library
# Make sure the root directory still exists
if not os.path.exists(path):
queue.pop()
while len(queue) != 0:
currDir = queue.pop()
currDirMTime = os.stat(currDir).st_mtime
# Retrieve previous information on the current directory, if any
if currDir in oldLibrary: oldDirMTime, oldDirectories, oldFiles = oldLibrary[currDir]
else: oldDirMTime, oldDirectories, oldFiles = -1, [], {}
# If the directory has not been modified, keep old information
if currDirMTime == oldDirMTime:
files, directories = oldFiles, oldDirectories
else:
files, directories = {}, []
for (filename, fullPath) in tools.listDir(currDir):
if isdir(fullPath):
directories.append(fullPath)
elif isfile(fullPath) and media.isSupported(filename):
if filename in oldFiles: files[filename] = oldFiles[filename]
else: files[filename] = [-1, FileTrack(fullPath)]
# Determine which files need to be updated
for filename, (oldMTime, track) in files.iteritems():
mTime = os.stat(track.getFilePath()).st_mtime
if mTime != oldMTime:
files[filename] = [mTime, media.getTrackFromFile(track.getFilePath())]
newLibrary[currDir] = (currDirMTime, directories, files)
mediaFiles.extend([track for mTime, track in files.itervalues()])
queue.extend(directories)
# Update the progress dialog
try:
text = ngettext('Scanning directories (one track found)', 'Scanning directories (%(nbtracks)u tracks found)', len(mediaFiles))
progress.pulse(text % {'nbtracks': len(mediaFiles)})
yield True
except progressDlg.CancelledException:
progress.destroy()
if creation:
shutil.rmtree(libPath)
yield False
# From now on, the process should not be cancelled
progress.setCancellable(False)
if creation: progress.pulse(_('Creating library...'))
else: progress.pulse(_('Refreshing library...'))
yield True
# Create the database
for track in mediaFiles:
album = track.getExtendedAlbum()
if track.hasAlbumArtist(): artist = track.getAlbumArtist()
else: artist = track.getArtist()
if artist in db:
allAlbums = db[artist]
if album in allAlbums: allAlbums[album].append(track)
else: allAlbums[album] = [track]
else:
db[artist] = {album: [track]}
progress.pulse()
yield True
# If an artist name begins with a known prefix, put it at the end (e.g., Future Sound of London (The))
prefixes = prefs.get(__name__, 'prefixes', PREFS_DEFAULT_PREFIXES)
for artist in db.keys():
artistLower = artist.lower()
for prefix in prefixes:
if artistLower.startswith(prefix):
db[artist[len(prefix):] + ' (%s)' % artist[:len(prefix)-1]] = db[artist]
del db[artist]
progress.pulse()
yield True
# Load favorites before removing the files from the disk
if self.currLib == libName: favorites = self.favorites
else: favorites = self.loadFavorites(libName)
# Re-create the library structure on the disk
if isdir(libPath):
shutil.rmtree(libPath)
os.mkdir(libPath)
# Put a version number
tools.touch(os.path.join(libPath, 'VERSION_%u' % VERSION))
overallNbAlbums = 0
overallNbTracks = 0
overallNbArtists = len(db)
# The 'artists' file contains all known artists with their index, the 'files' file contains the file structure of the root path
allArtists = sorted([(artist, str(indexArtist), len(db[artist])) for indexArtist, artist in enumerate(db)], key = lambda a: a[0].lower())
pickleSave(os.path.join(libPath, 'files'), newLibrary)
pickleSave(os.path.join(libPath, 'artists'), allArtists)
for (artist, indexArtist, nbAlbums) in allArtists:
artistPath = os.path.join(libPath, indexArtist)
overallNbAlbums += nbAlbums
os.mkdir(artistPath)
albums = []
for index, (name, tracks) in enumerate(db[artist].iteritems()):
length = sum([track.getLength() for track in tracks])
overallNbTracks += len(tracks)
albums.append((name, str(index), len(tracks), length))
pickleSave(os.path.join(artistPath, str(index)), sorted(tracks, key = lambda track: track.getNumber()))
albums.sort(cmp = lambda a1, a2: cmp(db[artist][a1[0]][0], db[artist][a2[0]][0]))
pickleSave(os.path.join(artistPath, 'albums'), albums)
progress.pulse()
yield True
self.libraries[libName] = (path, overallNbArtists, overallNbAlbums, overallNbTracks)
self.fillLibraryList()
if creation:
modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': libName, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled})
progress.destroy()
# Trim favorites and save them
newFavorites = {}
for (artist, albums) in favorites.iteritems():
if artist in db:
newFavorites[artist] = {}
for album in albums:
if album in db[artist]:
newFavorites[artist][album] = None
self.saveFavorites(libName, newFavorites)
# If the refreshed library is currently displayed, refresh the treeview as well
if self.currLib == libName:
self.saveTreeState()
self.favorites = newFavorites
self.loadArtists(self.tree, self.currLib)
self.restoreTreeState()
yield False
def __getTracksFromPaths(self, tree, paths):
"""
Return the list of tracks extracted from:
* The list 'paths' if it is not None
* The currently selected rows if 'paths' is None
"""
from sys import maxint
tracks = []
if paths is None:
paths = tree.getSelectedPaths()
for currPath in paths:
row = tree.getRow(currPath)
if row[ROW_TYPE] == TYPE_TRACK:
tracks.append(row[ROW_DATA])
elif row[ROW_TYPE] == TYPE_ALBUM:
tracks.extend(pickleLoad(row[ROW_FULLPATH]))
elif row[ROW_TYPE] == TYPE_ARTIST:
for album in pickleLoad(os.path.join(row[ROW_FULLPATH], 'albums')):
tracks.extend(pickleLoad(os.path.join(row[ROW_FULLPATH], album[ALB_INDEX])))
elif row[ROW_TYPE] == TYPE_HEADER:
for path in xrange(currPath[0]+1, maxint):
if not tree.isValidPath(path):
break
row = tree.getRow(path)
if row[ROW_TYPE] == TYPE_HEADER:
break
for album in pickleLoad(os.path.join(row[ROW_FULLPATH], 'albums')):
tracks.extend(pickleLoad(os.path.join(row[ROW_FULLPATH], album[ALB_INDEX])))
return tracks
def playPaths(self, tree, paths, replace):
"""
Replace/extend the tracklist
If the list 'paths' is None, use the current selection
"""
tracks = self.__getTracksFromPaths(tree, paths)
if replace: modules.postMsg(consts.MSG_CMD_TRACKLIST_SET, {'tracks': tracks, 'playNow': True})
else: modules.postMsg(consts.MSG_CMD_TRACKLIST_ADD, {'tracks': tracks, 'playNow': False})
def pickAlbumArtist(self, tree, artistPath):
""" Pick an album at random of the given artist and play it """
import random
# Expanding the artist row populates it, so that we can then pick an album at random
tree.expandRow(artistPath)
albumPath = artistPath + (random.randint(0, tree.getNbChildren(artistPath)-1), )
# Select the random album and play it
tree.get_selection().unselect_all()
tree.get_selection().select_path(albumPath)
tree.scroll(albumPath)
self.playPaths(tree, [albumPath], True)
def pickAlbumLibrary(self, tree):
""" Pick an album at random in the library and play it """
import random
# Pick an artist at random (make sure not to select an alphabetical header)
path = (random.randint(0, tree.getCount()-1), )
while tree.getItem(path, ROW_TYPE) != TYPE_ARTIST:
path = (random.randint(0, tree.getCount()-1), )
self.pickAlbumArtist(tree, path)
def switchFavoriteStateOfSelectedItems(self, tree):
""" Add to/remove from favorites the selected items """
# Go through selected items and switch their state
removed = False
for path in tree.getSelectedPaths():
if tree.getItem(path, ROW_TYPE) == TYPE_ALBUM:
album = tree.getItem(path, ROW_DATA)
artist = tree.getItem(path[:-1], ROW_DATA)
if self.isAlbumInFavorites(artist, album):
removed = True
tree.setItem(path, ROW_PIXBUF, icons.mediaDirMenuIcon())
self.removeFromFavorites(artist, album)
else:
tree.setItem(path, ROW_PIXBUF, icons.starDirMenuIcon())
self.addToFavorites(artist, album)
# If some favorites were removed, we may have to reload the tree
if self.showOnlyFavorites and removed:
self.saveTreeState()
self.loadArtists(self.tree, self.currLib)
self.restoreTreeState()
def switchFavoritesView(self, tree):
""" Show all/favorites """
self.saveTreeState()
self.showOnlyFavorites = not self.showOnlyFavorites
prefs.set(__name__, 'show-only-favorites', self.showOnlyFavorites)
self.loadArtists(self.tree, self.currLib)
self.restoreTreeState()
def showPopupMenu(self, tree, button, time, path):
""" Show a popup menu """
popup = gtk.Menu()
# Play
play = gtk.ImageMenuItem(gtk.STOCK_MEDIA_PLAY)
popup.append(play)
if path is None: play.set_sensitive(False)
else: play.connect('activate', lambda widget: self.playPaths(tree, None, True))
# Separator
popup.append(gtk.SeparatorMenuItem())
# Add to/remove from favorites
favCpt = 0
nonFavCpt = 0
for node in tree.getSelectedPaths():
if tree.getItem(node, ROW_TYPE) != TYPE_ALBUM:
favCpt = 1
nonFavCpt = 1
break
elif tree.getItem(node, ROW_PIXBUF) == icons.mediaDirMenuIcon():
nonFavCpt += 1
else:
favCpt += 1
if favCpt == 0: favorite = gtk.ImageMenuItem(_('Add to Favorites'))
elif nonFavCpt == 0: favorite = gtk.ImageMenuItem(_('Remove from Favorites'))
else: favorite = gtk.ImageMenuItem(_('Favorites'))
favorite.get_image().set_from_pixbuf(icons.starMenuIcon())
popup.append(favorite)
if TYPE_ALBUM in [tree.getItem(path, ROW_TYPE) for path in tree.getSelectedPaths()]:
favorite.connect('activate', lambda widget: self.switchFavoriteStateOfSelectedItems(tree))
else:
favorite.set_sensitive(False)
# Show only favorites
showFavorites = gtk.CheckMenuItem(_('Show Only Favorites'))
showFavorites.set_active(self.showOnlyFavorites)
showFavorites.connect('toggled', lambda widget: self.switchFavoritesView(tree))
popup.append(showFavorites)
# Separator
popup.append(gtk.SeparatorMenuItem())
# Collapse all nodes
collapse = gtk.ImageMenuItem(_('Collapse all'))
collapse.set_image(gtk.image_new_from_stock(gtk.STOCK_CLEAR, gtk.ICON_SIZE_MENU))
popup.append(collapse)
enabled = False
for child in self.tree.iterChildren(None):
if self.tree.row_expanded(child):
enabled = True
break
if enabled: collapse.connect('activate', lambda widget: self.tree.collapse_all())
else: collapse.set_sensitive(False)
# Refresh the library
refresh = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
refresh.connect('activate', lambda widget: idle_add(self.refreshLibrary(None, self.currLib, self.libraries[self.currLib][LIB_PATH]).next))
popup.append(refresh)
# Randomness
randomness = gtk.Menu()
randomnessItem = gtk.ImageMenuItem(_('Randomness'))
randomnessItem.get_image().set_from_icon_name('stock_shuffle', gtk.ICON_SIZE_MENU)
randomnessItem.set_submenu(randomness)
popup.append(randomnessItem)
# Random album of the selected artist
if path is not None and tree.getItem(path, ROW_TYPE) == TYPE_ARTIST:
album = gtk.MenuItem(_('Pick an album of %(artist)s' % {'artist': tree.getItem(path, ROW_NAME).replace('&', '&')}))
album.connect('activate', lambda widget: self.pickAlbumArtist(tree, path))
randomness.append(album)
# Random album in the entire library
album = gtk.MenuItem(_('Pick an album in the library'))
album.connect('activate', lambda widget: self.pickAlbumLibrary(tree))
randomness.append(album)
popup.show_all()
popup.popup(None, None, None, button, time)
# --== Populating the tree ==--
def loadArtists(self, tree, name):
""" Load the given library """
libPath = os.path.join(ROOT_PATH, name)
# Make sure the version number is the good one
if not os.path.exists(os.path.join(libPath, 'VERSION_%u' % VERSION)):
logger.error('[%s] Version number does not match, loading of library "%s" aborted' % (MOD_INFO[modules.MODINFO_NAME], name))
error = _('This library is deprecated, please refresh it.')
tree.replaceContent([(icons.errorMenuIcon(), None, error, TYPE_NONE, None, None)])
return
rows = []
icon = icons.dirMenuIcon()
prevChar = ''
allArtists = pickleLoad(os.path.join(libPath, 'artists'))
# Filter artists if needed
if self.showOnlyFavorites:
allArtists = [artist for artist in allArtists if self.isArtistInFavorites(artist[ART_NAME])]
rows.append((icons.starMenuIcon(), None, '%s' % _('My Favorites'), TYPE_FAVORITES_BANNER, None, None))
# Create the rows
for artist in allArtists:
if len(artist[ART_NAME]) != 0: currChar = unicode(artist[ART_NAME], errors='replace')[0].lower()
else: currChar = prevChar
if prevChar != currChar and not (prevChar.isdigit() and currChar.isdigit()):
prevChar = currChar
if currChar.isdigit(): rows.append((None, None, '0 - 9', TYPE_HEADER, None, None))
else: rows.append((None, None, '%s' % currChar.upper(), TYPE_HEADER, None, None))
rows.append((icon, None, htmlEscape(artist[ART_NAME]), TYPE_ARTIST, os.path.join(libPath, artist[ART_INDEX]), artist[ART_NAME]))
# Insert all rows, and then add a fake child to each artist
tree.replaceContent(rows)
for node in tree.iterChildren(None):
if tree.getItem(node, ROW_TYPE) == TYPE_ARTIST:
tree.appendRow(FAKE_CHILD, node)
def loadAlbums(self, tree, node, fakeChild):
""" Initial load of the albums of the given node, assuming it is of type TYPE_ARTIST """
rows = []
path = tree.getItem(node, ROW_FULLPATH)
artist = tree.getItem(node, ROW_DATA)
allAlbums = pickleLoad(os.path.join(tree.getItem(node, ROW_FULLPATH), 'albums'))
# Filter albums if only favorites should be shown
if self.showOnlyFavorites:
allAlbums = [album for album in allAlbums if self.isAlbumInFavorites(artist, album[ALB_NAME])]
# The icon depends on whether the album is in the favorites
for album in allAlbums:
if self.isAlbumInFavorites(artist, album[ALB_NAME]): icon = icons.starDirMenuIcon()
else: icon = icons.mediaDirMenuIcon()
rows.append((icon, '[%s]' % tools.sec2str(album[ALB_LENGTH], True), '%s' % htmlEscape(album[ALB_NAME]),
TYPE_ALBUM, os.path.join(path, album[ALB_INDEX]), album[ALB_NAME]))
# Add all the rows, and then add a fake child to each of them
tree.appendRows(rows, node)
tree.removeRow(fakeChild)
for child in tree.iterChildren(node):
tree.appendRow(FAKE_CHILD, child)
def loadTracks(self, tree, node, fakeChild):
""" Initial load of all tracks of the given node, assuming it is of type TYPE_ALBUM """
allTracks = pickleLoad(tree.getItem(node, ROW_FULLPATH))
icon = icons.mediaFileMenuIcon()
rows = [(icon, None, '%02u. %s' % (track.getNumber(), htmlEscape(track.getTitle())), TYPE_TRACK, track.getFilePath(), track) for track in allTracks]
tree.appendRows(rows, node)
tree.removeRow(fakeChild)
# --== Manage tree state ==--
def saveTreeState(self):
""" Save the current tree state """
if self.showOnlyFavorites: self.treeStates[self.currLib + ' favorites'] = self.tree.saveState(ROW_NAME)
else: self.treeStates[self.currLib] = self.tree.saveState(ROW_NAME)
def restoreTreeState(self):
""" Restore the tree state """
if self.showOnlyFavorites: name = self.currLib + ' favorites'
else: name = self.currLib
if name in self.treeStates:
self.tree.restoreState(self.treeStates[name], ROW_NAME)
def removeTreeStates(self, libName):
""" Remove the tree states associated to the given library """
if libName in self.treeStates: del self.treeStates[libName]
if libName + ' favorites' in self.treeStates: del self.treeStates[libName + ' favorites']
def renameTreeStates(self, oldLibName, newLibName):
""" Rename the tree states associated with oldLibName """
if oldLibName in self.treeStates:
self.treeStates[newLibName] = self.treeStates[oldLibName]
del self.treeStates[oldLibName]
if oldLibName + ' favorites' in self.treeStates:
self.treeStates[newLibName + ' favorites'] = self.treeStates[oldLibName + ' favorites']
del self.treeStates[oldLibName + ' favorites']
# --== Favorites ==--
def loadFavorites(self, libName):
""" Load favorites from the disk """
try: return pickleLoad(os.path.join(ROOT_PATH, libName, 'favorites'))
except: return {}
def saveFavorites(self, libName, favorites):
""" Save favorites to the disk """
pickleSave(os.path.join(ROOT_PATH, libName, 'favorites'), favorites)
def isArtistInFavorites(self, artist):
""" Return whether the given artist is in the favorites (at least one album) """
return artist in self.favorites
def isAlbumInFavorites(self, artist, album):
""" Return whether the given album is in the favorites """
return artist in self.favorites and album in self.favorites[artist]
def addToFavorites(self, artist, album):
""" Add the given album to the favorites """
if artist in self.favorites: self.favorites[artist][album] = None
else: self.favorites[artist] = {album: None}
def removeFromFavorites(self, artist, album):
""" Remove the given album from the favorites """
del self.favorites[artist][album]
if len(self.favorites[artist]) == 0:
del self.favorites[artist]
# --== GTK handlers ==--
def onRowExpanded(self, tree, node):
""" Populate the expanded row """
if tree.getItem(node, ROW_TYPE) == TYPE_ARTIST: self.loadAlbums(tree, node, tree.getChild(node, 0))
else: self.loadTracks(tree, node, tree.getChild(node, 0))
def onRowCollapsed(self, tree, node):
""" Replace all children of the node by a fake child """
tree.removeAllChildren(node)
tree.appendRow(FAKE_CHILD, node)
def onButtonPressed(self, tree, event, path):
""" A mouse button has been pressed """
if event.button == 3:
self.showPopupMenu(tree, event.button, event.time, path)
elif path is not None and tree.getItem(path, ROW_TYPE) != TYPE_NONE:
if event.button == 2:
self.playPaths(tree, [path], False)
elif event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS:
if tree.getItem(path, ROW_PIXBUF) != icons.dirMenuIcon(): self.playPaths(tree, None, True)
elif tree.row_expanded(path): tree.collapse_row(path)
else: tree.expand_row(path, False)
def onKeyPressed(self, tree, event):
""" A key has been pressed """
keyname = gtk.gdk.keyval_name(event.keyval)
if keyname == 'F5': idle_add(self.refreshLibrary(None, self.currLib, self.libraries[self.currLib][LIB_PATH]).next)
elif keyname == 'plus': tree.expandRows()
elif keyname == 'Left': tree.collapseRows()
elif keyname == 'Right': tree.expandRows()
elif keyname == 'minus': tree.collapseRows()
elif keyname == 'space': tree.switchRows()
elif keyname == 'Return': self.playPaths(tree, None, True)
def onDragDataGet(self, tree, context, selection, info, time):
""" Provide information about the data being dragged """
serializedTracks = '\n'.join([track.serialize() for track in self.__getTracksFromPaths(tree, None)])
selection.set(consts.DND_TARGETS[consts.DND_DAP_TRACKS][0], 8, serializedTracks)
def addAllExplorers(self):
""" Add all libraries to the Explorer module """
for (name, (path, nbArtists, nbAlbums, nbTracks)) in self.libraries.iteritems():
modules.postMsg(consts.MSG_CMD_EXPLORER_ADD, {'modName': MOD_L10N, 'expName': name, 'icon': icons.dirMenuIcon(), 'widget': self.scrolled})
def removeAllExplorers(self):
""" Remove all libraries from the Explorer module """
for (name, (path, nbArtists, nbAlbums, nbTracks)) in self.libraries.iteritems():
modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': name})
# --== Message handlers ==--
def onModLoaded(self):
""" This is the real initialization function, called when the module has been loaded """
self.tree = None
self.currLib = None
self.cfgWindow = None
self.libraries = prefs.get(__name__, 'libraries', PREFS_DEFAULT_LIBRARIES)
self.favorites = None
self.treeStates = prefs.get(__name__, 'tree-state', PREFS_DEFAULT_TREE_STATE)
self.showOnlyFavorites = prefs.get(__name__, 'show-only-favorites', PREFS_DEFAULT_SHOW_ONLY_FAVORITES)
# Scroll window
self.scrolled = gtk.ScrolledWindow()
self.scrolled.set_shadow_type(gtk.SHADOW_IN)
self.scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
self.scrolled.show()
idle_add(self.addAllExplorers)
def onModUnloaded(self):
""" The module has been unloaded """
if self.currLib is not None:
self.saveTreeState()
self.saveFavorites(self.currLib, self.favorites)
prefs.set(__name__, 'tree-state', self.treeStates)
prefs.set(__name__, 'libraries', self.libraries)
self.removeAllExplorers()
def onExplorerChanged(self, modName, expName):
""" A new explorer has been selected """
if modName == MOD_L10N and expName != self.currLib:
# Create the tree if needed
if self.tree is None:
self.__createTree()
# Save the state of the current library
if self.currLib is not None:
self.saveTreeState()
self.saveFavorites(self.currLib, self.favorites)
# Switch to the new library
self.currLib = expName
self.favorites = self.loadFavorites(self.currLib)
self.loadArtists(self.tree, self.currLib)
self.restoreTreeState()
# --== Configuration ==--
def configure(self, parent):
""" Show the configuration dialog """
if self.cfgWindow is None:
from gui import extListview, window
self.cfgWindow = window.Window('Library.glade', 'vbox1', __name__, MOD_L10N, 370, 400)
# Create the list of libraries
txtRdr = gtk.CellRendererText()
pixRdr = gtk.CellRendererPixbuf()
columns = ((None, [(txtRdr, TYPE_STRING)], 0, False, False),
('', [(pixRdr, gtk.gdk.Pixbuf), (txtRdr, TYPE_STRING)], 2, False, True))
self.cfgList = extListview.ExtListView(columns, sortable=False, useMarkup=True, canShowHideColumns=False)
self.cfgList.set_headers_visible(False)
self.cfgWindow.getWidget('scrolledwindow1').add(self.cfgList)
# Connect handlers
self.cfgList.connect('key-press-event', self.onCfgKeyboard)
self.cfgList.get_selection().connect('changed', self.onCfgSelectionChanged)
self.cfgWindow.getWidget('btn-add').connect('clicked', self.onAddLibrary)
self.cfgWindow.getWidget('btn-rename').connect('clicked', self.onRenameLibrary)
self.cfgWindow.getWidget('btn-remove').connect('clicked', lambda btn: self.removeSelectedLibraries(self.cfgList))
self.cfgWindow.getWidget('btn-refresh').connect('clicked', self.onRefresh)
self.cfgWindow.getWidget('btn-ok').connect('clicked', lambda btn: self.cfgWindow.hide())
self.cfgWindow.getWidget('btn-cancel').connect('clicked', lambda btn: self.cfgWindow.hide())
self.cfgWindow.getWidget('btn-help').connect('clicked', self.onHelp)
if not self.cfgWindow.isVisible():
self.fillLibraryList()
self.cfgWindow.getWidget('btn-ok').grab_focus()
self.cfgWindow.show()
def onRefresh(self, btn):
""" Refresh the first selected library """
name = self.cfgList.getSelectedRows()[0][0]
idle_add(self.refreshLibrary(self.cfgWindow, name, self.libraries[name][LIB_PATH]).next)
def onAddLibrary(self, btn):
""" Let the user create a new library """
from gui.selectPath import SelectPath
result = SelectPath(MOD_L10N, self.cfgWindow, self.libraries.keys(), ['/']).run()
if result is not None:
name, path = result
idle_add(self.refreshLibrary(self.cfgWindow, name, path, True).next)
def renameLibrary(self, oldName, newName):
""" Rename a library """
import shutil
self.libraries[newName] = self.libraries[oldName]
del self.libraries[oldName]
oldPath = os.path.join(ROOT_PATH, oldName)
newPath = os.path.join(ROOT_PATH, newName)
shutil.move(oldPath, newPath)
# Rename tree states as well
self.renameTreeStates(oldName, newName)
# Is it the current library?
if self.currLib == oldName:
self.currLib = newName
modules.postMsg(consts.MSG_CMD_EXPLORER_RENAME, {'modName': MOD_L10N, 'expName': oldName, 'newExpName': newName})
def onRenameLibrary(self, btn):
""" Let the user rename a library """
from gui.selectPath import SelectPath
name = self.cfgList.getSelectedRows()[0][0]
forbidden = [libName for libName in self.libraries if libName != name]
pathSelector = SelectPath(MOD_L10N, self.cfgWindow, forbidden, ['/'])
pathSelector.setPathSelectionEnabled(False)
result = pathSelector.run(name, self.libraries[name][LIB_PATH])
if result is not None and result[0] != name:
self.renameLibrary(name, result[0])
self.fillLibraryList()
def fillLibraryList(self):
""" Fill the list of libraries """
if self.cfgWindow is not None:
rows = [(name, icons.dirBtnIcon(), '%s\n%s - %u %s' % (htmlEscape(name), htmlEscape(path), nbTracks, htmlEscape(_('tracks'))))
for name, (path, nbArtists, nbAlbums, nbTracks) in sorted(self.libraries.iteritems())]
self.cfgList.replaceContent(rows)
def removeSelectedLibraries(self, list):
""" Remove all selected libraries """
import shutil
from gui import questionMsgBox
if list.getSelectedRowsCount() == 1:
remark = _('You will be able to recreate this library later on if you wish so.')
question = _('Remove the selected library?')
else:
remark = _('You will be able to recreate these libraries later on if you wish so.')
question = _('Remove all selected libraries?')
if questionMsgBox(self.cfgWindow, question, '%s %s' % (_('Your media files will not be removed.'), remark)) == gtk.RESPONSE_YES:
for row in list.getSelectedRows():
libName = row[0]
if self.currLib == libName:
self.currLib = None
# Remove the library from the disk
libPath = os.path.join(ROOT_PATH, libName)
if isdir(libPath):
shutil.rmtree(libPath)
# Remove the corresponding explorer
modules.postMsg(consts.MSG_CMD_EXPLORER_REMOVE, {'modName': MOD_L10N, 'expName': libName})
del self.libraries[libName]
# Remove tree states
self.removeTreeStates(libName)
# Clean up the listview
list.removeSelectedRows()
def onCfgKeyboard(self, list, event):
""" Remove the selection if possible """
if gtk.gdk.keyval_name(event.keyval) == 'Delete':
self.removeSelectedLibraries(list)
def onCfgSelectionChanged(self, selection):
""" The selection has changed, update the status of the buttons """
self.cfgWindow.getWidget('btn-remove').set_sensitive(selection.count_selected_rows() != 0)
self.cfgWindow.getWidget('btn-rename').set_sensitive(selection.count_selected_rows() == 1)
self.cfgWindow.getWidget('btn-refresh').set_sensitive(selection.count_selected_rows() == 1)
def onHelp(self, btn):
""" Display a small help message box """
from gui import help
helpDlg = help.HelpDlg(MOD_L10N)
helpDlg.addSection(_('Description'),
_('This module organizes your media files by tags instead of using the file structure of your drive. '
'Loading tracks is also faster because their tags are already known and do not have to be read again.'))
helpDlg.addSection(_('Usage'),
_('When you add a new library, you have to give the full path to the root directory of that library. '
'Then, all directories under this root path are recursively scanned for media files whose tags are read '
'and stored in a database.') + '\n\n' + _('Upon refreshing a library, the file structure under the root '
'directory and all media files are scanned for changes, to update the database accordingly.'))
helpDlg.show(self.cfgWindow)
./decibel-audio-player-1.06/src/tools/ 0000755 0001750 0001750 00000000000 11456551413 017713 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/tools/__init__.py 0000644 0001750 0001750 00000014061 11456551413 022026 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import consts, cPickle, gtk, gtk.glade, os
__dirCache = {}
def listDir(directory, listHiddenFiles=False):
"""
Return a list of tuples (filename, path) with the given directory content
The dircache module sorts the list of files, and either it's not needed or it's not sorted the way we want
"""
if directory in __dirCache: cachedMTime, list = __dirCache[directory]
else: cachedMTime, list = None, None
if os.path.exists(directory): mTime = os.stat(directory).st_mtime
else: mTime = 0
if mTime != cachedMTime:
# Make sure it's readable
if os.access(directory, os.R_OK | os.X_OK): list = os.listdir(directory)
else: list = []
__dirCache[directory] = (mTime, list)
return [(filename, os.path.join(directory, filename)) for filename in list if listHiddenFiles or filename[0] != '.']
__downloadCache = {}
def cleanupDownloadCache():
""" Remove temporary downloaded files """
for (cachedTime, file) in __downloadCache.itervalues():
try: os.remove(file)
except: pass
def downloadFile(url, cacheTimeout=3600):
"""
If the file has been in the cache for less than 'cacheTimeout' seconds, return the cached file
Otherwise download the file and cache it
Return a tuple (errorMsg, data) where data is None if an error occurred, errorMsg containing the error message in this case
"""
import socket, tempfile, time, urllib2
if url in __downloadCache: cachedTime, file = __downloadCache[url]
else: cachedTime, file = -cacheTimeout, None
now = int(time.time())
# If the timeout is not exceeded, get the data from the cache
if (now - cachedTime) <= cacheTimeout:
try:
input = open(file, 'rb')
data = input.read()
input.close()
return ('', data)
except:
# If something went wrong with the cache, proceed to download
pass
# Make sure to not be blocked by the request
socket.setdefaulttimeout(consts.socketTimeout)
try:
# Retrieve the data
request = urllib2.Request(url)
stream = urllib2.urlopen(request)
data = stream.read()
# Do we need to create a new temporary file?
if file is None:
handle, file = tempfile.mkstemp()
os.close(handle)
# On first file added to the cache, we register our clean up function
if len(__downloadCache) == 0:
import atexit
atexit.register(cleanupDownloadCache)
__downloadCache[url] = (now, file)
output = open(file, 'wb')
output.write(data)
output.close()
return ('', data)
except urllib2.HTTPError, err:
return ('The request failed with error code %u' % err.code, None)
except:
return ('The request failed', None)
return ('Unknown error', None)
def sec2str(seconds, alwaysShowHours=False):
""" Return a formatted string based on the given duration in seconds """
hours, seconds = divmod(seconds, 3600)
minutes, seconds = divmod(seconds, 60)
if alwaysShowHours or hours != 0: return '%u:%02u:%02u' % (hours, minutes, seconds)
else: return '%u:%02u' % (minutes, seconds)
def loadGladeFile(file, root=None):
""" Load the given Glade file and return the tree of widgets """
if root is None: return gtk.glade.XML(os.path.join(consts.dirRes, file), domain=consts.appNameShort)
else: return gtk.glade.XML(os.path.join(consts.dirRes, file), root, consts.appNameShort)
def pickleLoad(file):
""" Use cPickle to load the data structure stored in the given file """
input = open(file, 'r')
data = cPickle.load(input)
input.close()
return data
def pickleSave(file, data):
""" Use cPickle to save the data to the given file """
output = open(file, 'w')
cPickle.dump(data, output)
output.close()
def touch(filePath):
""" Equivalent to the Linux 'touch' command """
os.system('touch "%s"' % filePath)
def percentEncode(string):
"""
Percent-encode all the bytes in the given string
Couldn't find a Python method to do that
"""
mask = '%%%X' * len(string)
bytes = tuple([ord(c) for c in string])
return mask % bytes
def getCursorPosition():
""" Return a tuple (x, y) """
cursorNfo = gtk.gdk.display_get_default().get_pointer()
return (cursorNfo[1], cursorNfo[2])
def htmlEscape(string):
""" Replace characters &, <, and > by their equivalent HTML code """
output = ''
for c in string:
if c == '&': output += '&'
elif c == '<': output += '<'
elif c == '>': output += '>'
else: output += c
return output
def splitPath(path):
"""
Return a list composed of all the elements forming the given path
For instance, splitPath('/some/path/foo') returns ['some', 'path', 'foo']
"""
path = os.path.abspath(path)
components = []
while True:
head, tail = os.path.split(path)
if tail == '':
return [head] + components
else:
path = head
components = [tail] + components
./decibel-audio-player-1.06/src/tools/prefs.py 0000644 0001750 0001750 00000003763 11456551413 021415 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os, threading, tools
# Load user preferences from the disk
try: __usrPrefs = tools.pickleLoad(tools.consts.filePrefs)
except: __usrPrefs = {}
__mutex = threading.Lock() # Prevent concurrent calls to functions
__appGlobals = {} # Some global values shared by all the components of the application
def save():
""" Save user preferences to the disk """
__mutex.acquire()
tools.pickleSave(tools.consts.filePrefs, __usrPrefs)
os.chmod(tools.consts.filePrefs, 0600)
__mutex.release()
def set(module, name, value):
""" Change the value of a preference """
__mutex.acquire()
__usrPrefs[module + '_' + name] = value;
__mutex.release()
def get(module, name, default=None):
""" Retrieve the value of a preference """
__mutex.acquire()
try: value = __usrPrefs[module + '_' + name]
except: value = default
__mutex.release()
return value
# Command line used to start the application
def setCmdLine(cmdLine): __appGlobals['cmdLine'] = cmdLine
def getCmdLine(): return __appGlobals['cmdLine']
# Main widgets' tree created by Glade
def setWidgetsTree(tree): __appGlobals['wTree'] = tree
def getWidgetsTree(): return __appGlobals['wTree']
./decibel-audio-player-1.06/src/tools/log.py 0000644 0001750 0001750 00000002447 11456551413 021055 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import consts
class Logger:
def __init__(self, filename):
""" Constructor """
self.handler = open(filename, 'wt')
def __log(self, msgType, msg):
""" Private logging function """
self.handler.write('%-6s %s\n' % (msgType, msg))
self.handler.flush()
def info(self, msg):
""" Information message """
self.__log('INFO', msg)
def error(self, msg):
""" Error message """
self.__log('ERROR', msg)
logger = Logger(consts.fileLog)
./decibel-audio-player-1.06/src/tools/consts.py 0000644 0001750 0001750 00000021222 11456551413 021575 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, os.path, random, time
from gettext import gettext as _
# --- Not a constant, but it fits well here
random.seed(int(time.time()))
# --- Miscellaneous
socketTimeout = 10
# --- Strings
appName = 'Decibel Audio Player'
appVersion = '1.06'
appNameShort = 'decibel-audio-player'
# --- URLs
urlMain = 'http://decibel.silent-blade.org'
urlHelp = 'http://decibel.silent-blade.org/index.php?n=Main.Help'
# --- Directories
dirBaseUsr = os.path.expanduser('~')
dirBaseCfg = os.path.join(dirBaseUsr, '.config')
dirBaseSrc = os.path.join(os.path.dirname(__file__), '..')
dirRes = os.path.join(dirBaseSrc, '..', 'res')
dirDoc = os.path.join(dirBaseSrc, '..', 'doc')
dirPix = os.path.join(dirBaseSrc, '..', 'pix')
dirCfg = os.path.join(dirBaseCfg, appNameShort)
dirLog = os.path.join(dirCfg, 'Logs')
dirLocale = os.path.join(dirBaseSrc, '..', 'locale')
if not os.path.isdir(dirLocale) :
dirLocale = os.path.join(dirBaseSrc, '..', '..', 'locale')
# Make sure the config directory exists
if not os.path.exists(dirBaseCfg):
os.mkdir(dirBaseCfg)
if not os.path.exists(dirCfg):
os.mkdir(dirCfg)
# Make sure the log directory exists
if not os.path.exists(dirLog): os.mkdir(dirLog)
# --- Icons
fileImgIcon16 = os.path.join(dirPix, 'decibel-audio-player-16.png')
fileImgIcon24 = os.path.join(dirPix, 'decibel-audio-player-24.png')
fileImgIcon32 = os.path.join(dirPix, 'decibel-audio-player-32.png')
fileImgIcon48 = os.path.join(dirPix, 'decibel-audio-player-48.png')
fileImgIcon64 = os.path.join(dirPix, 'decibel-audio-player-64.png')
fileImgIcon128 = os.path.join(dirPix, 'decibel-audio-player-128.png')
fileImgStar16 = os.path.join(dirPix, 'star-16.png')
fileImgCatAll = os.path.join(dirPix, 'category-all.png')
fileImgCatDesktop = os.path.join(dirPix, 'category-desktop.png')
fileImgCatDecibel = os.path.join(dirPix, 'category-decibel.png')
fileImgCatExplorer = os.path.join(dirPix, 'category-explorer.png')
fileImgCatInternet = os.path.join(dirPix, 'category-internet.png')
# --- Files
fileLog = os.path.join(dirLog, 'log')
filePrefs = os.path.join(dirCfg, 'prefs.txt')
fileLicense = os.path.join(dirDoc, 'LICENCE')
# --- DBus constants
dbusService = 'org.mpris.dap'
dbusInterface = 'org.freedesktop.MediaPlayer'
# --- Tracks
UNKNOWN_DATE = 0
UNKNOWN_GENRE = _('Unknown Genre')
UNKNOWN_TITLE = _('Unknown Title')
UNKNOWN_ALBUM = _('Unknown Album')
UNKNOWN_ARTIST = _('Unknown Artist')
UNKNOWN_LENGTH = 0
UNKNOWN_BITRATE = 0
UNKNOWN_ENC_MODE = 0
UNKNOWN_MB_TRACKID = 0
UNKNOWN_DISC_NUMBER = 0
UNKNOWN_SAMPLE_RATE = 0
UNKNOWN_TRACK_NUMBER = 0
UNKNOWN_ALBUM_ARTIST = _('Unknown Album Artist')
# --- Drag'n'Drop
(
DND_URI, # From another application (e.g., from Nautilus)
DND_DAP_URI, # Inside DAP when tags are not known (e.g., from the FileExplorer)
DND_DAP_TRACKS # Inside DAP when tags are already known (e.g., from the Library)
) = range(3)
DND_TARGETS = {
DND_URI: ('text/uri-list', 0, DND_URI),
DND_DAP_URI: ('dap/uri-list', gtk.TARGET_SAME_APP, DND_DAP_URI),
DND_DAP_TRACKS: ('dap/tracks-list', gtk.TARGET_SAME_APP, DND_DAP_TRACKS)
}
# --- View modes
(
VIEW_MODE_FULL,
VIEW_MODE_MINI,
VIEW_MODE_PLAYLIST,
) = range(3)
# -- Categories a module can belong to
(
MODCAT_NONE,
MODCAT_DECIBEL,
MODCAT_DESKTOP,
MODCAT_INTERNET,
MODCAT_EXPLORER,
) = range(5)
# --- Message that can be sent/received by modules
# --- A message is always associated with a (potentially empty) dictionnary containing its parameters
(
# --== COMMANDS ==--
# GStreamer player
MSG_CMD_PLAY, # Play a resource Parameters: 'uri'
MSG_CMD_STOP, # Stop playing Parameters:
MSG_CMD_SEEK, # Jump to a position Parameters: 'seconds'
MSG_CMD_STEP, # Step back or forth Parameters: 'seconds'
MSG_CMD_SET_VOLUME, # Change the volume Parameters: 'value'
MSG_CMD_BUFFER, # Buffer a file Parameters: 'filename'
MSG_CMD_TOGGLE_PAUSE, # Toggle play/pause Parameters:
MSG_CMD_ENABLE_EQZ, # Enable the equalizer Parameters:
MSG_CMD_SET_EQZ_LVLS, # Set the levels of the 10-bands equalizer Parameters: 'lvls'
MSG_CMD_ENABLE_RG, # Enable ReplayGain Parameters:
MSG_CMD_SET_CD_SPEED, # Change drive speed when reading a CD Parameters: 'speed'
# Tracklist
MSG_CMD_NEXT, # Play the next track Parameters:
MSG_CMD_PREVIOUS, # Play the previous track Parameters:
MSG_CMD_TRACKLIST_SET, # Replace tracklist Parameters: 'tracks', 'playNow'
MSG_CMD_TRACKLIST_ADD, # Extend tracklist Parameters: 'tracks', 'playNow'
MSG_CMD_TRACKLIST_DEL, # Remove a track Parameters: 'idx'
MSG_CMD_TRACKLIST_CLR, # Clear tracklist Parameters:
MSG_CMD_TRACKLIST_SHUFFLE, # Shuffle the tracklist Parameters:
MSG_CMD_TRACKLIST_REPEAT, # Set/Unset the repeat function Parameters: 'repeat'
# Explorers
MSG_CMD_EXPLORER_ADD, # Add a new explorer Parameters: 'modName', 'expName', 'icon', 'widget'
MSG_CMD_EXPLORER_REMOVE, # Remove an explorer Parameters: 'modName', 'expName'
MSG_CMD_EXPLORER_RENAME, # Rename an explorer Parameters: 'modName', 'expName', 'newExpName'
# Covers
MSG_CMD_SET_COVER, # Cover file for the given track Parameters: 'track', 'pathThumbnail', 'pathFullSize'
# Misc
MSG_CMD_THREAD_EXECUTE, # An *internal* command for threaded modules Parameters: N/A
# --== EVENTS ==--
# Current track
MSG_EVT_PAUSED, # Paused Parameters:
MSG_EVT_STOPPED, # Stopped Parameters:
MSG_EVT_UNPAUSED, # Unpaused Parameters:
MSG_EVT_NEW_TRACK, # The current track has changed Parameters: 'track'
MSG_EVT_NEED_BUFFER, # The next track should be buffered Parameters:
MSG_EVT_TRACK_POSITION, # New position in the current track Parameters: 'seconds'
MSG_EVT_TRACK_ENDED_OK, # The current track has ended Parameters:
MSG_EVT_TRACK_ENDED_ERROR, # The current track has ended because of an error Parameters:
# GStreamer player
MSG_EVT_VOLUME_CHANGED, # The volume has changed Parameters: 'value'
# Tracklist
MSG_EVT_TRACK_MOVED, # The position of the current track has changed Parameters: 'hasPrevious', 'hasNext'
MSG_EVT_NEW_TRACKLIST, # A new tracklist has been set Parameters: 'tracks', 'playtime'
MSG_EVT_REPEAT_CHANGED, # The repeat function has been enabled/disabled Parameters: 'repeat'
MSG_EVT_TRACKLIST_NEW_SEL, # The tracklist has a new set of selected tracks Parameters: 'tracks'
# Application
MSG_EVT_APP_QUIT, # The application is quitting Parameters:
MSG_EVT_APP_STARTED, # The application has just started Parameters:
# Modules
MSG_EVT_MOD_LOADED, # The module has been loaded by request of the user Parameters:
MSG_EVT_MOD_UNLOADED, # The module has been unloaded by request of the user Parameters:
# Explorer manager
MSG_EVT_EXPLORER_CHANGED, # A new explorer has been selected Parameters: 'modName', 'expName'
# End value
MSG_END_VALUE
) = range(43)
./decibel-audio-player-1.06/src/tools/icons.py 0000644 0001750 0001750 00000012337 11456551413 021406 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk
from tools import consts
__lbl = None
__dirBtnIcon = None
__dirMenuIcon = None
__starMenuIcon = None
__prefsBtnIcon = None
__nullMenuIcon = None
__playMenuIcon = None
__pauseMenuIcon = None
__cdromMenuIcon = None
__errorMenuIcon = None
__starDirMenuIcon = None
__mediaDirMenuIcon = None
__mediaFileMenuIcon = None
__catDesktopIcon = None
__catDecibelIcon = None
__catExplorerIcon = None
__catInternetIcon = None
def __render(stock, size):
""" Return the given stock icon rendered at the given size """
global __lbl
if __lbl is None:
__lbl = gtk.Label()
return __lbl.render_icon(stock, size)
def dirMenuIcon():
""" Directories """
global __dirMenuIcon
if __dirMenuIcon is None:
__dirMenuIcon = __render(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_MENU)
return __dirMenuIcon
def dirBtnIcon():
""" Directories """
global __dirBtnIcon
if __dirBtnIcon is None:
__dirBtnIcon = __render(gtk.STOCK_DIRECTORY, gtk.ICON_SIZE_BUTTON)
return __dirBtnIcon
def prefsBtnIcon():
""" Preferences """
global __prefsBtnIcon
if __prefsBtnIcon is None:
__prefsBtnIcon = __render(gtk.STOCK_PREFERENCES, gtk.ICON_SIZE_BUTTON)
return __prefsBtnIcon
def playMenuIcon():
""" Play """
global __playMenuIcon
if __playMenuIcon is None:
__playMenuIcon = __render(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_MENU)
return __playMenuIcon
def pauseMenuIcon():
""" Pause """
global __pauseMenuIcon
if __pauseMenuIcon is None:
__pauseMenuIcon = __render(gtk.STOCK_MEDIA_PAUSE, gtk.ICON_SIZE_MENU)
return __pauseMenuIcon
def cdromMenuIcon():
""" CD-ROM """
global __cdromMenuIcon
if __cdromMenuIcon is None:
__cdromMenuIcon = __render(gtk.STOCK_CDROM, gtk.ICON_SIZE_MENU)
return __cdromMenuIcon
def starMenuIcon():
""" Star """
global __starMenuIcon
if __starMenuIcon is None:
__starMenuIcon = gtk.gdk.pixbuf_new_from_file(consts.fileImgStar16)
return __starMenuIcon
def errorMenuIcon():
""" Error """
global __errorMenuIcon
if __errorMenuIcon is None:
__errorMenuIcon = __render(gtk.STOCK_CANCEL, gtk.ICON_SIZE_MENU)
return __errorMenuIcon
def nullMenuIcon():
""" Transparent icon """
global __nullMenuIcon
if __nullMenuIcon is None:
__nullMenuIcon = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16)
__nullMenuIcon.fill(0x00000000)
return __nullMenuIcon
def mediaDirMenuIcon():
""" Media directory """
global __mediaDirMenuIcon
if __mediaDirMenuIcon is None:
__mediaDirMenuIcon = dirMenuIcon().copy() # We need a copy to modify it
cdromMenuIcon().composite(__mediaDirMenuIcon, 5, 5, 11, 11, 5, 5, 0.6875, 0.6875, gtk.gdk.INTERP_HYPER, 255)
return __mediaDirMenuIcon
def starDirMenuIcon():
""" Starred directory """
global __starDirMenuIcon
if __starDirMenuIcon is None:
__starDirMenuIcon = dirMenuIcon().copy() # We need a copy to modify it
starMenuIcon().composite(__starDirMenuIcon, 5, 5, 11, 11, 5, 5, 0.6875, 0.6875, gtk.gdk.INTERP_HYPER, 255)
return __starDirMenuIcon
def mediaFileMenuIcon():
""" Media file """
global __mediaFileMenuIcon
if __mediaFileMenuIcon is None:
__mediaFileMenuIcon = __render(gtk.STOCK_FILE, gtk.ICON_SIZE_MENU).copy() # We need a copy to modify it
cdromMenuIcon().composite(__mediaFileMenuIcon, 5, 5, 11, 11, 5, 5, 0.6875, 0.6875, gtk.gdk.INTERP_HYPER, 255)
return __mediaFileMenuIcon
def catDecibelIcon():
""" Directories """
global __catDecibelIcon
if __catDecibelIcon is None:
__catDecibelIcon = gtk.gdk.pixbuf_new_from_file(consts.fileImgCatDecibel)
return __catDecibelIcon
def catDesktopIcon():
""" Directories """
global __catDesktopIcon
if __catDesktopIcon is None:
__catDesktopIcon = gtk.gdk.pixbuf_new_from_file(consts.fileImgCatDesktop)
return __catDesktopIcon
def catInternetIcon():
""" Directories """
global __catInternetIcon
if __catInternetIcon is None:
__catInternetIcon = gtk.gdk.pixbuf_new_from_file(consts.fileImgCatInternet)
return __catInternetIcon
def catExplorerIcon():
""" Directories """
global __catExplorerIcon
if __catExplorerIcon is None:
__catExplorerIcon = gtk.gdk.pixbuf_new_from_file(consts.fileImgCatExplorer)
return __catExplorerIcon
./decibel-audio-player-1.06/src/media/ 0000755 0001750 0001750 00000000000 11456551413 017632 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/media/__init__.py 0000644 0001750 0001750 00000007500 11456551413 021745 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os, playlist, traceback
from format import monkeysaudio, asf, flac, mp3, mp4, mpc, ogg, wavpack
from os.path import splitext
from tools.log import logger
from track.fileTrack import FileTrack
# Supported formats with associated modules
mFormats = {'.ac3': monkeysaudio, '.ape': monkeysaudio, '.flac': flac, '.m4a': mp4, '.mp2': mp3, '.mp3': mp3, '.mp4': mp4, '.mpc': mpc,'.oga': ogg, '.ogg': ogg, '.wma': asf, '.wv': wavpack}
def isSupported(file):
""" Return True if the given file is a supported format """
try: return splitext(file.lower())[1] in mFormats
except: return False
def getSupportedFormats():
""" Return a list of all formats from which tags can be extracted """
return ['*' + ext for ext in mFormats]
def getTrackFromFile(file):
"""
Return a Track object, based on the tags of the given file
The 'file' parameter must be a real file (not a playlist or a directory)
"""
try:
return mFormats[splitext(file.lower())[1]].getTrack(file)
except:
logger.error('Unable to extract information from %s\n\n%s' % (file, traceback.format_exc()))
return FileTrack(file)
def getTracksFromFiles(files):
""" Same as getTrackFromFile(), but works on a list of files instead of a single one """
return [getTrackFromFile(file) for file in files]
def getTracks(filenames, sortByFilename=False, ignoreHiddenFiles=True):
"""
Same as getTracksFromFiles(), but works for any kind of filenames (files, playlists, directories)
If sortByFilename is True, files loaded from directories are sorted by filename instead of tags
If ignoreHiddenFiles is True, hidden files are ignored when walking directories
"""
allTracks = []
# Directories
for directory in [filename for filename in filenames if os.path.isdir(filename)]:
mediaFiles, playlists = [], []
for root, subdirs, files in os.walk(directory):
for file in files:
if not ignoreHiddenFiles or file[0] != '.':
if isSupported(file): mediaFiles.append(os.path.join(root, file))
elif playlist.isSupported(file): playlists.append(os.path.join(root, file))
if sortByFilename: allTracks.extend(sorted(getTracksFromFiles(mediaFiles), lambda t1, t2: cmp(t1.getFilePath(), t2.getFilePath())))
else: allTracks.extend(sorted(getTracksFromFiles(mediaFiles)))
for pl in playlists:
allTracks.extend(getTracksFromFiles(playlist.load(pl)))
# Files
tracks = getTracksFromFiles([filename for filename in filenames if os.path.isfile(filename) and isSupported(filename)])
if sortByFilename: allTracks.extend(sorted(tracks, lambda t1, t2: cmp(t1.getFilePath(), t2.getFilePath())))
else: allTracks.extend(sorted(tracks))
# Playlists
for pl in [filename for filename in filenames if os.path.isfile(filename) and playlist.isSupported(filename)]:
allTracks.extend(getTracksFromFiles(playlist.load(pl)))
return allTracks
./decibel-audio-player-1.06/src/media/playlist.py 0000644 0001750 0001750 00000003364 11456551413 022053 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import media, os.path
def isSupported(file):
""" Return True if the file has a supported format """
return file.lower()[-4:] == '.m3u'
def getSupportedFormats():
""" Return a list of supported playlist formats """
return ['*.m3u']
def save(files, playlist):
""" Create a playlist with the given files """
output = open(playlist, 'w')
output.writelines('\n'.join(files))
output.close()
def load(playlist):
""" Return the list of files loaded from the given playlist """
if not os.path.isfile(playlist):
return []
input = open(playlist)
files = [line for line in [line.strip() for line in input] if len(line) != 0 and line[0] != '#']
input.close()
path = os.path.dirname(playlist)
for i, file in enumerate(files):
if not os.path.isabs(file):
files[i] = os.path.join(path, file.replace('\\', '/'))
return [file for file in files if os.path.isfile(file) and media.isSupported(file)]
./decibel-audio-player-1.06/src/media/audioplayer.py 0000644 0001750 0001750 00000017175 11456551413 022535 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gst
class AudioPlayer:
def __init__(self, callbackEnded, usePlaybin2=True):
""" Constructor """
self.player = None
self.volume = 1
self.rgEnabled = False
self.eqzLevels = None
self.equalizer = None
self.eqzEnabled = False
self.usePlaybin2 = usePlaybin2
self.cdReadSpeed = 1
self.callbackEnded = callbackEnded
def __getPlayer(self):
""" Construct and return the GStreamer player """
self.__constructPlayer()
self.__getPlayer = self.__getPlayer_post # I love Python
return self.player
def __getPlayer_post(self):
""" Return the GStreamer player """
return self.player
def __constructPlayer(self):
""" Create the GStreamer pipeline """
if self.usePlaybin2:
self.player = gst.element_factory_make('playbin2', 'player')
self.player.connect('about-to-finish', self.__onAboutToFinish)
else:
self.player = gst.element_factory_make('playbin', 'player')
# No video
self.player.set_property('video-sink', gst.element_factory_make('fakesink', 'fakesink'))
# Restore volume
self.player.set_property('volume', self.volume)
# Change the audio sink to our own bin, so that an equalizer/replay gain element can be added later on if needed
self.audiobin = gst.Bin('audiobin')
self.audiosink = gst.element_factory_make('autoaudiosink', 'audiosink')
# Callback when the source of the playbin is changed
self.player.connect('notify::source', self.__onNewPlaybinSource)
self.audiobin.add(self.audiosink)
self.audiobin.add_pad(gst.GhostPad('sink', self.audiosink.get_pad('sink')))
self.player.set_property('audio-sink', self.audiobin)
# Monitor messages generated by the player
bus = self.player.get_bus()
bus.add_signal_watch()
bus.connect('message', self.__onGstMessage)
# Add equalizer?
if self.eqzEnabled:
self.equalizer = gst.element_factory_make('equalizer-10bands', 'equalizer')
self.audiobin.add(self.equalizer)
self.audiobin.get_pad('sink').set_target(self.equalizer.get_pad('sink'))
self.equalizer.link(self.audiosink)
if self.eqzLevels is not None:
self.setEqualizerLvls(self.eqzLevels)
# Add replay gain?
if self.rgEnabled:
replaygain = gst.element_factory_make('rgvolume', 'replaygain')
self.audiobin.add(replaygain)
self.audiobin.get_pad('sink').set_target(replaygain.get_pad('sink'))
if self.equalizer is None: replaygain.link(self.audiosink)
else: replaygain.link(self.equalizer)
def enableEqualizer(self):
""" Add an equalizer to the audio chain """
self.eqzEnabled = True
def enableReplayGain(self):
""" Add/Enable a replay gain element """
self.rgEnabled = True
def setEqualizerLvls(self, lvls):
""" Set the level of the 10-bands of the equalizer (levels must be a list/tuple with 10 values lying between -24 and +12) """
if len(lvls) == 10:
self.eqzLevels = lvls
if self.equalizer is not None:
self.equalizer.set_property('band0', lvls[0])
self.equalizer.set_property('band1', lvls[1])
self.equalizer.set_property('band2', lvls[2])
self.equalizer.set_property('band3', lvls[3])
self.equalizer.set_property('band4', lvls[4])
self.equalizer.set_property('band5', lvls[5])
self.equalizer.set_property('band6', lvls[6])
self.equalizer.set_property('band7', lvls[7])
self.equalizer.set_property('band8', lvls[8])
self.equalizer.set_property('band9', lvls[9])
def __onNewPlaybinSource(self, playbin, params):
""" Change the CR-ROM drive speed to 1 when applicable """
source = self.__getPlayer().get_by_name('source')
# Didn't find a way to determine the real class of source
# So we use the 'paranoia-mode' property to determine whether it's indeed a CD we're playing
try:
source.get_property('paranoia-mode')
source.set_property('read-speed', self.cdReadSpeed)
except:
pass
def __onAboutToFinish(self, isLast):
""" End of the track """
self.callbackEnded(False)
def __onGstMessage(self, bus, msg):
""" A new message generated by the player """
if msg.type == gst.MESSAGE_EOS:
self.callbackEnded(False)
elif msg.type == gst.MESSAGE_ERROR:
self.stop()
# It seems that the pipeline may not be able to play again any valid stream when an error occurs
# We thus create a new one, even if that's quite a ugly solution
self.__constructPlayer()
self.callbackEnded(True)
return True
def setCDReadSpeed(self, speed):
""" Set the CD-ROM drive read speed """
self.cdReadSpeed = speed
def setNextURI(self, uri):
""" Set the next URI """
self.__getPlayer().set_property('uri', uri.replace('%', '%25').replace('#', '%23'))
def setVolume(self, level):
""" Set the volume to the given level (0 <= level <= 1) """
if level < 0: self.volume = 0
elif level > 1: self.volume = 1
else: self.volume = level
if self.player is not None:
self.player.set_property('volume', self.volume)
def isPaused(self):
""" Return whether the player is paused """
return self.__getPlayer().get_state()[1] == gst.STATE_PAUSED
def isPlaying(self):
""" Return whether the player is paused """
return self.__getPlayer().get_state()[1] == gst.STATE_PLAYING
def setURI(self, uri):
""" Play the given URI """
self.__getPlayer().set_property('uri', uri.replace('%', '%25').replace('#', '%23'))
def play(self):
""" Play """
self.__getPlayer().set_state(gst.STATE_PLAYING)
def pause(self):
""" Pause """
self.__getPlayer().set_state(gst.STATE_PAUSED)
def stop(self):
""" Stop playing """
self.__getPlayer().set_state(gst.STATE_NULL)
def seek(self, where):
""" Jump to the given location """
self.__getPlayer().seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, where)
def getPosition(self):
""" Return the current position """
try: return self.__getPlayer().query_position(gst.FORMAT_TIME)[0]
except: return 0
def getDuration(self):
""" Return the duration of the current stream """
try: return self.__getPlayer().query_duration(gst.FORMAT_TIME)[0]
except: return 0
./decibel-audio-player-1.06/src/media/track/ 0000755 0001750 0001750 00000000000 11456551413 020736 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/media/track/__init__.py 0000644 0001750 0001750 00000034773 11456551413 023065 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import os.path, tools
from tools import consts, sec2str
from gettext import gettext as _
# Tags asscociated to a track
# The order should not be changed for compatibility reasons
(
TAG_RES, # Full path to the resource
TAG_SCH, # URI scheme (e.g., file, cdda)
TAG_NUM, # Track number
TAG_TIT, # Title
TAG_ART, # Artist
TAG_ALB, # Album
TAG_LEN, # Length in seconds
TAG_AAR, # Album artist
TAG_DNB, # Disc number
TAG_GEN, # Genre
TAG_DAT, # Year
TAG_MBT, # MusicBrainz track id
TAG_PLP, # Position in the playlist
TAG_PLL, # Length of the playlist
TAG_BTR, # Bit rate
TAG_MOD, # Constant or variable bit rate
TAG_SMP, # Sample rate
) = range(17)
# Special fields that may be used to call format()
FIELDS = (
( 'track' , _('Track number') ),
( 'title' , '\t' + _('Title') ),
( 'artist' , _('Artist') ),
( 'album' , _('Album') ),
( 'genre' , _('Genre') ),
( 'date' , _('Date') ),
( 'disc' , _('Disc number') ),
( 'bitrate' , _('Bit rate') ),
( 'sample_rate' , _('Sample rate') ),
( 'duration_sec', _('Duration in seconds (e.g., 194)') ),
( 'duration_str', _('Duration as a string (e.g., 3:14)') ),
( 'playlist_pos', _('Position of the track in the playlist') ),
( 'playlist_len', _('Number of tracks in the playlist') ),
( 'path' , _('Full path to the file') ),
)
def getFormatSpecialFields(usePango=True):
"""
Return a string in plain English (or whatever language being used) giving the special fields that may be used to call Track.format()
If usePango is True, the returned string uses Pango formatting for better presentation
"""
if usePango: return '\n'.join(['%s %s' % (field.ljust(14), desc) for (field, desc) in FIELDS])
else: return '\n'.join(['%s\t%s' % (field.ljust(14), desc) for (field, desc) in FIELDS])
class Track:
""" A track and its associated tags """
def __init__(self, resource=None, scheme=None):
""" Constructor """
self.tags = {}
if scheme is not None: self.tags[TAG_SCH] = scheme
if resource is not None: self.tags[TAG_RES] = resource
def setNumber(self, nb): self.tags[TAG_NUM] = nb
def setTitle(self, title): self.tags[TAG_TIT] = title
def setArtist(self, artist): self.tags[TAG_ART] = artist
def setAlbum(self, album): self.tags[TAG_ALB] = album
def setLength(self, length): self.tags[TAG_LEN] = length
def setAlbumArtist(self, albumArtist): self.tags[TAG_AAR] = albumArtist
def setDiscNumber(self, discNumber): self.tags[TAG_DNB] = discNumber
def setGenre(self, genre): self.tags[TAG_GEN] = genre
def setDate(self, date): self.tags[TAG_DAT] = date
def setMBTrackId(self, id): self.tags[TAG_MBT] = id
def setPlaylistPos(self, pos): self.tags[TAG_PLP] = pos
def setPlaylistLen(self, len): self.tags[TAG_PLL] = len
def setBitrate(self, bitrate): self.tags[TAG_BTR] = bitrate
def setSampleRate(self, sampleRate): self.tags[TAG_SMP] = sampleRate
def setVariableBitrate(self): self.tags[TAG_MOD] = 1
def hasNumber(self): return TAG_NUM in self.tags
def hasTitle(self): return TAG_TIT in self.tags
def hasArtist(self): return TAG_ART in self.tags
def hasAlbum(self): return TAG_ALB in self.tags
def hasLength(self): return TAG_LEN in self.tags
def hasAlbumArtist(self): return TAG_AAR in self.tags
def hasDiscNumber(self): return TAG_DNB in self.tags
def hasGenre(self): return TAG_GEN in self.tags
def hasDate(self): return TAG_DAT in self.tags
def hasMBTrackId(self): return TAG_MBT in self.tags
def hasPlaylistPos(self): return TAG_PLP in self.tags
def hasPlaylistLen(self): return TAG_PLL in self.tags
def hasBitrate(self): return TAG_BTR in self.tags
def hasSampleRate(self): return TAG_SMP in self.tags
def __get(self, tag, defaultValue):
""" Return the value of tag if it exists, or return defaultValue """
try: return self.tags[tag]
except: return defaultValue
def getFilePath(self): return self.tags[TAG_RES]
def getNumber(self): return self.__get(TAG_NUM, consts.UNKNOWN_TRACK_NUMBER)
def getTitle(self): return self.__get(TAG_TIT, consts.UNKNOWN_TITLE)
def getArtist(self): return self.__get(TAG_ART, consts.UNKNOWN_ARTIST)
def getAlbum(self): return self.__get(TAG_ALB, consts.UNKNOWN_ALBUM)
def getLength(self): return self.__get(TAG_LEN, consts.UNKNOWN_LENGTH)
def getAlbumArtist(self): return self.__get(TAG_AAR, consts.UNKNOWN_ALBUM_ARTIST)
def getDiscNumber(self): return self.__get(TAG_DNB, consts.UNKNOWN_DISC_NUMBER)
def getGenre(self): return self.__get(TAG_GEN, consts.UNKNOWN_GENRE)
def getDate(self): return self.__get(TAG_DAT, consts.UNKNOWN_DATE)
def getEncMode(self): return self.__get(TAG_MOD, consts.UNKNOWN_ENC_MODE)
def getMBTrackId(self): return self.__get(TAG_MBT, consts.UNKNOWN_MB_TRACKID)
def getPlaylistPos(self): return self.__get(TAG_PLP, -1)
def getPlaylistLen(self): return self.__get(TAG_PLL, -1)
def getBitrate(self):
""" Transform the bit rate into a string """
bitrate = self.__get(TAG_BTR, consts.UNKNOWN_BITRATE)
if bitrate == -1: return _('N/A')
elif self.getEncMode() == 1: return '~ %u kbps' % (bitrate / 1000)
else: return '%u kbps' % (bitrate / 1000)
def getSampleRate(self):
""" Transform the sample rate into a string"""
return '%.1f kHz' % (self.__get(TAG_SMP, consts.UNKNOWN_SAMPLE_RATE) / 1000.0)
def getSafeNumber(self): return str(self.__get(TAG_NUM, ''))
def getSafeTitle(self): return self.__get(TAG_TIT, '')
def getSafeArtist(self): return self.__get(TAG_ART, '')
def getSafeAlbum(self): return self.__get(TAG_ALB, '')
def getSafeLength(self): return str(self.__get(TAG_LEN, ''))
def getSafeMBTrackId(self): return self.__get(TAG_MBT, '')
def getURI(self):
""" Return the complete URI to the resource """
try: return self.tags[TAG_SCH] + '://' + self.tags[TAG_RES]
except: raise RuntimeError, 'The track is an unknown type of resource'
def getExtendedAlbum(self):
""" Return the album name plus the disc number, if any """
if self.getDiscNumber() != consts.UNKNOWN_DISC_NUMBER:
return _('%(album)s [Disc %(discnum)u]') % {'album': self.getAlbum(), 'discnum': self.getDiscNumber()}
else:
return self.getAlbum()
def getType(self):
""" Return the format of the track """
if self.tags[TAG_SCH] == 'cdda': return _('CDDA Track')
else: return os.path.splitext(self.tags[TAG_RES])[1][1:].lower()
def __str__(self):
""" String representation """
return '%s - %s - %s (%u)' % (self.getArtist(), self.getAlbum(), self.getTitle(), self.getNumber())
def __cmp__(self, track):
""" Compare two tracks"""
# Artist
if self.hasAlbumArtist(): selfArtist = self.getAlbumArtist()
else: selfArtist = self.getArtist()
if track.hasAlbumArtist(): otherArtist = track.getAlbumArtist()
else: otherArtist = track.getArtist()
result = cmp(selfArtist.lower(), otherArtist.lower())
if result != 0:
return result
# Album
result = cmp(self.getAlbum().lower(), track.getAlbum().lower())
if result != 0:
return result
# Disc number
result = self.getDiscNumber() - track.getDiscNumber()
if result != 0:
return result
# Track number
result = self.getNumber() - track.getNumber()
if result != 0:
return result
# Finally, file names
return cmp(self.getFilePath(), track.getFilePath())
def format(self, fmtString):
""" Replace the special fields in the given string by their corresponding value """
result = fmtString
result = result.replace( '{path}', self.getFilePath() )
result = result.replace( '{album}', self.getAlbum() )
result = result.replace( '{track}', str(self.getNumber()) )
result = result.replace( '{title}', self.getTitle() )
result = result.replace( '{artist}', self.getArtist() )
result = result.replace( '{genre}', self.getGenre() )
result = result.replace( '{date}', str(self.getDate()) )
result = result.replace( '{disc}', str(self.getDiscNumber()) )
result = result.replace( '{bitrate}', self.getBitrate() )
result = result.replace( '{sample_rate}', str(self.getSampleRate()) )
result = result.replace( '{duration_sec}', str(self.getLength()) )
result = result.replace( '{duration_str}', sec2str(self.getLength()) )
result = result.replace( '{playlist_pos}', str(self.getPlaylistPos()) )
result = result.replace( '{playlist_len}', str(self.getPlaylistLen()) )
return result
def formatHTMLSafe(self, fmtString):
"""
Replace the special fields in the given string by their corresponding value
Also ensure that the fields don't contain HTML special characters (&, <, >)
"""
result = fmtString
result = result.replace( '{path}', tools.htmlEscape(self.getFilePath()) )
result = result.replace( '{album}', tools.htmlEscape(self.getAlbum()) )
result = result.replace( '{track}', str(self.getNumber()) )
result = result.replace( '{title}', tools.htmlEscape(self.getTitle()) )
result = result.replace( '{artist}', tools.htmlEscape(self.getArtist()) )
result = result.replace( '{genre}', tools.htmlEscape(self.getGenre()) )
result = result.replace( '{date}', str(self.getDate()) )
result = result.replace( '{disc}', str(self.getDiscNumber()) )
result = result.replace( '{bitrate}', self.getBitrate() )
result = result.replace( '{sample_rate}', self.getSampleRate() )
result = result.replace( '{duration_sec}', str(self.getLength()) )
result = result.replace( '{duration_str}', sec2str(self.getLength()) )
result = result.replace( '{playlist_pos}', str(self.getPlaylistPos()) )
result = result.replace( '{playlist_len}', str(self.getPlaylistLen()) )
return result
def __addIfKnown(self, dic, key, tag, unknownValue):
""" This is an helper function used by the getMPRISMetadata() function """
value = self.__get(tag, unknownValue)
if value != unknownValue:
dic[key] = value
def getMPRISMetadata(self):
""" Return a dictionary with all available data in an MPRIS-compatible format """
data = {'location': self.getURI()}
self.__addIfKnown(data, 'tracknumber', TAG_NUM, consts.UNKNOWN_TRACK_NUMBER)
self.__addIfKnown(data, 'title', TAG_TIT, consts.UNKNOWN_TITLE)
self.__addIfKnown(data, 'time', TAG_LEN, consts.UNKNOWN_LENGTH)
self.__addIfKnown(data, 'artist', TAG_ART, consts.UNKNOWN_ARTIST)
self.__addIfKnown(data, 'album', TAG_ALB, consts.UNKNOWN_ALBUM)
self.__addIfKnown(data, 'mb track id', TAG_MBT, consts.UNKNOWN_MB_TRACKID)
self.__addIfKnown(data, 'genre', TAG_GEN, consts.UNKNOWN_GENRE)
self.__addIfKnown(data, 'date', TAG_DAT, consts.UNKNOWN_DATE)
self.__addIfKnown(data, 'audio-bitrate', TAG_BTR, -1)
self.__addIfKnown(data, 'audio-samplerate', TAG_SMP, consts.UNKNOWN_SAMPLE_RATE)
# 'mtime' must be in milliseconds
if 'time' in data:
data['mtime'] = data['time'] * 1000
return data
def getTags(self):
""" Return the disctionary of tags """
return self.tags
def setTags(self, tags):
""" Set the disctionary of tags """
self.tags = tags
def serialize(self):
""" Serialize this Track object, return the corresponding string """
tags = []
for tag, value in self.tags.iteritems():
tags.append(str(tag))
tags.append((str(value)).replace(' ', '\x00'))
return ' '.join(tags)
def unserialize(self, serialTrack):
""" Unserialize the given track"""
tags = serialTrack.split(' ')
for i in xrange(0, len(tags), 2):
tag = int(tags[i])
if tag in (TAG_NUM, TAG_LEN, TAG_DNB, TAG_DAT, TAG_PLP, TAG_PLL, TAG_BTR, TAG_SMP, TAG_MOD): self.tags[tag] = int(tags[i+1])
else: self.tags[tag] = tags[i+1].replace('\x00', ' ')
def unserialize(serialTrack):
""" Return the Track object corresponding to the given serialized version """
t = Track()
t.unserialize(serialTrack)
return t
./decibel-audio-player-1.06/src/media/track/cdTrack.py 0000644 0001750 0001750 00000001774 11456551413 022674 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.track import Track
class CDTrack(Track):
""" A Track that has been created from an audio CD """
def __init__(self, resource):
""" Constructor """
Track.__init__(self, resource, 'cdda')
./decibel-audio-player-1.06/src/media/track/fileTrack.py 0000644 0001750 0001750 00000001771 11456551413 023222 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.track import Track
class FileTrack(Track):
""" A Track that has been created from a file """
def __init__(self, resource):
""" Constructor """
Track.__init__(self, resource, 'file')
./decibel-audio-player-1.06/src/media/format/ 0000755 0001750 0001750 00000000000 11456551413 021122 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/media/format/__init__.py 0000644 0001750 0001750 00000004525 11456551413 023241 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
def createFileTrack(file, bitrate, length, samplerate, isVBR, title=None, album=None, artist=None, albumArtist=None,
musicbrainzId=None, genre=None, trackNumber=None, date=None, discNumber=None):
""" Create a new FileTrack object based on the given information """
from media.track.fileTrack import FileTrack
track = FileTrack(file)
track.setLength(length)
track.setBitrate(bitrate)
track.setSampleRate(samplerate)
if isVBR:
track.setVariableBitrate()
if title is not None:
track.setTitle(title)
if album is not None:
track.setAlbum(album)
if artist is not None:
track.setArtist(artist)
if albumArtist is not None:
track.setAlbumArtist(albumArtist)
if musicbrainzId is not None:
track.setMBTrackId(musicbrainzId)
if genre is not None:
track.setGenre(genre)
if date is not None:
try: track.setDate(int(date))
except: pass
# The format of the track number may be 'X' or 'X/Y'
# We discard Y since we don't use this information
if trackNumber is not None:
try: track.setNumber(int(trackNumber.split('/')[0]))
except: pass
# The format of the disc number may be 'X' or 'X/Y'
# We discard the disc number when Y is less than 2
if discNumber is not None:
try:
discNumber = discNumber.split('/')
if len(discNumber) == 1 or int(discNumber[1]) > 1:
track.setDiscNumber(int(discNumber[0]))
except:
pass
return track
./decibel-audio-player-1.06/src/media/format/wavpack.py 0000644 0001750 0001750 00000003473 11456551413 023137 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from a WavPack file """
from mutagen.wavpack import WavPack
wvFile = WavPack(filename)
length = int(round(wvFile.info.length))
samplerate = int(wvFile.info.sample_rate)
try: title = str(wvFile['Title'][0])
except: title = None
try: album = str(wvFile['Album'][0])
except: album = None
try: artist = str(wvFile['Artist'][0])
except: artist = None
try: albumArtist = str(wvFile['Album Artist'][0])
except: albumArtist = None
try: genre = str(wvFile['genre'][0])
except: genre = None
try: trackNumber = str(wvFile['Track'][0])
except: trackNumber = None
try: discNumber = str(wvFile['Disc'][0])
except: discNumber = None
try: date = str(wvFile['Year'][0])
except: date = None
return createFileTrack(filename, -1, length, samplerate, False, title, album, artist, albumArtist,
None, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/mp3.py 0000644 0001750 0001750 00000004204 11456551413 022173 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an mp3 file """
from mutagen.mp3 import MP3
from mutagen.id3 import ID3
mp3File = MP3(filename)
length = int(round(mp3File.info.length))
bitrate = int(mp3File.info.bitrate)
samplerate = int(mp3File.info.sample_rate)
if mp3File.info.mode == 1: isVBR = True
else: isVBR = False
try: id3 = ID3(filename)
except: return createFileTrack(filename, bitrate, length, samplerate, isVBR)
try: title = str(id3['TIT2'])
except: title = None
try: album = str(id3['TALB'])
except: album = None
try: artist = str(id3['TPE1'])
except: artist = None
try: albumArtist = str(id3['TPE2'])
except: albumArtist = None
try: musicbrainzId = id3['UFID:http://musicbrainz.org'].data
except: musicbrainzId = None
try: genre = str(id3['TCON'])
except: genre = None
try: trackNumber = str(id3['TRCK'])
except: trackNumber = None
try: date = str(id3['TDRC'][0].year)
except: date = None
try: discNumber = str(id3['TPOS'])
except: discNumber = None
return createFileTrack(filename, bitrate, length, samplerate, isVBR, title, album, artist, albumArtist,
musicbrainzId, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/asf.py 0000644 0001750 0001750 00000003767 11456551413 022262 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an asf file """
from mutagen.asf import ASF
asfFile = ASF(filename)
length = int(round(asfFile.info.length))
bitrate = int(asfFile.info.bitrate)
samplerate = int(asfFile.info.sample_rate)
try: trackNumber = str(asfFile['WM/TrackNumber'][0])
except: trackNumber = None
try: discNumber = str(asfFile['WM/PartOfSet'][0])
except: discNumber = None
try: date = str(asfFile['WM/Year'][0])
except: date = None
try: title = str(asfFile['Title'][0])
except: title = None
try: album = str(asfFile['WM/AlbumTitle'][0])
except: album = None
try: artist = str(asfFile['Author'][0])
except: artist = None
try: albumArtist = str(asfFile['WM/AlbumArtist'][0])
except: albumArtist = None
try: genre = str(asfFile['WM/Genre'][0])
except: genre = None
try: musicbrainzId = str(asfFile['MusicBrainz/Track Id'][0])
except: musicbrainzId = None
return createFileTrack(filename, bitrate, length, samplerate, False, title, album, artist, albumArtist,
musicbrainzId, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/ogg.py 0000644 0001750 0001750 00000003770 11456551413 022257 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an Ogg Vorbis file """
from mutagen.oggvorbis import OggVorbis
oggFile = OggVorbis(filename)
length = int(round(oggFile.info.length))
bitrate = int(oggFile.info.bitrate)
samplerate = int(oggFile.info.sample_rate)
try: title = str(oggFile['title'][0])
except: title = None
try: album = str(oggFile['album'][0])
except: album = None
try: artist = str(oggFile['artist'][0])
except: artist = None
try: albumArtist = str(oggFile['albumartist'][0])
except: albumArtist = None
try: genre = str(oggFile['genre'][0])
except: genre = None
try: musicbrainzId = str(oggFile['musicbrainz_trackid'][0])
except: musicbrainzId = None
try: trackNumber = str(oggFile['tracknumber'][0])
except: trackNumber = None
try: discNumber = str(oggFile['discnumber'][0])
except: discNumber = None
try: date = str(oggFile['date'][0])
except: date = None
return createFileTrack(filename, bitrate, length, samplerate, True, title, album, artist, albumArtist,
musicbrainzId, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/mp4.py 0000644 0001750 0001750 00000003561 11456551413 022201 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an mp4 file """
from mutagen.mp4 import MP4
mp4File = MP4(filename)
length = int(round(mp4File.info.length))
bitrate = int(mp4File.info.bitrate)
samplerate = int(mp4File.info.sample_rate)
try: trackNumber = str(mp4File['trkn'][0][0])
except: trackNumber = None
try: discNumber = str(mp4File['disk'][0][0])
except: discNumber = None
try: date = str(mp4File['\xa9day'][0][0])
except: date = None
try: title = str(mp4File['\xa9nam'][0])
except: title = None
try: album = str(mp4File['\xa9alb'][0])
except: album = None
try: artist = str(mp4File['\xa9ART'][0])
except: artist = None
try: genre = str(mp4File['\xa9gen'][0])
except: genre = None
try: albumArtist = str(mp4File['aART'][0])
except: albumArtist = None
return createFileTrack(filename, bitrate, length, samplerate, False, title, album, artist, albumArtist,
None, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/monkeysaudio.py 0000644 0001750 0001750 00000003211 11456551413 024200 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an APE file """
from mutagen.monkeysaudio import MonkeysAudio
mFile = MonkeysAudio(filename)
length = int(round(mFile.info.length))
samplerate = int(mFile.info.sample_rate)
try: trackNumber = str(mFile['Track'][0])
except: trackNumber = None
try: date = str(mFile['Year'][0])
except: date = None
try: title = str(mFile['Title'][0])
except: title = None
try: album = str(mFile['Album'][0])
except: album = None
try: artist = str(mFile['Artist'][0])
except: artist = None
try: genre = str(mFile['Genre'][0])
except: genre = None
return createFileTrack(filename, -1, length, samplerate, False, title, album, artist, None,
None, genre, trackNumber, date, None)
./decibel-audio-player-1.06/src/media/format/mpc.py 0000644 0001750 0001750 00000003726 11456551413 022263 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from an mpc file """
from mutagen.musepack import Musepack
mpcFile = Musepack(filename)
length = int(round(mpcFile.info.length))
bitrate = int(mpcFile.info.bitrate * 1000)
samplerate = int(mpcFile.info.sample_rate)
try: trackNumber = str(mpcFile['Track'])
except: trackNumber = None
try: discNumber = str(mpcFile['Discnumber'])
except: discNumber = None
try: date = str(mpcFile['Year'])
except: date = None
try: title = str(mpcFile['Title'])
except: title = None
try: genre = str(mpcFile['Genre'])
except: genre = None
try: musicbrainzId = str(mpcFile['MUSICBRAINZ_TRACKID'])
except: musicbrainzId = None
try: album = str(mpcFile['Album'])
except: album = None
try: artist = str(mpcFile['Artist'])
except: artist = None
try: albumArtist = str(mpcFile['Album Artist'])
except: albumArtist = None
return createFileTrack(filename, bitrate, length, samplerate, False, title, album, artist, albumArtist,
musicbrainzId, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/media/format/flac.py 0000644 0001750 0001750 00000003677 11456551413 022416 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
from media.format import createFileTrack
def getTrack(filename):
""" Return a Track created from a FLAC file """
from mutagen.flac import FLAC
flacFile = FLAC(filename)
length = int(round(flacFile.info.length))
samplerate = int(flacFile.info.sample_rate)
try: title = str(flacFile['title'][0])
except: title = None
try: album = str(flacFile['album'][0])
except: album = None
try: artist = str(flacFile['artist'][0])
except: artist = None
try: albumArtist = str(flacFile['albumartist'][0])
except: albumArtist = None
try: genre = str(flacFile['genre'][0])
except: genre = None
try: musicbrainzId = str(flacFile['musicbrainz_trackid'][0])
except: musicbrainzId = None
try: trackNumber = str(flacFile['tracknumber'][0])
except: trackNumber = None
try: discNumber = str(flacFile['discnumber'][0])
except: discNumber = None
try: date = str(flacFile['date'][0])
except: date = None
return createFileTrack(filename, -1, length, samplerate, False, title, album, artist, albumArtist,
musicbrainzId, genre, trackNumber, date, discNumber)
./decibel-audio-player-1.06/src/gui/ 0000755 0001750 0001750 00000000000 11456551413 017337 5 ustar ingelres ingelres ./decibel-audio-player-1.06/src/gui/extListview.py 0000644 0001750 0001750 00000063157 11456551413 022254 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ExtListView v1.8
#
# v1.8:
# * Added an __iter__ method
# * Don't detach the model while inserting rows to avoid unwanted scrolling
#
# v1.7:
# * The 'extlistview-modified' signal was not generated when calling clear() and replaceContent()
# * Added selectRows(), removeRow(), and removeRows() method
# * Really fixed the intermittent improper columns resizing
# * Added the 'extlistview-selection-changed' signal
#
# v1.6:
# * Added a context menu to column headers allowing users to show/hide columns
# * Improved sorting a bit
#
# v1.5:
# * Fixed intermittent improper columns resizing
#
# v1.4:
# * Replaced TYPE_INT by TYPE_PYOBJECT as the fifth parameter type of extListview-dnd
# (see http://www.daa.com.au/pipermail/pygtk/2007-October/014311.html)
# * Prevent sorting rows when the list is empty
#
# v1.3:
# * Greatly improved speed when sorting a lot of rows
# * Added support for gtk.CellRendererToggle
# * Improved replaceContent() method
# * Added a call to set_cursor() when removing selected row(s)
# * Added getFirstSelectedRow(), appendRows(), addColumnAttribute(), unselectAll() and selectAll() methods
# * Set expand to False when calling pack_start()
#
# v1.2:
# * Fixed D'n'D reordering bugs
# * Improved code for testing the state of the keys for mouse clicks
# * Added quite a few new methods (replaceContent, hasMarkAbove, hasMarkUnder, __len__, iterSelectedRows, iterAllRows)
#
# v1.1:
# * Added a call to set_cursor() when unselecting all rows upon clicking on the empty area
# * Sort indicators are now displayed whenever needed
import gtk, random
from gtk import gdk
from gobject import signal_new, TYPE_INT, TYPE_STRING, TYPE_BOOLEAN, TYPE_PYOBJECT, TYPE_NONE, SIGNAL_RUN_LAST
# Internal d'n'd (reordering)
DND_REORDERING_ID = 1024
DND_INTERNAL_TARGET = ('extListview-internal', gtk.TARGET_SAME_WIDGET, DND_REORDERING_ID)
# Custom signals
signal_new('extlistview-dnd', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.DragContext, TYPE_INT, TYPE_INT, gtk.SelectionData, TYPE_INT, TYPE_PYOBJECT))
signal_new('extlistview-modified', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, ())
signal_new('extlistview-button-pressed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.Event, TYPE_PYOBJECT))
signal_new('extlistview-column-visibility-changed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (TYPE_STRING, TYPE_BOOLEAN))
signal_new('button-press-event', gtk.TreeViewColumn, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.Event, ))
signal_new('extlistview-selection-changed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (TYPE_PYOBJECT, ))
class ExtListViewColumn(gtk.TreeViewColumn):
"""
TreeViewColumn does not signal right-click events, and we need them
This subclass is equivalent to TreeViewColumn, but it signals these events
Most of the code in this class comes from Quod Libet (http://www.sacredchao.net/quodlibet)
"""
def __init__(self, title=None, cell_renderer=None, **args):
""" Constructor, see gtk.TreeViewColumn """
gtk.TreeViewColumn.__init__(self, title, cell_renderer, **args)
label = gtk.Label(title)
self.set_widget(label)
label.show()
label.__realize = label.connect('realize', self.onRealize)
def onRealize(self, widget):
widget.disconnect(widget.__realize)
del widget.__realize
button = widget.get_ancestor(gtk.Button)
if button is not None:
button.connect('button-press-event', self.onButtonPressed)
def onButtonPressed(self, widget, event):
self.emit('button-press-event', event)
class ExtListView(gtk.TreeView):
def __init__(self, columns, sortable=True, dndTargets=[], useMarkup=False, canShowHideColumns=True):
"""
If sortable is True, the user can click on headers to sort the contents of the list
The d'n'd targets are the targets accepted by the list (e.g., [('text/uri-list', 0, 0)])
Note that for the latter, the identifier 1024 must not be used (internally used for reordering)
If useMarkup is True, the 'markup' attributes is used instead of 'text' for CellRendererTexts
"""
gtk.TreeView.__init__(self)
self.selection = self.get_selection()
# Sorting rows
self.sortLastCol = None # The last column used for sorting (needed to switch between ascending/descending)
self.sortAscending = True # Ascending or descending order
self.sortColCriteria = {} # For each column, store the tuple of indexes used to sort the rows
# Default configuration for this list
self.set_rules_hint(True)
self.set_headers_visible(True)
self.selection.set_mode(gtk.SELECTION_MULTIPLE)
# Create the columns
nbEntries = 0
dataTypes = []
for (title, renderers, sortIndexes, expandable, visible) in columns:
if title is None:
nbEntries += len(renderers)
dataTypes += [renderer[1] for renderer in renderers]
else:
column = ExtListViewColumn(title)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
column.set_expand(expandable)
column.set_visible(visible)
if canShowHideColumns:
column.connect('button-press-event', self.onColumnHeaderClicked)
self.append_column(column)
if sortable:
column.set_clickable(True)
column.connect('clicked', self.__sortRows)
self.sortColCriteria[column] = sortIndexes
for (renderer, type) in renderers:
nbEntries += 1
dataTypes.append(type)
column.pack_start(renderer, False)
if isinstance(renderer, gtk.CellRendererToggle): column.add_attribute(renderer, 'active', nbEntries-1)
elif isinstance(renderer, gtk.CellRendererPixbuf): column.add_attribute(renderer, 'pixbuf', nbEntries-1)
elif isinstance(renderer, gtk.CellRendererText):
if useMarkup: column.add_attribute(renderer, 'markup', nbEntries-1)
else: column.add_attribute(renderer, 'text', nbEntries-1)
# Mark management
self.markedRow = None
self.markColumn = len(dataTypes)
dataTypes.append(TYPE_BOOLEAN) # When there's no other solution, this additional entry helps in finding the marked row
# Create the ListStore associated with this tree
self.store = gtk.ListStore(*dataTypes)
self.set_model(self.store)
# Drag'n'drop management
self.dndContext = None
self.dndTargets = dndTargets
self.motionEvtId = None
self.dndStartPos = None
self.dndReordering = False
if len(dndTargets) != 0:
self.enable_model_drag_dest(dndTargets, gdk.ACTION_DEFAULT)
# TreeView events
self.connect('drag-begin', self.onDragBegin)
self.connect('drag-motion', self.onDragMotion)
self.connect('button-press-event', self.onButtonPressed)
self.connect('drag-data-received', self.onDragDataReceived)
self.connect('button-release-event', self.onButtonReleased)
# Selection events
self.selection.connect('changed', self.onSelectionChanged)
# Show the list
self.show()
# --== Miscellaneous ==--
def __getIterOnSelectedRows(self):
""" Return a list of iterators pointing to the selected rows """
return [self.store.get_iter(path) for path in self.selection.get_selected_rows()[1]]
def __resizeColumns(self):
""" That's the only way I could find to make sure columns are correctly resized (e.g., columns_autosize() has no effect) """
for column in self.get_columns():
column.queue_resize()
def addColumnAttribute(self, colIndex, renderer, attribute, value):
""" Add a new attribute to the given column """
self.get_column(colIndex).add_attribute(renderer, attribute, value)
# --== Mark management ==--
def hasMark(self):
""" True if a mark has been set """
return self.markedRow is not None
def hasMarkAbove(self, index):
""" True if a mark is set and is above the given index """
return self.markedRow is not None and self.markedRow > index
def hasMarkUnder(self, index):
""" True if a mark is set and is undex the given index """
return self.markedRow is not None and self.markedRow < index
def clearMark(self):
""" Remove the mark """
if self.markedRow is not None:
self.setItem(self.markedRow, self.markColumn, False)
self.markedRow = None
def getMark(self):
""" Return the index of the marked row """
return self.markedRow
def setMark(self, rowIndex):
""" Put the mark on the given row, it will move with the row itself (e.g., D'n'D) """
self.clearMark()
self.markedRow = rowIndex
self.setItem(rowIndex, self.markColumn, True)
def __findMark(self):
""" Linear search for the marked row -- To be used only when there's no other solution """
iter = self.store.get_iter_first()
while iter is not None:
if self.store.get_value(iter, self.markColumn) == True:
self.markedRow = self.store.get_path(iter)[0]
break
iter = self.store.iter_next(iter)
# --== Sorting content ==--
def __resetSorting(self):
""" Reset sorting such that the next column click will result in an ascending sorting """
if self.sortLastCol is not None:
self.sortLastCol.set_sort_indicator(False)
self.sortLastCol = None
def __cmpRows(self, row1, row2, criteria, ascending):
""" Compare two rows based on the given criteria, the latter being a tuple of the indexes to use for the comparison """
# Sorting on the first criterion may be done either ascending or descending
criterion = criteria[0]
result = cmp(row1[criterion], row2[criterion])
if result != 0:
if ascending: return result
else: return -result
# For subsequent criteria, the order is always ascending
for criterion in criteria[1:]:
result = cmp(row1[criterion], row2[criterion])
if result != 0:
return result
return 0
def __sortRows(self, column):
""" Sort the rows """
if len(self.store) == 0:
return
if self.sortLastCol is not None:
self.sortLastCol.set_sort_indicator(False)
# Find how sorting must be done
if self.sortLastCol == column:
self.sortAscending = not self.sortAscending
else:
self.sortLastCol = column
self.sortAscending = True
# Dump the rows, sort them, and reorder the list
rows = [tuple(r) + (i,) for i, r in enumerate(self.store)]
criteria = self.sortColCriteria[column]
rows.sort(lambda r1, r2: self.__cmpRows(r1, r2, criteria, self.sortAscending))
self.store.reorder([r[-1] for r in rows])
# Move the mark if needed
if self.markedRow is not None:
self.__findMark()
column.set_sort_indicator(True)
if self.sortAscending: column.set_sort_order(gtk.SORT_ASCENDING)
else: column.set_sort_order(gtk.SORT_DESCENDING)
self.emit('extlistview-modified')
# --== Selection ==--
def unselectAll(self):
""" Unselect all rows """
self.selection.unselect_all()
def selectAll(self):
""" Select all rows """
self.selection.select_all()
def selectRows(self, paths):
""" Select the given rows """
self.unselectAll()
for path in paths:
self.selection.select_path(path)
def getSelectedRowsCount(self):
""" Return how many rows are currently selected """
return self.selection.count_selected_rows()
def getSelectedRows(self):
""" Return all selected row(s) """
return [tuple(self.store[path])[:-1] for path in self.selection.get_selected_rows()[1]]
def getFirstSelectedRow(self):
""" Return only the first selected row """
return tuple(self.store[self.selection.get_selected_rows()[1][0]])[:-1]
def getFirstSelectedRowIndex(self):
""" Return the index of the first selected row """
return self.selection.get_selected_rows()[1][0][0]
def iterSelectedRows(self):
""" Iterate on all selected row(s) """
for path in self.selection.get_selected_rows()[1]:
yield tuple(self.store[path])[:-1]
# --== Retrieving content / Iterating on content ==--
def __len__(self):
""" Return how many rows are stored in the list """
return len(self.store)
def getCount(self):
""" Return how many rows are stored in the list """
return len(self.store)
def __iter__(self):
""" Iterate on all rows """
for row in self.store:
yield tuple(row)[:-1]
def iterAllRows(self):
""" Iterate on all rows """
for row in self.store:
yield tuple(row)[:-1]
def getRow(self, rowIndex):
""" Return the given row """
return tuple(self.store[rowIndex])[:-1]
def getAllRows(self):
""" Return all rows """
return [tuple(row)[:-1] for row in self.store]
def getItem(self, rowIndex, colIndex):
""" Return the value of the given item """
return self.store.get_value(self.store.get_iter(rowIndex), colIndex)
# --== Adding/removing/modifying content ==--
def clear(self):
""" Remove all rows from the list """
self.__resetSorting()
self.clearMark()
self.store.clear()
self.__resizeColumns()
self.emit('extlistview-modified')
def setItem(self, rowIndex, colIndex, value):
""" Change the value of the given item """
# Check if changing that item may change the sorting: if so, reset sorting
if self.sortLastCol is not None and colIndex in self.sortColCriteria[self.sortLastCol]:
self.__resetSorting()
self.store.set_value(self.store.get_iter(rowIndex), colIndex, value)
def removeRows(self, paths):
""" Remove the given rows """
self.freeze_child_notify()
# We must work with iters because paths become meaningless once we start removing rows
for iter in [self.store.get_iter(path) for path in paths]:
path = self.store.get_path(iter)[0]
# Move the mark if needed
if self.markedRow is not None:
if path < self.markedRow: self.markedRow -= 1
elif path == self.markedRow: self.markedRow = None
# Remove the current row
if self.store.remove(self.store.get_iter(path)): self.set_cursor(path)
elif len(self.store) != 0: self.set_cursor(len(self.store)-1)
self.thaw_child_notify()
if len(self.store) == 0:
self.set_cursor(0)
self.__resetSorting()
self.__resizeColumns()
self.emit('extlistview-modified')
def removeRow(self, path):
""" Remove the given row """
self.removeRows((path, ))
def removeSelectedRows(self):
""" Remove the selected row(s) """
self.removeRows(self.selection.get_selected_rows()[1])
def cropSelectedRows(self):
""" Remove all rows but the selected ones """
pathsList = self.selection.get_selected_rows()[1]
self.freeze_child_notify()
self.selection.select_all()
for path in pathsList:
self.selection.unselect_path(path)
self.removeSelectedRows()
self.selection.select_all()
self.thaw_child_notify()
def insertRows(self, rows, position=None):
""" Insert or append (if position is None) some rows to the list """
if len(rows) == 0:
return
# Insert the additional column used for the mark management
if type(rows[0]) is tuple: rows[:] = [row + (False,) for row in rows]
else: rows[:] = [row + [False] for row in rows]
# Move the mark if needed
if self.markedRow is not None and position is not None and position <= self.markedRow:
self.markedRow += len(rows)
# Insert rows
self.freeze_child_notify()
if position is None:
for row in rows:
self.store.append(row)
else:
for row in rows:
self.store.insert(position, row)
position += 1
self.thaw_child_notify()
self.__resetSorting()
self.emit('extlistview-modified')
def appendRows(self, rows):
""" Helper function, equivalent to insertRows(rows, None) """
self.insertRows(rows, None)
def replaceContent(self, rows):
""" Replace the content of the list with the given rows """
self.freeze_child_notify()
self.set_model(None)
self.clear()
self.appendRows(rows)
self.set_model(self.store)
self.thaw_child_notify()
def shuffle(self):
""" Shuffle the content of the list """
order = range(len(self.store))
random.shuffle(order)
self.store.reorder(order)
# Move the mark if needed
if self.markedRow is not None:
self.__findMark()
self.__resetSorting()
self.emit('extlistview-modified')
# --== D'n'D management ==--
def enableDNDReordering(self):
""" Enable the use of Drag'n'Drop to reorder the list """
self.dndReordering = True
self.dndTargets.append(DND_INTERNAL_TARGET)
self.enable_model_drag_dest(self.dndTargets, gdk.ACTION_DEFAULT)
def __isDropAfter(self, pos):
""" Helper function, True if pos is gtk.TREE_VIEW_DROP_AFTER or gtk.TREE_VIEW_DROP_INTO_OR_AFTER """
return pos == gtk.TREE_VIEW_DROP_AFTER or pos == gtk.TREE_VIEW_DROP_INTO_OR_AFTER
def __moveSelectedRows(self, x, y):
""" Internal function used for drag'n'drop """
iterList = self.__getIterOnSelectedRows()
dropInfo = self.get_dest_row_at_pos(int(x), int(y))
if dropInfo is None:
pos, path = gtk.TREE_VIEW_DROP_INTO_OR_AFTER, len(self.store) - 1
else:
pos, path = dropInfo[1], dropInfo[0][0]
if self.__isDropAfter(pos) and path < len(self.store)-1:
pos = gtk.TREE_VIEW_DROP_INTO_OR_BEFORE
path += 1
self.freeze_child_notify()
for srcIter in iterList:
srcPath = self.store.get_path(srcIter)[0]
if self.__isDropAfter(pos):
dstIter = self.store.insert_after(self.store.get_iter(path), self.store[srcIter])
else:
dstIter = self.store.insert_before(self.store.get_iter(path), self.store[srcIter])
if path == srcPath:
path += 1
self.store.remove(srcIter)
dstPath = self.store.get_path(dstIter)[0]
if srcPath > dstPath:
path += 1
if self.markedRow is not None:
if srcPath == self.markedRow: self.markedRow = dstPath
elif srcPath < self.markedRow and dstPath >= self.markedRow: self.markedRow -= 1
elif srcPath > self.markedRow and dstPath <= self.markedRow: self.markedRow += 1
self.thaw_child_notify()
self.__resetSorting()
self.emit('extlistview-modified')
# --== GTK Handlers ==--
def onButtonPressed(self, tree, event):
""" A mouse button has been pressed """
retVal = False
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
if pathInfo is None: path = None
else: path = pathInfo[0]
if event.button == 1 or event.button == 3:
if path is None:
self.selection.unselect_all()
if len(self.store) != 0:
tree.set_cursor(len(self.store))
else:
if self.dndReordering and self.motionEvtId is None and event.button == 1:
self.dndStartPos = (int(event.x), int(event.y))
self.motionEvtId = gtk.TreeView.connect(self, 'motion-notify-event', self.onMouseMotion)
stateClear = not (event.state & (gdk.SHIFT_MASK | gdk.CONTROL_MASK))
if stateClear and not self.selection.path_is_selected(path):
# We block the 'changed' signal here because it's emitted by the default GTK handler
# And we don't need to emit it multiple times because of what we're doing here
self.selection.handler_block_by_func(self.onSelectionChanged)
self.selection.unselect_all()
self.selection.select_path(path)
self.selection.handler_unblock_by_func(self.onSelectionChanged)
else:
retVal = (stateClear and self.getSelectedRowsCount() > 1 and self.selection.path_is_selected(path))
self.emit('extlistview-button-pressed', event, path)
return retVal
def onButtonReleased(self, tree, event):
""" A mouse button has been released """
if self.motionEvtId is not None:
self.disconnect(self.motionEvtId)
self.dndContext = None
self.motionEvtId = None
if len(self.dndTargets) != 0:
self.enable_model_drag_dest(self.dndTargets, gdk.ACTION_DEFAULT)
stateClear = not (event.state & (gdk.SHIFT_MASK | gdk.CONTROL_MASK))
if stateClear and event.state & gdk.BUTTON1_MASK and self.getSelectedRowsCount() > 1:
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
if pathInfo is not None:
self.selection.unselect_all()
self.selection.select_path(pathInfo[0][0])
def onMouseMotion(self, tree, event):
""" The mouse has been moved """
if self.dndContext is None and self.drag_check_threshold(self.dndStartPos[0], self.dndStartPos[1], int(event.x), int(event.y)):
self.dndContext = self.drag_begin([DND_INTERNAL_TARGET], gdk.ACTION_COPY, 1, event)
def onDragBegin(self, tree, context):
""" A drag'n'drop operation has begun """
if self.getSelectedRowsCount() == 1: context.set_icon_stock(gtk.STOCK_DND, 0, 0)
else: context.set_icon_stock(gtk.STOCK_DND_MULTIPLE, 0, 0)
def onDragDataReceived(self, tree, context, x, y, selection, dndId, time):
""" Some data has been dropped into the list """
if dndId == DND_REORDERING_ID: self.__moveSelectedRows(x, y)
else: self.emit('extlistview-dnd', context, int(x), int(y), selection, dndId, time)
def onDragMotion(self, tree, context, x, y, time):
""" Prevent rows from being dragged *into* other rows (this is a list, not a tree) """
drop = self.get_dest_row_at_pos(int(x), int(y))
if drop is not None and (drop[1] == gtk.TREE_VIEW_DROP_INTO_OR_AFTER or drop[1] == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
self.enable_model_drag_dest([('invalid-position', 0, -1)], gdk.ACTION_DEFAULT)
else:
self.enable_model_drag_dest(self.dndTargets, gdk.ACTION_DEFAULT)
def onColumnHeaderClicked(self, column, event):
""" A column header has been clicked """
if event.button == 3:
# Create a menu with a CheckMenuItem per column
menu = gtk.Menu()
nbVisibleItems = 0
lastVisibleItem = None
for column in self.get_columns():
item = gtk.CheckMenuItem(column.get_title())
item.set_active(column.get_visible())
item.connect('toggled', self.onShowHideColumn, column)
item.show()
menu.append(item)
# Count how many columns are visible
if item.get_active():
nbVisibleItems += 1
lastVisibleItem = item
# Don't allow the user to hide the only visible column left
if nbVisibleItems == 1:
lastVisibleItem.set_sensitive(False)
menu.popup(None, None, None, event.button, event.get_time())
def onShowHideColumn(self, menuItem, column):
""" Switch the visibility of the given column """
column.set_visible(not column.get_visible())
self.__resizeColumns()
self.emit('extlistview-column-visibility-changed', column.get_title(), column.get_visible())
def onSelectionChanged(self, selection):
""" The selection has changed """
self.emit('extlistview-selection-changed', self.getSelectedRows())
./decibel-audio-player-1.06/src/gui/__init__.py 0000644 0001750 0001750 00000003126 11456551413 021452 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, tools
def __msgBox(parent, type, buttons, header, text):
""" Show a generic message box """
dlg = gtk.MessageDialog(parent, gtk.DIALOG_MODAL, type, buttons, header)
dlg.set_title(tools.consts.appName)
if text is None: dlg.set_markup(header)
else: dlg.format_secondary_markup(text)
response = dlg.run()
dlg.destroy()
return response
# Functions used to display various message boxes
def infoMsgBox( parent, header, text=None): __msgBox(parent, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, header, text)
def errorMsgBox( parent, header, text=None): __msgBox(parent, gtk.MESSAGE_ERROR, gtk.BUTTONS_OK, header, text)
def questionMsgBox(parent, header, text=None): return __msgBox(parent, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, header, text)
./decibel-audio-player-1.06/src/gui/authentication.py 0000644 0001750 0001750 00000013674 11456551413 022743 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, gui, tools
from tools import consts, prefs
from base64 import b64encode, b64decode
from gettext import gettext as _
# The dialog used for authentication
mBtnOk = None
mAuthDlg = None
mChkStore = None
mTxtLogin = None
mTxtPasswd = None
__keyring = None
def __loadKeyring():
""" Load the keyring if needed """
global __keyring
import gnomekeyring as gk
if __keyring is None:
__keyring = gk.get_default_keyring_sync()
if __keyring is None:
__keyring = 'login'
def __loadAuthInfo(id):
""" Load the login/password associated with id, either from the Gnome keyring or from the prefs """
try:
import gnomekeyring as gk
useGK = True
except:
useGK = False
# No Gnome keyring
if not useGK:
login = prefs.get(__name__, id + '_login', None)
passwd = prefs.get(__name__, id + '_passwd', None)
if login is not None and passwd is not None: return (login, b64decode(passwd))
else: return None
# From here we can use the Gnome keyring
__loadKeyring()
try: gk.create_sync(__keyring, None)
except gk.AlreadyExistsError: pass
token = prefs.get(__name__, id + '_gkToken', None)
if token is not None:
try:
login, passwd = gk.item_get_info_sync(__keyring, token).get_secret().split('\n')
return (login, passwd)
except:
pass
return None
def __storeAuthInfo(id, login, passwd):
""" Store the login/password associated with id, either in the Gnome keyring or in the prefs """
try:
import gnomekeyring as gk
useGK = True
except:
useGK = False
# No Gnome keyring
if not useGK:
prefs.set(__name__, id + '_login', login)
prefs.set(__name__, id + '_passwd', b64encode(passwd)) # Pretty useless, but the user prefers not to see his password as clear text
return
# From here we can use the Gnome keyring
__loadKeyring()
try:
label = '%s (%s)' % (consts.appName, id)
authInfo = '\n'.join((login, passwd))
token = gk.item_create_sync(__keyring, gk.ITEM_GENERIC_SECRET, label, {'appName': consts.appName, 'id': id}, authInfo, True)
prefs.set(__name__, id + '_gkToken', token)
except:
pass
def getAuthInfo(id, reason, defaultLogin=None, force=False, parent=None):
"""
The parameter id may be any arbitrary string, but it must be unique as it identifies the login information given by the user.
If a {login/password} is already known for this identifier, it is immediately returned without asking anything to the user.
If no login is currently known, ask the user for the authentication information.
"""
global mBtnOk, mAuthDlg, mChkStore, mTxtLogin, mTxtPasswd
if not force:
authInfo = __loadAuthInfo(id)
if authInfo is not None:
return authInfo
if mAuthDlg is None:
wTree = tools.loadGladeFile('Authentication.glade')
mBtnOk = wTree.get_widget('btn-ok')
mAuthDlg = wTree.get_widget('dlg-main')
mChkStore = wTree.get_widget('chk-store')
mTxtLogin = wTree.get_widget('txt-login')
mTxtPasswd = wTree.get_widget('txt-passwd')
wTree.get_widget('lbl-reason').set_text(_('Enter your username and password for\n%(reason)s') % {'reason': reason})
wTree.get_widget('dlg-action_area').set_child_secondary(wTree.get_widget('btn-help'), True) # Glade fails to do that
wTree.get_widget('lbl-title').set_markup('%s' % _('Password required'))
mAuthDlg.set_title(consts.appName)
mAuthDlg.resize_children()
mAuthDlg.connect('response', onResponse)
mTxtLogin.connect('changed', lambda entry: mBtnOk.set_sensitive(mTxtLogin.get_text() != '' and mTxtPasswd.get_text() != ''))
mTxtPasswd.connect('changed', lambda entry: mBtnOk.set_sensitive(mTxtLogin.get_text() != '' and mTxtPasswd.get_text() != ''))
if defaultLogin is not None:
mTxtLogin.set_text(defaultLogin)
mTxtPasswd.set_text('')
mTxtPasswd.grab_focus()
else:
mTxtLogin.set_text('')
mTxtPasswd.set_text('')
mTxtLogin.grab_focus()
mBtnOk.set_sensitive(False)
mAuthDlg.show_all()
respId = mAuthDlg.run()
mAuthDlg.hide()
if respId == gtk.RESPONSE_OK:
login = mTxtLogin.get_text()
passwd = mTxtPasswd.get_text()
if mChkStore.get_active():
__storeAuthInfo(id, login, passwd)
return (login, passwd)
return None
def onResponse(dlg, respId):
""" One of the button in the dialog box has been clicked """
if respId == gtk.RESPONSE_HELP:
primary = _('About password storage safety')
secondary = '%s\n\n%s' % (_('If you use Gnome, it is safe to store your password since the Gnome keyring is used.'),
_('If you do not use Gnome, beware that, although not stored as clear text, an attacker could retrieve it.'))
gui.infoMsgBox(dlg, primary, secondary)
dlg.stop_emission('response')
./decibel-audio-player-1.06/src/gui/progressDlg.py 0000644 0001750 0001750 00000006240 11456551413 022206 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import tools
class CancelledException(Exception):
pass
class ProgressDlg:
""" Display a dialog with a progress bar """
def __init__(self, parent, primaryText, secondaryText):
""" Contructor """
self.parent = parent
self.cancelled = False
# Widgets
self.wTree = tools.loadGladeFile('Progress.glade')
self.dialog = self.wTree.get_widget('dlg')
self.lblCurrent = self.wTree.get_widget('lbl-current')
self.progressBar = self.wTree.get_widget('progress-bar')
# GTK+ handlers
self.wTree.get_widget('btn-cancel').connect('clicked', self.onCancel)
# Configure and show the progress dialog
if parent is not None:
parent.set_sensitive(False)
self.dialog.set_transient_for(parent)
self.setPrimaryText(primaryText)
self.setSecondaryText(secondaryText)
self.dialog.set_title(tools.consts.appName)
self.dialog.set_deletable(False)
self.dialog.show_all()
def destroy(self):
""" Destroy the progress dialog """
if self.parent is not None:
self.parent.set_sensitive(True)
self.dialog.hide()
self.dialog.destroy()
def pulse(self, txt=None):
"""
Pulse the progress bar
If txt is not None, change the current action to that value
Raise CancelledException if the user has clicked on the cancel button
"""
if self.cancelled:
raise CancelledException()
if txt is not None:
self.lblCurrent.set_markup('%s' % txt)
self.progressBar.pulse()
def setPrimaryText(self, txt):
""" Set the primary text for this progress dialog """
self.wTree.get_widget('lbl-primary').set_markup('%s' % txt)
def setSecondaryText(self, txt):
""" Set the secondary text for this progress dialog """
self.wTree.get_widget('lbl-secondary').set_label(txt)
def setCancellable(self, cancellable):
""" Enable/disable the cancel button """
self.wTree.get_widget('btn-cancel').set_sensitive(cancellable)
def onCancel(self, btn):
""" The cancel button has been clicked """
self.cancelled = True
def hasBeenCancelled(self):
""" Return True if the user has clicked on the cancel button """
return self.cancelled
./decibel-audio-player-1.06/src/gui/window.py 0000644 0001750 0001750 00000006305 11456551413 021224 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, tools
class Window(gtk.Window):
"""
Add some functionalities to gtk.Window:
* Automatically save and restore size
* Hide the window instead of destroying it
* Add a isVisible() function
* Add a getWidget() function that acts like get_widget() (in gtk.glade)
"""
def __init__(self, resFile, container, modName, title, defaultWidth, defaultHeight):
""" Constructor """
gtk.Window.__init__(self)
# Load only the top-level container of the given glade file
self.wTree = tools.loadGladeFile(resFile, container)
self.visible = False
self.modName = modName
# Configure the window
self.set_title(title)
self.add(self.wTree.get_widget(container))
if tools.prefs.get(modName, 'win-is-maximized', False):
self.maximize()
self.resize(tools.prefs.get(modName, 'win-width', defaultWidth), tools.prefs.get(modName, 'win-height', defaultHeight))
self.set_position(gtk.WIN_POS_CENTER)
# Connect GTK handlers
self.connect('delete-event', self.onDelete)
self.connect('size-allocate', self.onResize)
self.connect('window-state-event', self.onState)
def getWidget(self, name):
""" Return the widget with the given name """
return self.wTree.get_widget(name)
def isVisible(self):
""" Return True if the window is currently visible """
return self.visible
def show(self):
""" Show the window if not visible, bring it to top otherwise """
self.visible = True
self.show_all()
self.present()
def hide(self):
""" Hide the window """
self.visible = False
gtk.Window.hide(self)
# --== GTK handlers ==--
def onResize(self, win, rect):
""" Save the new size of the dialog """
if win.window is not None and not win.window.get_state() & gtk.gdk.WINDOW_STATE_MAXIMIZED:
tools.prefs.set(self.modName, 'win-width', rect.width)
tools.prefs.set(self.modName, 'win-height', rect.height)
def onState(self, win, evt):
""" Save the new state of the dialog """
tools.prefs.set(self.modName, 'win-is-maximized', bool(evt.new_window_state & gtk.gdk.WINDOW_STATE_MAXIMIZED))
def onDelete(self, win, evt):
""" Hide the window instead of deleting it """
self.hide()
return True
./decibel-audio-player-1.06/src/gui/fileChooser.py 0000644 0001750 0001750 00000006723 11456551413 022163 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, tools
__currDir = tools.consts.dirBaseUsr
def openFile(parent, title):
""" Return the selected file, or None if cancelled """
global __currDir
btn = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)
dialog = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_OPEN, btn)
dialog.set_select_multiple(False)
dialog.set_current_folder(__currDir)
file = None
if dialog.run() == gtk.RESPONSE_OK:
file = dialog.get_filename()
__currDir = dialog.get_current_folder()
dialog.destroy()
return file
def openFiles(parent, title, filterPatterns={}):
"""
Return a list of files, or None if cancelled
The format of filter must be {'Name1': ['filter1', 'filter2'], 'Name2': ['filter3'] ... }
"""
global __currDir
btn = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)
dialog = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_OPEN, btn)
dialog.set_select_multiple(True)
dialog.set_current_folder(__currDir)
# Add filters
for name, patterns in filterPatterns.iteritems():
filter = gtk.FileFilter()
filter.set_name(name)
map(filter.add_pattern, patterns)
dialog.add_filter(filter)
files = None
if dialog.run() == gtk.RESPONSE_OK:
files = dialog.get_filenames()
__currDir = dialog.get_current_folder()
dialog.destroy()
return files
def openDirectory(parent, title):
""" Return a directory, or None if cancelled """
global __currDir
btn = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK)
dialog = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, btn)
dialog.set_select_multiple(False)
dialog.set_current_folder(__currDir)
directory = None
if dialog.run() == gtk.RESPONSE_OK:
directory = dialog.get_filename()
__currDir = dialog.get_current_folder()
dialog.destroy()
return directory
def save(parent, title, defaultFile, defaultDir=None):
""" Return a filename, or None if cancelled """
global __currDir
btn = (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)
dialog = gtk.FileChooserDialog(title, parent, gtk.FILE_CHOOSER_ACTION_SAVE, btn)
dialog.set_current_name(defaultFile)
dialog.set_do_overwrite_confirmation(True)
if defaultDir is None: dialog.set_current_folder(__currDir)
else: dialog.set_current_folder(defaultDir)
file = None
if dialog.run() == gtk.RESPONSE_OK:
file = dialog.get_filename()
__currDir = dialog.get_current_folder()
dialog.destroy()
return file
./decibel-audio-player-1.06/src/gui/help.py 0000644 0001750 0001750 00000004200 11456551413 020635 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import pango, tools
mDlg = None
mTxtBuffer = None
class HelpDlg:
""" Show a help dialog box """
def __init__(self, title):
""" Constructor """
global mDlg, mTxtBuffer
if mDlg is None:
wTree = tools.loadGladeFile('HelpDlg.glade')
mDlg = wTree.get_widget('dlg-main')
mTxtBuffer = wTree.get_widget('txt-help').get_buffer()
mDlg.set_title(tools.consts.appName)
mTxtBuffer.create_tag('title', weight=pango.WEIGHT_BOLD, scale=pango.SCALE_X_LARGE)
mTxtBuffer.create_tag('section', weight=pango.WEIGHT_BOLD, scale=pango.SCALE_LARGE)
self.nbSections = 0
mTxtBuffer.set_text('')
mTxtBuffer.insert_with_tags_by_name(mTxtBuffer.get_end_iter(), title + '\n', 'title')
def addSection(self, title, content):
""" Create a new section with the given title and content """
self.nbSections += 1
mTxtBuffer.insert(mTxtBuffer.get_end_iter(), '\n\n')
mTxtBuffer.insert_with_tags_by_name(mTxtBuffer.get_end_iter(), '%u. %s' % (self.nbSections, title), 'section')
mTxtBuffer.insert(mTxtBuffer.get_end_iter(), '\n\n%s' % content)
def show(self, parent):
""" Show the help dialog box """
mDlg.set_transient_for(parent)
mDlg.show_all()
mDlg.run()
mDlg.hide()
./decibel-audio-player-1.06/src/gui/preferences.py 0000644 0001750 0001750 00000021464 11456551413 022221 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, gui, modules, tools
from tools import consts, icons
from gettext import gettext as _
(
ROW_ENABLED, # True if the module is currently enabled
ROW_TEXT, # Name and description of the module
ROW_ICON, # An icon indicating whether the module is configurable
ROW_UNLOADABLE, # True if the module can be disabled
ROW_INSTANCE, # Instance of the module, if any
ROW_MODINFO # Information exported by a module
) = range(6)
class Preferences:
""" Allow the user to load/unload/configure modules """
def __init__(self):
""" Constructor """
import gobject
from gui import extListview, window
self.window = window.Window('Preferences.glade', 'vbox1', __name__, _('Preferences'), 495, 440)
self.currCat = consts.MODCAT_NONE
# List of modules
textRdr = gtk.CellRendererText()
toggleRdr = gtk.CellRendererToggle()
columns = (('', [(toggleRdr, gobject.TYPE_BOOLEAN)], ROW_ENABLED, False, True),
('', [(textRdr, gobject.TYPE_STRING)], ROW_TEXT, True, True),
('', [(gtk.CellRendererPixbuf(), gtk.gdk.Pixbuf)], ROW_ICON, False, True),
(None, [(None, gobject.TYPE_BOOLEAN)], ROW_UNLOADABLE, False, False),
(None, [(None, gobject.TYPE_PYOBJECT)], ROW_INSTANCE, False, False),
(None, [(None, gobject.TYPE_PYOBJECT)], ROW_MODINFO, False, False))
self.list = extListview.ExtListView(columns, sortable=False, useMarkup=True, canShowHideColumns=False)
self.list.set_headers_visible(False)
self.list.addColumnAttribute(0, toggleRdr, 'activatable', ROW_UNLOADABLE)
toggleRdr.connect('toggled', self.onModuleToggled)
self.window.getWidget('scrolledwindow1').add(self.list)
self.fillList()
self.list.get_column(1).set_cell_data_func(textRdr, self.__fmtColumnColor)
# Categories
self.iconview = self.window.getWidget('iconview')
# Create the store for the iconview
self.iconviewStore = gtk.ListStore(gobject.TYPE_STRING, gtk.gdk.Pixbuf, gobject.TYPE_INT)
categories = [
(_('Decibel'), icons.catDecibelIcon(), consts.MODCAT_DECIBEL),
(_('Desktop'), icons.catDesktopIcon(), consts.MODCAT_DESKTOP),
(_('Internet'), icons.catInternetIcon(), consts.MODCAT_INTERNET),
(_('Explorer'), icons.catExplorerIcon(), consts.MODCAT_EXPLORER),
]
for category in sorted(categories):
self.iconviewStore.append([category[0], category[1], category[2]])
self.iconview.set_model(self.iconviewStore)
self.iconview.set_text_column(0)
self.iconview.set_pixbuf_column(1)
self.iconview.set_size_request(84, -1)
# GTK handlers
self.iconview.connect('selection-changed', self.onCategoryChanged)
self.window.getWidget('btn-help').connect('clicked', self.onHelp)
self.list.connect('extlistview-button-pressed', self.onButtonPressed)
self.list.get_selection().connect('changed', self.onSelectionChanged)
self.window.getWidget('btn-prefs').connect('clicked', self.onPreferences)
self.window.getWidget('btn-close').connect('clicked', lambda btn: self.window.hide())
def __fmtColumnColor(self, col, cll, mdl, it):
""" Grey out module that are not enabled """
style = self.list.get_style()
enabled = mdl.get_value(it, ROW_ENABLED)
if enabled: cll.set_property('foreground-gdk', style.text[gtk.STATE_NORMAL])
else: cll.set_property('foreground-gdk', style.text[gtk.STATE_INSENSITIVE])
def show(self):
""" Show the dialog box """
if not self.window.isVisible():
self.list.unselectAll()
self.iconview.grab_focus()
self.iconview.select_path((0,))
self.window.getWidget('btn-prefs').set_sensitive(False)
self.window.show()
def fillList(self):
""" Fill the list of modules according to the currently selected category """
rows = []
for (name, data) in modules.getModules():
instance = data[modules.MOD_INSTANCE]
category = data[modules.MOD_INFO][modules.MODINFO_CATEGORY]
mandatory = data[modules.MOD_INFO][modules.MODINFO_MANDATORY]
configurable = data[modules.MOD_INFO][modules.MODINFO_CONFIGURABLE]
if (configurable or not mandatory) and category == self.currCat:
if configurable and instance is not None: icon = icons.prefsBtnIcon()
else: icon = None
text = '%s\n%s' % (tools.htmlEscape(_(name)), tools.htmlEscape(data[modules.MOD_INFO][modules.MODINFO_DESC]))
rows.append((instance is not None, text, icon, not mandatory, instance, data[modules.MOD_INFO]))
rows.sort(key=lambda row: row[ROW_TEXT])
self.list.replaceContent(rows)
# --== GTK handlers ==--
def onButtonPressed(self, list, event, path):
""" A mouse button has been pressed """
if event.button == 1 and event.type == gtk.gdk._2BUTTON_PRESS and path is not None:
# Double-clicking an enabled and configurable module opens the configuration dialog
row = self.list.getRow(path)
if row[ROW_ENABLED] and row[ROW_ICON] is not None:
row[ROW_INSTANCE].configure(self.window)
def onModuleToggled(self, renderer, path):
""" A module has been enabled/disabled """
row = self.list.getRow(path)
name = row[ROW_MODINFO][modules.MODINFO_NAME]
if row[ROW_ENABLED]:
modules.unload(name)
else:
try: modules.load(name)
except modules.LoadException, e: gui.errorMsgBox(self.window, _('Unable to load this module.'), str(e))
self.fillList()
def onCategoryChanged(self, iconview):
""" A new category has been selected """
selection = iconview.get_selected_items()
if len(selection) == 0: self.currCat = consts.MODCAT_NONE
else: self.currCat = self.iconviewStore[selection[0][0]][2]
self.fillList()
def onHelp(self, btn):
""" Show a small help message box """
from gui import help
helpDlg = help.HelpDlg(_('Modules'))
helpDlg.addSection(_('Description'),
_('This dialog box shows the list of available modules, which are small pieces of code that add '
'some functionnalities to the application. You can enable/disable a module by checking/unchecking '
'the check box in front of it. Note that some modules (e.g., the File Explorer) cannot be disabled.'))
helpDlg.addSection(_('Configuring a Module'),
_('When a module may be configured, a specific icon is displayed on the right of the corresponding line. '
'To configure a module, simply select it and then click on the "Preferences" button on the bottom of '
'the dialog box. Note that configuring a module is only possible when it is enabled.'))
helpDlg.show(self.window)
def onSelectionChanged(self, selection):
""" Decide whether the new selection may be configured """
sensitive = self.list.getSelectedRowsCount() == 1 and self.list.getFirstSelectedRow()[ROW_ICON] is not None
self.window.getWidget('btn-prefs').set_sensitive(sensitive)
def onPreferences(self, btn):
""" Configure the selected module """
self.list.getFirstSelectedRow()[ROW_INSTANCE].configure(self.window)
# --== Global functions ==--
__instance = None
def show():
""" Show the preferences dialog box """
global __instance
if __instance is None:
__instance = Preferences()
__instance.show()
./decibel-audio-player-1.06/src/gui/selectPath.py 0000644 0001750 0001750 00000010150 11456551413 022002 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import fileChooser, gtk, gui, os.path, tools
from gettext import gettext as _
class SelectPath:
def __init__(self, title, parent, forbiddenNames=[], forbiddenChars=[]):
""" Constructor """
wTree = tools.loadGladeFile('SelectPath.glade')
self.btnOk = wTree.get_widget('btn-ok')
self.dialog = wTree.get_widget('dlg')
self.txtName = wTree.get_widget('txt-name')
self.txtPath = wTree.get_widget('txt-path')
self.btnOpen = wTree.get_widget('btn-open')
self.forbiddenNames = forbiddenNames
self.forbiddenChars = forbiddenChars
self.dialog.set_title(title)
self.dialog.set_transient_for(parent)
# Handlers
self.btnOpen.connect('clicked', self.onBtnOpen)
self.txtName.connect('changed', self.onTxtFieldChanged)
self.txtPath.connect('changed', self.onTxtFieldChanged)
self.dialog.connect('response', self.onCheckDlgResponse)
def setNameSelectionEnabled(self, enabled):
""" Enable/disable path selection """
self.txtName.set_sensitive(enabled)
def setPathSelectionEnabled(self, enabled):
""" Enable/disable path selection """
self.txtPath.set_sensitive(enabled)
self.btnOpen.set_sensitive(enabled)
def run(self, defaultName='', defaultPath=''):
""" Return a tuple (name, path) or None if the user cancelled the dialog """
self.btnOk.set_sensitive(False)
self.txtName.set_text(defaultName)
self.txtPath.set_text(defaultPath)
self.txtName.grab_focus()
self.dialog.show_all()
result = None
if self.dialog.run() == gtk.RESPONSE_OK:
result = (self.txtName.get_text(), self.txtPath.get_text())
self.dialog.hide()
return result
# --== GTK handlers ==--
def onBtnOpen(self, btn):
""" Let the user choose a folder, and fill the corresponding field in the dialog """
path = fileChooser.openDirectory(self.dialog, _('Choose a folder'))
if path is not None:
self.txtPath.set_text(path)
def onTxtFieldChanged(self, txtEntry):
""" Enable/disable the OK button based on the content of the text fields """
self.btnOk.set_sensitive(self.txtName.get_text() != '' and self.txtPath.get_text() != '')
def onCheckDlgResponse(self, dialog, response, *args):
""" Prevent clicking on the OK button if values are not correct """
if response == gtk.RESPONSE_OK:
name = self.txtName.get_text()
path = self.txtPath.get_text()
if not os.path.isdir(path):
gui.errorMsgBox(dialog, _('This path does not exist'), _('Please select an existing directory.'))
dialog.stop_emission('response')
elif name in self.forbiddenNames:
gui.errorMsgBox(dialog, _('The name is incorrect'), _('This name is not allowed.\nPlease use another one.'))
dialog.stop_emission('response')
else:
for ch in name:
if ch in self.forbiddenChars:
gui.errorMsgBox(dialog, _('The name is incorrect'), _('The character %s is not allowed.\nPlease use another name.') % ch)
dialog.stop_emission('response')
break
./decibel-audio-player-1.06/src/gui/about.py 0000644 0001750 0001750 00000004222 11456551413 021023 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk, os.path, webbrowser
from tools import consts
from gettext import gettext as _
def show(parent):
""" Show an about dialog box """
dlg = gtk.AboutDialog()
dlg.set_transient_for(parent)
# Hook to handle clicks on the URL
gtk.about_dialog_set_url_hook(lambda dlg, url: webbrowser.open(url))
# Set credit information
dlg.set_name(consts.appName)
dlg.set_comments('...And Music For All')
dlg.set_version(consts.appVersion)
dlg.set_website(consts.urlMain)
dlg.set_website_label(consts.urlMain)
dlg.set_translator_credits(_('translator-credits'))
dlg.set_artists([
_('Decibel Audio Player icon:'),
' Sébastien Durel ',
'',
_('Other icons:'),
' 7 icon pack ',
' Tango project ',
])
dlg.set_authors([
_('Developer:'),
' François Ingelrest ',
'',
_('Thanks to:'),
' Emilio Pozuelo Monfort ',
])
# Set logo
dlg.set_logo(gtk.gdk.pixbuf_new_from_file(consts.fileImgIcon128))
# Load the licence from the disk if possible
if os.path.isfile(consts.fileLicense) :
dlg.set_license(open(consts.fileLicense).read())
dlg.set_wrap_license(True)
dlg.run()
dlg.destroy()
./decibel-audio-player-1.06/src/gui/extTreeview.py 0000644 0001750 0001750 00000033071 11456551413 022230 0 ustar ingelres ingelres # -*- coding: utf-8 -*-
#
# Author: Ingelrest François (Francois.Ingelrest@gmail.com)
#
# 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 Library 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import gtk
from gtk import gdk
from gobject import signal_new, TYPE_NONE, TYPE_PYOBJECT, SIGNAL_RUN_LAST
# Custom signals
signal_new('exttreeview-row-expanded', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (TYPE_PYOBJECT,))
signal_new('exttreeview-row-collapsed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (TYPE_PYOBJECT,))
signal_new('exttreeview-button-pressed', gtk.TreeView, SIGNAL_RUN_LAST, TYPE_NONE, (gdk.Event, TYPE_PYOBJECT))
class ExtTreeView(gtk.TreeView):
def __init__(self, columns, useMarkup=False):
""" If useMarkup is True, the markup attribute will be used instead of the text one for CellRendererTexts """
gtk.TreeView.__init__(self)
self.selection = self.get_selection()
# Default configuration for this tree
self.set_headers_visible(False)
self.selection.set_mode(gtk.SELECTION_MULTIPLE)
# Create the columns
nbEntries = 0
dataTypes = []
for (title, renderers, expandable) in columns:
if title is None:
for (renderer, type) in renderers:
nbEntries += 1
dataTypes.append(type)
else:
column = gtk.TreeViewColumn(title)
column.set_sizing(gtk.TREE_VIEW_COLUMN_AUTOSIZE)
column.set_expand(expandable)
self.append_column(column)
for (renderer, type) in renderers:
nbEntries += 1
dataTypes.append(type)
column.pack_start(renderer, False)
if isinstance(renderer, gtk.CellRendererText):
if useMarkup: column.add_attribute(renderer, 'markup', nbEntries-1)
else: column.add_attribute(renderer, 'text', nbEntries-1)
else:
column.add_attribute(renderer, 'pixbuf', nbEntries-1)
# Create the TreeStore associated with this tree
self.store = gtk.TreeStore(*dataTypes)
self.set_model(self.store)
# Drag'n'drop management
self.dndContext = None
self.dndSources = None
self.dndStartPos = None
self.motionEvtId = None
self.isDraggableFunc = lambda: True
self.connect('drag-begin', self.onDragBegin)
self.connect('row-expanded', self.onRowExpanded)
self.connect('row-collapsed', self.onRowCollapsed)
self.connect('button-press-event', self.onButtonPressed)
self.connect('button-release-event', self.onButtonReleased)
# Show the tree
self.show()
# --== Miscellaneous ==--
def __getSafeIter(self, path):
""" Return None if path is None, an iter on path otherwise """
if path is None: return None
else: return self.store.get_iter(path)
def scroll(self, path):
""" Ensure that path is visible """
self.scroll_to_cell(path)
def selectPaths(self, paths):
""" Select all the given paths """
self.selection.unselect_all()
for path in paths:
self.selection.select_path(path)
# --== Retrieving content ==--
def getCount(self):
""" Return how many rows are stored in the tree """
return len(self.store)
def __len__(self):
""" Return how many rows are stored in the tree """
return len(self.store)
def isValidPath(self, path):
""" Return whether the path exists """
try: self.store.get_iter(path)
except: return False
return True
def getRow(self, path):
""" Return the given row """
return tuple(self.store[path])
def getRows(self, paths):
""" Return the given rows """
return [tuple(self.store[path]) for path in paths]
def getSelectedRows(self):
""" Return selected row(s) """
return [tuple(self.store[path]) for path in self.selection.get_selected_rows()[1]]
def getSelectedPaths(self):
""" Return a list containg the selected path(s) """
return self.selection.get_selected_rows()[1]
def iterSelectedRows(self):
""" Iterate on selected rows """
for path in self.selection.get_selected_rows()[1]:
yield tuple(self.store[path])
def getSelectedRowsCount(self):
""" Return how many rows are currently selected """
return self.selection.count_selected_rows()
def isRowSelected(self, rowPath):
""" Return whether the given is selected """
return self.selection.path_is_selected(rowPath)
def getItem(self, rowPath, colIndex):
""" Return the value of the given item """
return self.store.get_value(self.store.get_iter(rowPath), colIndex)
def getNbChildren(self, parentPath):
""" Return the number of children of the given path """
return self.store.iter_n_children(self.__getSafeIter(parentPath))
def getChild(self, parentPath, num):
""" Return a path to the given child, or None if none """
child = self.store.iter_nth_child(self.__getSafeIter(parentPath), num)
if child is None: return None
else: return self.store.get_path(child)
def iterChildren(self, parentPath):
""" Iterate on the children of the given path """
iter = self.store.iter_children(self.__getSafeIter(parentPath))
while iter is not None:
yield self.store.get_path(iter)
iter = self.store.iter_next(iter)
# --== Adding/removing content ==--
def replaceContent(self, rows):
""" Replace the content of the list with the given rows """
parent = self.__getSafeIter(None)
self.freeze_child_notify()
self.set_model(None)
self.store.clear()
for row in rows:
self.store.append(parent, row)
self.set_model(self.store)
self.thaw_child_notify()
def clear(self):
""" Remove all rows from the tree """
self.store.clear()
def appendRow(self, row, parentPath=None):
""" Append a row to the tree """
return self.store.get_path(self.store.append(self.__getSafeIter(parentPath), row))
def appendRows(self, rows, parentPath=None):
""" Append some rows to the tree """
parent = self.__getSafeIter(parentPath)
self.freeze_child_notify()
for row in rows:
self.store.append(parent, row)
self.thaw_child_notify()
def insertRowBefore(self, row, parentPath, siblingPath):
""" Insert a row as a child of parent before siblingPath """
self.store.insert_before(self.__getSafeIter(parentPath), self.store.get_iter(siblingPath), row)
def insertRowAfter(self, row, parentPath, siblingPath):
""" Insert a row as a child of parent after siblingPath """
self.store.insert_after(self.__getSafeIter(parentPath), self.store.get_iter(siblingPath), row)
def removeRow(self, rowPath):
""" Remove the given row """
self.store.remove(self.store.get_iter(rowPath))
def removeAllChildren(self, rowPath):
""" Remove all the children of the given row """
self.freeze_child_notify()
while self.getNbChildren(rowPath) != 0:
self.removeRow(self.getChild(rowPath, 0))
self.thaw_child_notify()
def setItem(self, rowPath, colIndex, value):
""" Change the value of the given item """
self.store.set_value(self.store.get_iter(rowPath), colIndex, value)
# --== Changing the state of nodes ==--
def expandRow(self, path):
""" Expand the given row """
self.expand_row(path, False)
def expandRows(self, paths=None):
""" Expand the given rows, or the selected rows if paths is None """
if paths is None:
paths = self.getSelectedPaths()
for path in paths:
self.expand_row(path, False)
def collapseRows(self, paths=None):
""" Collapse the given rows, or the selected rows if paths is None """
if paths is None:
paths = self.getSelectedPaths()
for path in paths:
self.collapse_row(path)
def switchRows(self, paths=None):
""" Collapse expanded/expand collapsed given rows, or the selected rows if paths is None """
if paths is None:
paths = self.getSelectedPaths()
for path in paths:
if self.row_expanded(path): self.collapse_row(path)
else: self.expand_row(path, False)
# --== D'n'D management ==--
def setIsDraggableFunc(self, isDraggableFunc):
""" The function must return True is the selected rows can be dragged, False otherwise """
self.isDraggableFunc = isDraggableFunc
def setDNDSources(self, sources):
""" Define which kind of D'n'D this tree will generate """
self.dndSources = sources
# --== Saving/restoring the current state of the tree ==--
def saveState(self, nameIndex):
"""
Return a structure representing the current state of the tree
The nameIndex parameter is the index of the value that stores rows' name
"""
import collections
queue = collections.deque((None,))
expandedNodes = []
while len(queue) != 0:
for row in self.iterChildren(queue.pop()):
if self.row_expanded(row):
queue.append(row)
expandedNodes.append((row, self.getRow(row)[nameIndex]))
return (self.get_visible_range(), self.selection.get_selected_rows()[1], expandedNodes)
def restoreState(self, state, nameIndex):
""" Try to restore the given state, saved with saveState() """
(visibleRange, selectedRows, expandedNodes) = state
for (row, name) in expandedNodes:
if self.isValidPath(row) and self.getRow(row)[nameIndex] == name and not self.row_expanded(row):
self.expand_row(row, False)
if visibleRange is not None:
self.scroll(visibleRange[0])
for path in selectedRows:
self.selection.select_path(path)
# --== GTK Handlers ==--
def onRowExpanded(self, tree, iter, path):
""" A row has been expanded """
self.emit('exttreeview-row-expanded', path)
def onRowCollapsed(self, tree, iter, path):
""" A row has been collapsed """
self.emit('exttreeview-row-collapsed', path)
def onButtonPressed(self, tree, event):
""" A mouse button has been pressed """
retVal = False
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
if pathInfo is None: path = None
else: path = pathInfo[0]
if event.button == 1 or event.button == 3:
if path is None:
self.selection.unselect_all()
else:
if event.button == 1 and self.motionEvtId is None:
self.dndStartPos = (int(event.x), int(event.y))
self.motionEvtId = gtk.TreeView.connect(self, 'motion-notify-event', self.onMouseMotion)
stateClear = not (event.state & (gdk.SHIFT_MASK | gdk.CONTROL_MASK))
if stateClear and not self.selection.path_is_selected(path):
self.selection.unselect_all()
self.selection.select_path(path)
else:
retVal = (stateClear and self.getSelectedRowsCount() > 1 and self.selection.path_is_selected(path))
self.emit('exttreeview-button-pressed', event, path)
return retVal
def onButtonReleased(self, tree, event):
""" A mouse button has been released """
if self.motionEvtId is not None:
self.disconnect(self.motionEvtId)
self.dndContext = None
self.motionEvtId = None
stateClear = not (event.state & (gdk.SHIFT_MASK | gdk.CONTROL_MASK))
if stateClear and event.state & gdk.BUTTON1_MASK and self.getSelectedRowsCount() > 1:
pathInfo = self.get_path_at_pos(int(event.x), int(event.y))
if pathInfo is not None:
self.selection.unselect_all()
self.selection.select_path(pathInfo[0])
def onMouseMotion(self, tree, event):
""" The mouse has been moved """
if self.dndContext is None and self.isDraggableFunc() and self.dndSources is not None:
if self.drag_check_threshold(self.dndStartPos[0], self.dndStartPos[1], int(event.x), int(event.y)):
self.dndContext = self.drag_begin(self.dndSources, gdk.ACTION_COPY, 1, event)
def onDragBegin(self, tree, context):
""" A drag'n'drop operation has begun """
if self.getSelectedRowsCount() == 1: context.set_icon_stock(gtk.STOCK_DND, 0, 0)
else: context.set_icon_stock(gtk.STOCK_DND_MULTIPLE, 0, 0)
./decibel-audio-player-1.06/pix/ 0000755 0001750 0001750 00000000000 11456551413 016564 5 ustar ingelres ingelres ./decibel-audio-player-1.06/pix/decibel-audio-player-16.png 0000644 0001750 0001750 00000001653 11456551413 023503 0 ustar ingelres ingelres PNG
IHDR a bKGD pHYs u0 u03r tIME
:~ 8IDAT8u_hu }{;6vHF"ThѨFMJEхuS "dP:' cΝmng;.ċ\<anBbeBgd>'w.Y _xtt;N?<\ 7htzٟsho+c7C}eKʉJh%Gt.Aς/xK䷁Rʀ(PJFm5k1 "MaIEaHDAadsy7_WS{ Beۏv(kyM< *$ Nl0ߞzCcA͢EFh94r.uTw#uJOh,!qH7)\XjDlԖX_:2t 2G,-5IaӑMSJ!ɥ]]FPj@*aưZ)RqL45dˬ-?O]f``Lrzh4-ZFr+MoC~WQ @HB )db/>'09Iڇu9vyRbX
$NR)d,ƏdC=Gը~jH)b0L22ؚwy frY