[Rhythmbox-devel] New plugin
- From: Manu Wagner <manu wagner sfr fr>
- To: rhythmbox-devel gnome org
- Subject: [Rhythmbox-devel] New plugin
- Date: Thu, 11 Feb 2010 14:08:03 +0100
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]