[Rhythmbox-devel] New plugin



Hello,

I developed the following plugin (see post on ubuntu forum)

http://ubuntuforums.org/showthread.php?t=1381198

This enables to display a cover grid view in the middle rhythmbox pane.
I succeeded in implementing the following two actions :
1. drag 'n drop of images on an album cover to set the cover art
2. double click plays the album

But I'd like to add a drag'n drop over rhythmbox sources from the iconview, especially towards ipod/multimedia devices and I'm stuck !
I know I have to enable drag'n dropping from the iconview using drag_source_set() but I can't find a proper target type definition...
Could someone help me here ?
Second question, will it automagically trigger the ipod addon appropriate action once I successfully copy tracks to an ipod source ?
Big thanks in advance to those willing to help...

Regards
Manu
# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
#
# Copyright (C) 2010 - Manu Wagner
#
# 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, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.

import string
import sys
import re
import os, threading
from threading import Thread
from sets import Set
from Queue import Queue
import cgi
import traceback
import gobject
import gtk, gtk.glade, gtk.gdk
import rb
import rhythmdb

ui_str = """
<ui>
    <toolbar name="ToolBar">
    	<placeholder name="ToolBarPluginPlaceholder">
            <toolitem name="CoverArtBrowser" action="CoverArtBrowser"/>
    	</placeholder>
    </toolbar>
</ui>
"""

CoverSize = 92

class CoverArtBrowserPlugin (rb.Plugin):
    def __init__(self):
        rb.Plugin.__init__(self)
    def activate(self, shell):
	print "CoverArtBrowser DEBUG - activate()"
        self.shell = shell
        self.db = shell.get_property('db')
        self.running = False
        self.cover_db = None
        self.local_search = None
        self.dialog = None

        data = dict()
        manager = shell.get_player().get_property('ui-manager')

	icon_file_name = self.find_file("coverbrowser.png")
        iconsource = gtk.IconSource()
        iconsource.set_filename(icon_file_name)
        iconset = gtk.IconSet()
        iconset.add_source(iconsource)
        iconfactory = gtk.IconFactory()
        iconfactory.add("covermgr_icon", iconset)
        iconfactory.add_default()
        data['action_group'] = gtk.ActionGroup('CoverArtBrowserPluginActions')

        action = gtk.Action('CoverArtBrowser', _('Cover Art _Browser'),
                            _("Show a cover art browser"),
                            "covermgr_icon")
        action.connect('activate', self.show_browser_dialog, shell)
        data['action_group'].add_action(action)

        manager.insert_action_group(data['action_group'], 0)
        data['ui_id'] = manager.add_ui_from_string(ui_str)
        manager.ensure_update()

        shell.set_data('CoverArtBrowserPluginInfo', data)

    def deactivate(self, shell):
	print "CoverArtBrowser DEBUG - deactivate()"
        data = shell.get_data('CoverArtBrowserPluginInfo')

        manager = shell.get_player().get_property('ui-manager')
        manager.remove_ui(data['ui_id'])
        manager.remove_action_group(data['action_group'])
        manager.ensure_update()

        shell.set_data('CoverArtBrowserPluginInfo', None)

        self.shell = None
        self.db = None
        self.dialog = None
        self.running = False

    def create_configure_dialog(self, dialog=None):
        if not dialog:
            dialog = self.create_dialog()
        return dialog

    def show_browser_dialog(self, action, shell):
        self.create_dialog().show()

    def create_dialog(self):
	print "CoverArtBrowser DEBUG - create_dialog()"
        if self.dialog:
	    self.close_callback(None)
            #return self.dialog
        #Only load it after all plugins are loaded
        import CoverArtDatabase
        import LocalCoverArtSearch
        CoverArtDatabase.ART_SEARCHES_LOCAL = []
        if not self.cover_db:
            self.cover_db = CoverArtDatabase.CoverArtDatabase()
        if not self.local_search:
            self.loader = rb.Loader()
            #self.local_search = LocalCoverArtSearch.LocalCoverArtSearch(self.loader)
	self.local_search = LocalCoverArtSearch.LocalCoverArtSearch()
        glade_file = self.find_file("coverart_browser.glade")
        self.gladexml = gtk.glade.XML(glade_file)
        
	self.dialog = gtk.VBox()
        self.status_label = self.gladexml.get_widget("status_label")
        self.covers_view = self.gladexml.get_widget("covers_view")
	self.start_button = self.gladexml.get_widget("start_button")
        self.close_button = self.gladexml.get_widget("close_button")

	self.start_button.set_sensitive(False)
	self.close_button.set_sensitive(False)
	# pour mettre le fond en noir
    	style = self.covers_view.get_style().copy()
    	for state in (gtk.STATE_NORMAL, gtk.STATE_PRELIGHT,gtk.STATE_ACTIVE):
		style.base[state] = gtk.gdk.Color(0,0,0)
    	self.covers_view.set_style(style)
	
	self.vbox=self.gladexml.get_widget("dialog-vbox1")
	self.vbox.reparent(self.dialog)
	self.vbox.show_all()
	self.dialog.show_all()
	self.shell.add_widget(self.dialog, rb.SHELL_UI_LOCATION_MAIN_NOTEBOOK)
	self.shell.notebook_set_page(self.dialog)

        self.covers_model = gtk.ListStore(gobject.TYPE_STRING, gtk.gdk.Pixbuf, rhythmdb.Entry)
        self.covers_view.set_model(self.covers_model)
        self.unknown_cover = gtk.gdk.pixbuf_new_from_file_at_size(\
                self.find_file('rhythmbox-missing-artwork.svg'), CoverSize, CoverSize)
        self.error_cover = gtk.gdk.pixbuf_new_from_file_at_size(\
                self.find_file('rhythmbox-error-artwork.svg'), CoverSize, CoverSize)
	#self.shell.connect("visibility-changed",self.close_callback)
        self.iter = None
	self.current_drop = None
        print "Loading albums"
        self.load_albums()
        return self.dialog
    def enable_controls_and_cb(self):
	print "CoverArtBrowser DEBUG - enable_controls_and_cb"
	self.start_button.set_sensitive(True)
	self.close_button.set_sensitive(True)
	self.start_button.connect("clicked", self.startstop_callback)
        self.close_button.connect("clicked", self.close_callback)
	targets = None	
	targets = gtk.target_list_add_image_targets (targets, writable=True)
	targets = gtk.target_list_add_image_targets (targets)
	targets = gtk.target_list_add_uri_targets (targets)
	#self.covers_view.enable_model_drag_dest(targets,gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
	#self.covers_view.drag_source_set (gtk.gdk.BUTTON1_MASK, targets, gtk.gdk.ACTION_COPY)
	self.covers_view.drag_dest_set (gtk.DEST_DEFAULT_ALL, targets, gtk.gdk.ACTION_COPY)

	targets_drop = None	
	targets_drop = gtk.target_list_add_image_targets (targets_drop)
	targets_drop = gtk.target_list_add_uri_targets (targets_drop)
	targets_drop = gtk.target_list_add_text_targets (targets_drop)

	self.covers_view.drag_source_set (gtk.gdk.BUTTON1_MASK,[('RBLibrarySource',gtk.TARGET_SAME_APP,0)], gtk.gdk.ACTION_DEFAULT)

	#self.covers_view.enable_model_drag_dest([('image/x-xpixmap', 0, 0)],gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_MOVE)
	self.covers_view.connect("item-activated", self.coverdoubleclicked_callback)
	#self.covers_view.connect("activate-cursor-item",self.coverclicked_callback)
	self.covers_view.connect("drag-data-received", self.dragimage_callback)
	self.covers_view.connect("drag-motion", self.drop_callback)
	#self.covers_view.connect("pixbuf-dropped", self.dragimage_callback)

    def close_callback(self, widget):
	print "CoverArtBrowser DEBUG - close_callback()"
	self.dialog.hide()
	self.dialog.destroy()
	self.dialog=None
	
    def coverclicked_callback(self, widget,item):
	print "CoverArtBrowser DEBUG - coverclicked_callback()"
	model=widget.get_model()
	entry = model[item][2]
	self.get_pixbuf(self.db, entry, self.load_entry)
	    
    def coverdoubleclicked_callback(self, widget,item):
	print "CoverArtBrowser DEBUG - coverdoubleclicked_callback()"
	print `item`
	model=widget.get_model()
	print `model`
	entry = model[item][2]
	print `entry`
        st_album = self.db.entry_get (entry, rhythmdb.PROP_ALBUM) or _("Unknown")
	print `st_album`
	
	search = (rhythmdb.QUERY_PROP_EQUALS, rhythmdb.PROP_ALBUM,st_album)
	query = self.db.query_new()
        self.db.query_append(query, search)
        query_model = self.db.query_model_new_empty()
        self.db.do_full_query_parsed(query_model, query)
	
	def process_entry(model, path, iter, data):
	    (parsed_entry,) = model.get(iter, 0)
	    st_track_found = self.db.entry_get (parsed_entry, rhythmdb.PROP_TRACK_NUMBER) or _("Unknown")
	    if st_track_found==1:
		self.shell.props.shell_player.play_entry(parsed_entry)
            	art_location = self.cover_db.build_art_cache_filename (self.db, parsed_entry, 'jpg')
            	if os.path.exists (art_location):
                	pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(art_location, CoverSize, CoverSize)
	    		self.covers_model.set(self.covers_model.get_iter(item), 1, pixbuf)

	query_model.foreach(process_entry, None)

    def display_cover(self,db,pixbuf,uris):
	if self.current_drop:
		pixbuf = pixbuf.scale_simple(CoverSize, CoverSize, gtk.gdk.INTERP_BILINEAR)
		self.covers_model.set(self.current_drop, 1, pixbuf)
		self.cover_count += 1
		self.set_status()
	return
    def dragimage_callback(self, widget, drag_context, x, y, selection_data, info, timestamp):
	print "CoverArtBrowser DEBUG - dragimage_callback()"
	model = widget.get_model()
	print `model`
	item = self.covers_view.get_dest_item_at_pos(x,y)[0]
	print `item`
	entry = None
	if item:
		entry = model[item][2]
		print `entry`
		uris = selection_data.get_uris ()
		if uris:
			print "this is an url"
			print `uris`
			self.current_drop = self.covers_model.get_iter(item)
			self.cover_db.set_pixbuf_from_uri (self.db, entry, uris[0], self.display_cover)
		pixbuf = selection_data.get_pixbuf()
		if pixbuf:
			print "this is an pixbuf"
	    		pixbuf = pixbuf.scale_simple(CoverSize, CoverSize, gtk.gdk.INTERP_BILINEAR)
			#self.covers_model.set(item, 1, pixbuf)
    def drop_callback(self, widget, drag_context, x, y, timestamp):
	print "CoverArtBrowser DEBUG - drop_callback()"
	return

    def startstop_callback(self, widget):
	print "CoverArtBrowser DEBUG - startstop_callback()"
        self.set_running(not self.running)
    def load_finished(self,entry):
	print "CoverArtBrowser DEBUG - load_finished()"
    def load_albums(self):
	print "CoverArtBrowser DEBUG - load_albums()"
        self.album_queue = Queue()
        self.album_loader = Thread(target = self.album_load)
        self.album_loader.start()
        self.albums = Set()
        self.album_count = 0
        self.cover_count = 0
        self.db.entry_foreach_by_type(self.db.entry_type_get_by_name("song"), \
                                      self.load_entry_callback)
	print "CoverArtBrowser DEBUG - load_albums() FINISHED"
        #for album, artist_info in self.albums.iteritems():
        #    for artist, info in artist_info:
        #        entry = info[0]

    def load_entry_callback(self, entry):
	print "CoverArtBrowser DEBUG - load_entry_callback()"
        album = self.db.entry_get(entry, rhythmdb.PROP_ALBUM)
        artist = self.db.entry_get(entry, rhythmdb.PROP_ARTIST)
	
	#track = self.db.entry_get(entry, rhythmdb.PROP_TRACK_NUMBER)
	# we only deal with the first track of album
	#if track==1:
	pixbuf = self.unknown_cover
	add = False
	if album not in self.albums:
	    self.album_count += 1
	    self.albums.add(album)
	    #self.album_queue.put([artist, album, entry])
	    tree_iter = self.covers_model.append((cgi.escape('%s - %s' % (artist, album)),pixbuf,entry))
	    print "CoverArtBrowser DEBUG - load_entry_callback"
	    self.iter = self.iter or tree_iter
	    self.album_queue.put([artist, album, entry, tree_iter])

    def album_load(self):
        while True:
	    print "CoverArtBrowser DEBUG - album_load()"
            artist, album, entry, tree_iter = self.album_queue.get()
            art_location = self.cover_db.build_art_cache_filename (self.db, entry, 'jpg')
	    #print "CoverArtBrowser DEBUG - album_load, art_location = "
	    #print `art_location`
            if os.path.exists (art_location):
                pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(art_location, CoverSize, CoverSize)
		#print "CoverArtBrowser DEBUG - album_load OK"
                self.cover_count += 1
                gtk.gdk.threads_enter()
                self.covers_model.set(tree_iter, 1, pixbuf)
		self.set_status()
                gtk.gdk.threads_leave()
		if self.album_queue.empty():
			gtk.gdk.threads_enter()
			print "CoverArtBrowser DEBUG - album_load() FINISHED"
			self.enable_controls_and_cb()
			gtk.gdk.threads_leave()

    def set_running(self, value):
        self.running = value
        if self.running:
	    print "CoverArtBrowser DEBUG - set_running() = TRUE"
            self.start_button.set_label("Cancel")
            self.load_iter()
        else:
	    print "CoverArtBrowser DEBUG - set_running() = FALSE"
            self.start_button.set_label("Fetch covers")

    def load_iter(self):
	print "CoverArtBrowser DEBUG - load_iter()"
        if (not self.running) or (not self.iter):
            self.set_running(False)
            return
        entry = self.covers_model.get(self.iter, 2)[0]
        self.get_pixbuf(self.db, entry, self.load_entry)


    def get_pixbuf (self, db, entry, callback):
	print "CoverArtBrowser DEBUG - get_pixbuf()"
        if entry is None:
            callback (entry, None, None)
            return

        st_artist = db.entry_get (entry, rhythmdb.PROP_ARTIST) or _("Unknown")
        st_album = db.entry_get (entry, rhythmdb.PROP_ALBUM) or _("Unknown")

        # replace quote characters
        # don't replace single quote: could be important punctuation
        for char in ["\""]:
            st_artist = st_artist.replace (char, '')
            st_album = st_album.replace (char, '')

        Coroutine (self.next, self.cover_db.image_search, db, st_album, st_artist,entry, False, callback).begin()

    def load_entry(self, entry, pixbuf, location):
	print "CoverArtBrowser DEBUG - load_entry()"
        if self.dialog:
            pixbuf = pixbuf.scale_simple(CoverSize, CoverSize, gtk.gdk.INTERP_BILINEAR)
            self.covers_model.set(self.iter, 1, pixbuf)
            self.next()

    def next(self):
	print "CoverArtBrowser DEBUG - next()"
	if self.iter is None:
	    self.set_running(False)
	    return
        else:
            if (self.covers_model.get(self.iter, 1)[0] == self.unknown_cover):
                self.covers_model.set(self.iter, 1, self.error_cover)
            self.iter = self.covers_model.iter_next(self.iter)
            current_cover=None
	    if self.iter:
            	current_cover = (self.covers_model.get(self.iter, 1)[0])
            	while self.iter and (current_cover != self.unknown_cover):
                	self.iter = self.covers_model.iter_next(self.iter)
                	if self.iter:
				current_cover = (self.covers_model.get(self.iter, 1)[0])
            self.load_iter()

    def set_status(self):
	albumleft = self.album_count-self.cover_count
        self.status_label.set_label("%d covers left to download" % albumleft)

    def reset(self):
	albumleft = self.album_count-self.cover_count
        self.status_label.set_label("%d covers left to download" % albumleft)
class Coroutine:
    """A simple message-passing coroutine implementation.
    Not thread- or signal-safe.
    Usage:
            def my_iter (plexer, args):
                    some_async_task (..., callback=plexer.send (tokens))
                    yield None
                    tokens, (data, ) = plexer.receive ()
                    ...
            Coroutine (my_iter, args).begin ()
    """
    def __init__ (self, error_callback, iter, *args):
	print "CoverArtBrowser DEBUG - Coroutine::__init__()"
        self._continuation = iter (self, *args)
        self.error_callback = error_callback
        self._executing = False
    def _resume (self):
	print "CoverArtBrowser DEBUG - Coroutine::_resume()"
        if not self._executing:
            self._executing = True
            try:
                try:
                    self._continuation.next ()
                    while self._data:
		        print "CoverArtBrowser DEBUG - Coroutine::_resume, OK()"
                        self._continuation.next ()
                except StopIteration:
                    print "CoverArtBrowser DEBUG - Coroutine::_resume, STOPITERATION()"
		    pass
                # Catch all exceptions for faulty art plugins
                except Exception:
		    print "CoverArtBrowser DEBUG - Coroutine::_resume, EXCEPTION()"
                    self.error_callback()
            finally:
                self._executing = False
    def clear (self):
	print "CoverArtBrowser DEBUG - Coroutine::clear()"
        self._data = []
    def begin (self):
	print "CoverArtBrowser DEBUG - Coroutine::begin()"
        self.clear ()
        self._resume ()
    def send (self, *tokens):
	print "CoverArtBrowser DEBUG - Coroutine::send()"
        def callback (*args):
            self._data.append ((tokens, args))
            self._resume ()
        return callback
    def receive (self):
	print "CoverArtBrowser DEBUG - Coroutine::receive()"
        return self._data.pop (0)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]