immsview
author fabien
Fri, 06 Feb 2004 23:14:40 -0500
branchimmsview
changeset 26 76768c12aa04
parent 25 d59f0f20fa8a
child 27 9ac26f2f83e1
permissions -rwxr-xr-x
[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()