[svn] Display only distinct UID. The path selected is arbitrary, but
it's always the current one if Current is pressed.
#!/usr/bin/python
# Copyright (C) 2004 by Fabien Ninoles
# IMMSView is aim to be a replacement to XMMS playlist editor
# with better support for IMMS plugin.
# IMMSView 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, or (at your option)
# any later version.
# IMMSView is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with GNU Emacs; see the file COPYING. If not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
_version_ = "$Id: immsview 1708 2004-02-07 04:14:40Z fabien $"
# $Log$
# Revision 1.26 2004/02/07 04:14:40 fabien
# Display only distinct UID. The path selected is arbitrary, but
# it's always the current one if Current is pressed.
#
# Revision 1.25 2004/02/07 01:21:32 fabien
# Some typos... (better take a break now...)
#
# Revision 1.24 2004/02/07 01:18:41 fabien
# Better like a four colors gradient.
#
# Revision 1.23 2004/02/07 00:53:02 fabien
# Make a gradient of color instead...
# I'm not sure which one I prefer however.
#
# Revision 1.22 2004/02/06 06:10:49 fabien
# Add presentation page.
#
# Revision 1.21 2004/02/05 17:14:33 fabien
# Fix curtime until refresh.
#
# Revision 1.20 2004/02/05 15:46:17 fabien
# Remove the useless ORDER BY from the query for
# speed improvemennt (Doh!)
#
# Revision 1.19 2004/02/05 07:07:38 fabien
# Add some colors related to the rating.
#
# Revision 1.18 2004/02/04 22:21:47 fabien
# Try to update the Last string dynamically... doesn't seem to work.
#
# Revision 1.17 2004/02/04 21:31:45 fabien
# Update the current song. This slow down thing a bit (any querying
# take one to 2 seconds... That's pity!) but it make sure that at least
# this song is correct.
#
# Revision 1.16 2004/02/04 20:20:40 fabien
# Used the default sort function (return 0).
#
# Revision 1.15 2004/02/04 05:46:57 fabien
# Add SQL quoting for path name.
#
# Revision 1.14 2004/02/03 21:50:28 fabien
# Update also on double click.
#
# Revision 1.13 2004/02/03 20:55:27 fabien
# Play selected: check if the file exist, elsewhere try to
# update it through the Db.
#
# Revision 1.12 2004/02/03 18:50:03 fabien
# Add double click and remove sorting when refreshing data.
#
# Revision 1.11 2004/02/02 15:45:25 fabien
# Add a comment about the autocommit value in db.connect()
#
# Revision 1.10 2004/02/02 15:42:43 fabien
# OK, reset current after changing song manually... however, I most
# sleep for almost a second to be sure to catch it.
#
# Revision 1.9 2004/02/02 06:54:09 fabien
# Add cvs log since I don't maintain a changelog currently.
#
# Revision 1.8 2004/02/02 06:44:10 fabien
# All functions are now implemented (with a somewhat "cleaner" interface.
#
# Revision 1.7 2004/02/02 04:51:35 fabien
# Switch to GTK2.
#
# Revision 1.6 2004/02/01 17:21:02 fabien
# Add play selected song.
#
# Revision 1.5 2004/02/01 16:22:40 fabien
# Add title.
#
# Revision 1.4 2004/02/01 16:12:45 fabien
# Add current and correct plot function.
#
# Revision 1.3 2004/02/01 15:59:04 fabien
# Adding xmms selection and last display.
#
# Revision 1.2 2004/02/01 04:40:41 fabien
# add versioning.
#
# Revision 1.1 2004/02/01 03:05:25 fabien
# Premiere version de immsview.
# The aim of immsview is to become a better playlist editor than the
# normal
# TODO:
# * IMMS:
# - Add composed rating
# - Rating edition
# - SID grouping
# - UID grouping!!! (yes, you can have different path with the same UID)
# * XMMS:
# - getting current playlist
# - editing playlist
# * File support:
# - adding, deleting, suppressing a file (including updating other
# interface).
# - artist, title, genre informations (ID3)
# * Interface:
# - Real application interface (with menu, icons, accelerators, etc.)
import pygtk
pygtk.require('2.0')
import sys
import os
import sqlite
import gobject
import gtk
import gtk.glade
import gettext
import xmms.control
import time
gtk.glade.bindtextdomain('immsview', '/usr/share/immsview/LANG')
gtk.glade.textdomain('immsview')
_ = gettext.gettext
def strtime(seconds):
secs = abs(round(seconds))
minutes = secs / 60;
hours = minutes / 60;
days = hours / 24;
secs = secs % 60;
minutes %= 60;
hours %= 24;
if seconds < 0:
s = "-"
else:
s = ""
if days >= 1:
s += "%dd %dh" % (days, hours)
elif hours >= 1:
s += "%dh%02d" % (hours, minutes)
elif minutes >= 1:
s += "%d'%02d\"" % (minutes, secs)
else:
s += "%d\"" % (secs)
return s
class XMMSControl:
def __getattr__(self, name):
return xmms.control.__dict__[name]
def get_current_file(self):
return self.get_playlist_file(
self.get_playlist_pos())
def find_in_playlist(self, filename):
for idx in range(self.get_playlist_length()):
if filename == self.get_playlist_file(idx):
return idx
return -1
def play_file(self, filename):
idx = self.find_in_playlist(filename)
if idx == -1:
self.enqueue_and_play((filename,))
else:
self.set_playlist_pos(idx)
def quote_sql(str):
return str.replace("'", "''")
class IMMSDb:
_dbname = os.environ['HOME'] + '/.imms/imms.db'
# _dbname = os.environ['HOME'] + '/.imms/imms.backup.db'
def __init__(self):
# autocommit = 1 disable autocommit!
self.cx = sqlite.connect(IMMSDb._dbname, autocommit = 1,
timeout = 2, encoding = ('utf-8', 'replace'))
def commit(self):
# self.cx.commit()
pass
def _get_ratings(self, min = 0, max = 250):
cu = self.cx.cursor()
cu.execute('''SELECT Rating.uid, Rating.rating
FROM Rating
WHERE Rating.rating >= %d
AND Rating.rating <= %d
ORDER BY Rating.rating;''' % (min, max))
return cu.fetchall()
def _get_library_uid(self, uid):
cu = self.cx.cursor()
cu.execute('''SELECT Library.path
FROM Library
WHERE Library.uid = %d;''' % (uid,))
return cu.fetchone()
def get_uid_by_path(self, path):
cu = self.cx.cursor()
cu.execute("""SELECT Library.uid FROM Library
WHERE Library.path = '%s';""" % quote_sql(path))
return map(lambda x: x[0], cu.fetchall())
def get_ratings_and_info(self, uids = None):
print time.ctime(time.time()) + ": querying"
cu = self.cx.cursor()
qry = '''SELECT l.uid, r.rating, l.path, ls.last
FROM Library l, Rating r, Last ls
WHERE l.uid = r.uid AND l.sid = ls.sid'''
if uids:
qry += ' AND (l.uid = %d' % (uids.pop())
for uid in uids:
qry += ' OR l.uid = %d' % uid
qry += ')'
qry += ';'
cu.execute(qry)
# Better to fetch everything since locking can really mess
# things in imms plugin.
print time.ctime(time.time()) + ": mapping"
results = {}
tune = cu.fetchone()
while tune:
try:
uid = int(tune[0])
if results.has_key(uid):
results[uid]['path'].append(
tune[2].decode('utf-8', 'replace'))
else:
results[uid] = {
'rating' : int(tune[1]),
'path' : [ tune[2].decode('utf-8', 'replace') ],
'last' : int(tune[3])}
except UnicodeDecodeError:
print tune[2]
tune = cu.fetchone()
return results
_gdk_colors = []
for i in range(76):
if i <= 25:
red = 255
green = i * 255 / 25
blue = 0
elif i <= 50:
red = (50-i) * 255 / 25
green = 255
blue = 0
else:
red = 0
green = 255
blue = (i-50) * 255 / 25
_gdk_colors.append("#%02X%02X%02X" % (red, green, blue))
def rating_to_color(rate):
rate = min(max(rate,75),150)
return _gdk_colors[rate-75]
class IMMSStore(gtk.ListStore):
COL_RATING = 0
COL_PATH = 1
COL_LAST_STR = 2
COL_LAST = 3
COL_SELECT = 4
COL_UID = 5
COL_RATING_COLOR = 6
def __init__(self, db):
gtk.ListStore.__init__(self,
gobject.TYPE_INT,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_INT,
gobject.TYPE_BOOLEAN,
gobject.TYPE_INT,
gobject.TYPE_STRING,
)
self.db = db
self.set_default_sort_func(self.default_sort)
self.set_sort_column_id(IMMSStore.COL_RATING, gtk.SORT_DESCENDING)
self.curtime = time.time()
def default_sort(self, a, b, dummy):
return 0
def tune_to_giter(self, uid, tune, giter = None, path = None):
if path:
for fn in tune['path']:
if fn == path:
break
else:
fn = tune['path'][0]
else:
fn = tune['path'][0]
if not giter:
giter = self.append(None)
self.set(giter,
IMMSStore.COL_UID, uid,
IMMSStore.COL_RATING, tune['rating'],
IMMSStore.COL_PATH, fn,
IMMSStore.COL_LAST, tune['last'],
IMMSStore.COL_LAST_STR, strtime(self.curtime-tune['last']),
IMMSStore.COL_RATING_COLOR, rating_to_color(tune['rating']),
IMMSStore.COL_SELECT, gtk.FALSE)
return giter
def refresh(self):
self.curtime = time.time()
col, order = self.get_sort_column_id()
if col:
self.set_sort_column_id(-1, gtk.SORT_ASCENDING)
tunes = self.db.get_ratings_and_info()
self.clear()
print time.ctime(time.time()) + ": inserting"
for uid, tune in tunes.items():
self.tune_to_giter(uid, tune)
print time.ctime(time.time()) + ": end insert"
if col:
self.set_sort_column_id(col, order)
def find_selected_giter(self):
giter = self.get_iter_first()
while giter:
if self.get_value(giter, IMMSStore.COL_SELECT):
break
giter = self.iter_next(giter)
return giter
def find_giter_from_uid(self, uid):
giter = self.get_iter_first()
while giter:
if self.get_value(giter, IMMSStore.COL_UID) == uid:
break
giter = self.iter_next(giter)
return giter
def find_giter_from_path(self, song):
uids = self.db.get_uid_by_path(song)
if len(uids) == 0:
return None
uid = uids[0]
giter = self.find_giter_from_uid(uid)
if not giter:
tunes = self.db.get_ratings_and_info([uid])
if tunes > 0:
giter = self.tune_to_giter(uid, tunes[uid], None, song)
else:
giter = self.update_giter(giter, song)
return giter
def update_giter(self, giter, path = None):
uid = self.get_value(giter, IMMSStore.COL_UID)
tunes = self.db.get_ratings_and_info([uid,])
if len(tunes) > 0:
return self.tune_to_giter(uid, tunes[uid], giter, path)
return giter
## def get_value(self, giter, col):
## # sniff! Can't override built-ins
## if col == IMMSStore.COL_LAST_STR:
## return strtime(time.time() -
## self.get_value(giter, IMMSStore.COL_LAST))
## else:
## return gtk.ListStore.get_value(self, giter, col)
class IMMSView(gtk.TreeView):
def __init__(self, model, xmms):
gtk.TreeView.__init__(self, model)
self.xmms = xmms
self.create_widgets()
def create_widgets(self):
renderer = gtk.CellRendererText()
renderer.set_property('weight', 700)
renderer.set_property('background-set', gtk.TRUE)
column = gtk.TreeViewColumn(_("Rating"), renderer,
weight_set = IMMSStore.COL_SELECT,
background = IMMSStore.COL_RATING_COLOR,
text = IMMSStore.COL_RATING)
column.set_sort_column_id(IMMSStore.COL_RATING)
self.append_column(column)
renderer = gtk.CellRendererText()
renderer.set_property('weight', 700)
# renderer.set_property('background-set', gtk.FALSE)
column = gtk.TreeViewColumn(_("Last"), renderer,
weight_set = IMMSStore.COL_SELECT,
text = IMMSStore.COL_LAST_STR)
column.set_sort_column_id(IMMSStore.COL_LAST)
self.append_column(column)
column = gtk.TreeViewColumn(_("File"), renderer,
weight_set = IMMSStore.COL_SELECT,
text = IMMSStore.COL_PATH)
column.set_resizable(gtk.TRUE)
column.set_sort_column_id(IMMSStore.COL_PATH)
self.append_column(column)
self.set_search_column(IMMSStore.COL_PATH)
self.set_headers_clickable(gtk.TRUE)
self.connect('row-activated', self.on_row_activated)
def set_current_song(self, song):
model = self.get_model()
giter = model.find_selected_giter()
if giter:
model.update_giter(giter)
model.set_value(giter, IMMSStore.COL_SELECT, gtk.FALSE)
giter = model.find_giter_from_path(song)
if giter:
model.set_value(giter, IMMSStore.COL_SELECT, gtk.TRUE)
self.set_cursor(model.get_path(giter))
def get_filename(self, giter):
model = self.get_model()
fn = model.get_value(model.update_giter(giter), IMMSStore.COL_PATH)
try:
os.stat(fn)
except OSError:
return None
return fn
def get_file_selected(self):
model, giter = self.get_selection().get_selected()
if giter:
return self.get_filename(giter)
return None
def on_row_activated(self, tview, path, col):
model = self.get_model()
giter = model.get_iter(path)
fn = self.get_filename(giter)
self.set_current_song(fn)
self.xmms.play_file(fn)
class IMMSToolbar(gtk.Toolbar):
# _IMMSPLOT_COMMAND = 'immsplot &'
_IMMSPLOT_COMMAND = '/home/fabien/bin/immsplot &'
_SLEEP_TIME = 5
def __init__(self, iview, xmms):
gtk.Toolbar.__init__(self)
self.iview = iview
self.xmms = xmms
self.create_widgets()
def create_widgets(self):
self.append_item(_('Refresh'), _('Refresh list'),
None, None, self.do_refresh)
self.append_item(_('Plot'), _('Show graph of rates'),
None, None, self.plot)
self.append_item(_('Current'), _('Get current song'),
None, None, self.do_get_current)
self.append_item(_('Previous'), _('Play previous song'),
None, None, self.do_play_prev)
self.append_item(_('Play'), _('Play selection'),
None, None, self.do_play)
self.append_item(_('Next'), _('Play next song'),
None, None, self.do_play_next)
def plot(self, dummy):
os.system(self._IMMSPLOT_COMMAND)
def do_refresh(self, dummy):
self.iview.get_model().refresh()
def do_get_current(self, dummy):
song = self.xmms.get_current_file()
self.iview.set_current_song(song)
def do_play(self, dummy):
fn = self.iview.get_file_selected()
if fn:
self.xmms.play_file(fn)
self.sleep(self._SLEEP_TIME)
self.do_get_current(dummy)
def do_play_prev(self, dummy):
self.xmms.playlist_prev()
self.sleep(self._SLEEP_TIME)
self.do_get_current(dummy)
def do_play_next(self, dummy):
self.xmms.playlist_next()
self.sleep(self._SLEEP_TIME)
self.do_get_current(dummy)
def sleep(self, secs):
start = time.time()
while (time.time() - start) < secs:
time.sleep(0.2)
if gtk.main_iteration_do(gtk.FALSE):
break;
class Application:
def __init__(self):
self.xmms = XMMSControl()
self.db = IMMSDb()
self.model = IMMSStore(self.db)
def main(self):
self.create_widgets()
gtk.mainloop()
def create_widgets(self):
root = gtk.Window()
root.set_title(_("IMMSView"))
root.connect('destroy', gtk.mainquit)
vbox = gtk.VBox(spacing = 3)
root.add(vbox)
vbox.show()
iview = IMMSView(self.model,self.xmms)
scroll = gtk.ScrolledWindow()
scroll.add(iview)
vbox.pack_end(scroll)
iview.show()
scroll.show()
toolbar = IMMSToolbar(iview, self.xmms)
vbox.pack_start(toolbar, expand = gtk.FALSE)
toolbar.show()
root.show()
toolbar.do_refresh(None)
toolbar.do_get_current(None)
if __name__ == '__main__':
app = Application()
app.main()