Banshee Plugin



Here is a Banshee plugin that is heavily based off of the existing
Rhythmbox plugin. Seeing as Ubuntu 11.04 will be shipping with Banshee
as the default media player, some may find this useful.

Any feedback/suggestions welcome.

Thanks.
__kupfer_name__ = _("Banshee")
__kupfer_sources__ = ("BansheeSource", )
__description__ = _("Play and enqueue tracks and browse the music library")
__version__ = ""
__author__ = ("William Friesen <wfriesen gmail com>, "
              "Ulrik Sverdrup <ulrik sverdrup gmail com>")

import dbus
import unicodedata
from hashlib import md5
from contextlib import closing
import os
import sqlite3

from kupfer.objects import (Leaf, Source, Action, RunnableLeaf,
		SourceLeaf )
from kupfer import icons, utils, config
from kupfer import task
from kupfer.obj.apps import AppLeafContentMixin
from kupfer.plugin import rhythmbox_support
from kupfer.plugin.rhythmbox import _locale_sort_artist_album_songs

NEEDED_KEYS= set(("title", "artist", "album", "track-number", "location", ))
UNICODE_KEYS = set(("title", "artist", "album"))

def get_banshee_songs(dbfile, typ="song", keys=NEEDED_KEYS):
	"""Return a list of info dictionaries for all songs
	in a Banshee library database file, with dictionary
	keys as given in @keys.
	"""
	banshee_dbfile = os.path.expanduser(dbfile)

	lSongs = []

	with closing(sqlite3.connect(banshee_dbfile, timeout=1)) as conn:
		c = conn.cursor()
		c.execute("""SELECT CoreAlbums.Title, CoreArtists.Name, CoreTracks.Title, CoreTracks.Uri, CoreTracks.TrackNumber
				FROM CoreTracks
				JOIN CoreArtists ON CoreTracks.ArtistID = CoreArtists.ArtistID
				JOIN CoreAlbums ON CoreTracks.AlbumID = CoreAlbums.AlbumID""")
		for album, artist, title, uri, track_no in c:
			info = {}
			info["title"] = title
			info["artist"] = artist
			info["album"] = album
			info["track-number"] = track_no
			info["location"] = uri
			lSongs.append(info)
	return lSongs

def _create_dbus_connection(activate=True, object_name="SourceManager/PlayQueue", iface_name="PlayQueue"):
	SERVICE_NAME = "org.bansheeproject.Banshee"
	OBJECT_NAME = "/org/bansheeproject/Banshee/%s" % object_name
	IFACE_NAME = "org.bansheeproject.Banshee.%s" % iface_name

	interface = None
	obj = None
	sbus = dbus.SessionBus()

	try:
		#check for running Banshee (code from note.py)
		proxy_obj = sbus.get_object('org.freedesktop.DBus',
				'/org/freedesktop/DBus')
		dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus')
		if activate or dbus_iface.NameHasOwner(SERVICE_NAME):
			obj = sbus.get_object(SERVICE_NAME, OBJECT_NAME)
		if obj:
			interface = dbus.Interface(obj, IFACE_NAME)
	except dbus.exceptions.DBusException, err:
		pretty.print_debug(err)
	return interface

def _create_dbus_connection_playbackcontroller():
	return _create_dbus_connection(object_name="PlaybackController", iface_name="PlaybackController")

def _tostr(ustr):
	return ustr.encode("UTF-8")

def play_song(info):
	song = []
	song.append(info)
	enqueue_songs(song)
	interface = _create_dbus_connection_playbackcontroller()
	if not interface:
		return
	interface.First()

def enqueue_songs(info):
	songs = list(info)
	if not songs:
		return
	interface = _create_dbus_connection()
	if not interface:
		return
	for song in songs:
		uri = _tostr(song["location"])
		interface.EnqueueUri(uri, False)

class EnqueueTask(task.ThreadTask):
	def __init__(self, songs):
		super(task.Task, self).__init__()
		self.songs = songs
	def thread_do(self):
		enqueue_songs(self.songs)

class Play (RunnableLeaf):
	def __init__(self):
		RunnableLeaf.__init__(self, name=_("Play"))
	def run(self):
		utils.spawn_async(("banshee-1", "--play"))
	def get_description(self):
		return _("Resume playback in Banshee")
	def get_icon_name(self):
		return "media-playback-start"

class Pause (RunnableLeaf):
	def __init__(self):
		RunnableLeaf.__init__(self, name=_("Pause"))
	def run(self):
		utils.spawn_async(("banshee-1", "--pause"))
	def get_description(self):
		return _("Pause playback in Banshee")
	def get_icon_name(self):
		return "media-playback-pause"

class Next (RunnableLeaf):
	def __init__(self):
		RunnableLeaf.__init__(self, name=_("Next"))
	def run(self):
		utils.spawn_async(("banshee", "--next"))
	def get_description(self):
		return _("Jump to next track in Banshee")
	def get_icon_name(self):
		return "media-skip-forward"

class Previous (RunnableLeaf):
	def __init__(self):
		RunnableLeaf.__init__(self, name=_("Previous"))
	def run(self):
		utils.spawn_async(("banshee", "--previous"))
	def get_description(self):
		return _("Jump to previous track in Banshee")
	def get_icon_name(self):
		return "media-skip-backward"

class ClearQueue (RunnableLeaf):
	def __init__(self):
		RunnableLeaf.__init__(self, name=_("Clear Queue"))
	def run(self):
		interface = _create_dbus_connection()
		if not interface:
			return
		interface.Clear()
	def get_icon_name(self):
		return "edit-clear"

def _songs_from_leaf(leaf):
	"return a sequence of songs from @leaf"
	if isinstance(leaf, SongLeaf):
		return (leaf.object, )
	if isinstance(leaf, TrackCollection):
		return list(leaf.object)

class PlayTracks (Action):
	rank_adjust = 5
	def __init__(self):
		Action.__init__(self, _("Play"))

	def is_async(self):
		return True

	def activate(self, leaf):
		return self.activate_multiple((leaf, ))

	def activate_multiple(self, objects):
		# for multiple dispatch, play the first and enqueue the rest
		to_enqueue = []
		objects = iter(objects)
		# take only the first object in the first loop
		# notice the break
		for leaf in objects:
			songs = _songs_from_leaf(leaf)
			if not songs:
				continue
			play_song(songs[0])
			to_enqueue.extend(songs[1:])
			break
		for leaf in objects:
			to_enqueue.extend(_songs_from_leaf(leaf))
		if to_enqueue:
			return EnqueueTask(to_enqueue)

	def get_description(self):
		return _("Play tracks in Banshee")
	def get_icon_name(self):
		return "media-playback-start"

class Enqueue (Action):
	def __init__(self):
		Action.__init__(self, _("Enqueue"))

	def is_async(self):
		return True

	def activate(self, leaf):
		return self.activate_multiple((leaf, ))

	def activate_multiple(self, objects):
		to_enqueue = []
		for leaf in objects:
			to_enqueue.extend(_songs_from_leaf(leaf))
		return EnqueueTask(to_enqueue)

	def get_description(self):
		return _("Add tracks to the play queue")
	def get_gicon(self):
		return icons.ComposedIcon("gtk-execute", "media-playback-start")
	def get_icon_name(self):
		return "media-playback-start"

class SongLeaf (Leaf):
	serializable = 1
	def __init__(self, info, name=None):
		"""Init with song info
		@info: Song information dictionary
		"""
		if not name: name = info["title"]
		Leaf.__init__(self, info, name)
	def repr_key(self):
		"""To distinguish songs by the same name"""
		return (self.object["title"], self.object["artist"],
				self.object["album"])
	def get_actions(self):
		yield PlayTracks()
		yield Enqueue()
	def get_description(self):
		# TRANS: Song description
		return _("by %(artist)s from %(album)s") % {
				"artist": self.object["artist"],
				"album": self.object["album"],
				}
	def get_icon_name(self):
		return "audio-x-generic"

class CollectionSource (Source):
	def __init__(self, leaf):
		Source.__init__(self, unicode(leaf))
		self.leaf = leaf
	def get_items(self):
		for song in self.leaf.object:
			yield SongLeaf(song)
	def repr_key(self):
		return self.leaf.repr_key()
	def get_description(self):
		return self.leaf.get_description()
	def get_thumbnail(self, w, h):
		return self.leaf.get_thumbnail(w, h)
	def get_gicon(self):
		return self.leaf.get_gicon()
	def get_icon_name(self):
		return self.leaf.get_icon_name()

class TrackCollection (Leaf):
	"""A generic track collection leaf, such as one for
	an Album or an Artist
	"""
	def __init__(self, info, name):
		"""Init with track collection
		@info: Should be a sequence of song information dictionaries
		"""
		Leaf.__init__(self, info, name)
	def get_actions(self):
		yield PlayTracks()
		yield Enqueue()
	def has_content(self):
		return True
	def content_source(self, alternate=False):
		return CollectionSource(self)
	def get_icon_name(self):
		return "media-optical"

class AlbumLeaf (TrackCollection):
	def get_description(self):
		artist = None
		for song in self.object:
			if not artist:
				artist = song["artist"]
			elif artist != song["artist"]:
				# TRANS: Multiple artist description "Artist1 et. al. "
				artist = _("%s et. al.") % artist
				break
		# TRANS: Album description "by Artist"
		return _("by %s") % (artist, )
	def get_thumbnail(self, width, height):
		if not hasattr(self, "cover_file"):
			artist = self.object[0]["artist"]
			album = self.object[0]["album"]
			hash_string = "%s\t%s" % (artist, album)
			hash_string = unicodedata.normalize("NFKD", hash_string)
			cache_name = "album-%s.jpg" % (md5(hash_string).hexdigest())
			self.cover_file = config.get_cache_file(("media-art", cache_name))
		return icons.get_pixbuf_from_file(self.cover_file, width, height)

class ArtistAlbumsSource (CollectionSource):
	def get_items(self):
		albums = {}
		for song in self.leaf.object:
			album = song["album"]
			album_list = albums.get(album, [])
			album_list.append(song)
			albums[album] = album_list
		for album in albums:
			yield AlbumLeaf(albums[album], album)
	def should_sort_lexically(self):
		return True

class ArtistLeaf (TrackCollection):
	def get_description(self):
		# TRANS: Artist songs collection description
		return _("Tracks by %s") % (unicode(self), )
	def get_gicon(self):
		return icons.ComposedIcon("media-optical", "system-users")
	def content_source(self, alternate=False):
		if alternate:
			return CollectionSource(self)
		return ArtistAlbumsSource(self)

class BansheeAlbumsSource (Source):
	def __init__(self, library):
		Source.__init__(self, _("Albums"))
		self.library = library

	def get_items(self):
		for album in self.library:
			yield AlbumLeaf(self.library[album], album)
	def should_sort_lexically(self):
		return True

	def get_description(self):
		return _("Music albums in Banshee library")
	def get_gicon(self):
		return icons.ComposedIcon("media-player-banshee", "media-optical",
				emblem_is_fallback=True)
	def get_icon_name(self):
		return "banshee"
	def provides(self):
		yield AlbumLeaf

class BansheeArtistsSource (Source):
	def __init__(self, library):
		Source.__init__(self, _("Artists"))
		self.library = library

	def get_items(self):
		for artist in self.library:
			yield ArtistLeaf(self.library[artist], artist)
	def should_sort_lexically(self):
		return True

	def get_description(self):
		return _("Music artists in Banshee library")
	def get_gicon(self):
		return icons.ComposedIcon("media-player-banshee", "system-users",
				emblem_is_fallback=True)
	def get_icon_name(self):
		return "banshee"
	def provides(self):
		yield ArtistLeaf

class BansheeSongsSource (Source):
	"""The whole song library in Leaf representation"""
	def __init__(self, library):
		Source.__init__(self, _("Songs"))
		self.library = library

	def get_items(self):
		for song in _locale_sort_artist_album_songs(self.library):
			yield SongLeaf(song)

	def get_actions(self):
		return ()
	def get_description(self):
		return _("Songs in Banshee library")
	def get_gicon(self):
		return icons.ComposedIcon("media-player-banshee", "audio-x-generic",
				emblem_is_fallback=True)
	def provides(self):
		yield SongLeaf

class BansheeSource (AppLeafContentMixin, Source):
	appleaf_content_id = "banshee-1"
	def __init__(self):
		Source.__init__(self, _("Banshee"))
	def get_items(self):
		try:
			dbfile = config.get_config_file("banshee.db", "banshee-1")
			songs = get_banshee_songs(dbfile=dbfile)
		except StandardError, e:
			self.output_error(e)
			songs = []
		albums = rhythmbox_support.parse_rhythmbox_albums(songs)
		artists = rhythmbox_support.parse_rhythmbox_artists(songs)
		yield Play()
		yield Pause()
		yield Next()
		yield Previous()
		yield ClearQueue()
		artist_source = BansheeArtistsSource(artists)
		album_source = BansheeAlbumsSource(albums)
		songs_source = BansheeSongsSource(artists)
		yield SourceLeaf(artist_source)
		yield SourceLeaf(album_source)
		yield SourceLeaf(songs_source)

	def get_description(self):
		return _("Play and enqueue tracks and browse the music library")
	def get_icon_name(self):
		return "media-player-banshee"
	def provides(self):
		yield RunnableLeaf
		yield SourceLeaf
		yield SongLeaf


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