remuco-source-0.9.6/Makefile 0000644 0000000 0000000 00000005621 11700415064 015716 0 ustar root root 0000000 0000000 # -----------------------------------------------------------------------------
# Makefile intended for end users. It is a wrapper around setup.py.
# -----------------------------------------------------------------------------
#PREFIX ?= /usr/local
#SETUP := python setup.py install --prefix=$(PREFIX)
SETUP := python setup.py install
ADAPTERS := $(shell ls adapter)
help:
@echo
@echo "To install a player adapter (and required base components), run:"
@for PA in $(ADAPTERS); do echo " make install-$$PA"; done
@echo
@echo "To uninstall a player adapter, run:"
@for PA in $(ADAPTERS); do echo " make uninstall-$$PA"; done
@echo
@echo "To uninstall all components (base and player adapters), run:"
@echo " make uninstall-all"
@echo
@echo "Of course, be root or use 'sudo' when needed."
@echo
all: help
@true
install: help
@true
uninstall: help
@true
install-base: clean
python base/module/install-check.py
REMUCO_COMPONENTS="" $(SETUP) --record install-base.log
@echo "+-----------------------------------------------------------------+"
@echo "| Installed Remuco base."
@echo "+-----------------------------------------------------------------+"
install-%: install-base
@IC=adapter/$(subst install-,,$@)/install-check.py ; \
[ ! -e $$IC ] || python $$IC
REMUCO_COMPONENTS=$(subst install-,,$@) $(SETUP) --record install-tmp.log
diff --suppress-common-lines -n install-base.log install-tmp.log \
| grep "^/" > install-$(subst install-,,$@).log
rm install-tmp.log
@echo "+-----------------------------------------------------------------+"
@echo "| Installed player adapter '$(subst install-,,$@)'."
@[ ! -e adapter/$(subst install-,,$@)/.wip ] || \
echo "| WARNING: This adapter is still work in progress!"
@echo "+-----------------------------------------------------------------+"
uninstall-all: $(addprefix uninstall-,$(ADAPTERS)) uninstall-base
@echo "+-----------------------------------------------------------------+"
@echo "| Uninstalled all components."
@echo "+-----------------------------------------------------------------+"
uninstall-%:
@PA='$(subst uninstall-,,$@)'; \
if [ -e install-$$PA.log ] ; then \
cat install-$$PA.log | xargs rm -f || exit 1; \
rm install-$$PA.log ; \
echo "+-----------------------------------------------------------------+" ; \
echo "| Uninstalled component '$$PA'." ; \
echo "+-----------------------------------------------------------------+" ; \
else \
echo "+-----------------------------------------------------------------+" ; \
echo "| Skipped component '$$PA' (install log does not exist)" ; \
echo "+-----------------------------------------------------------------+" ; \
fi
clean:
python setup.py clean --all
@echo "+-----------------------------------------------------------------+"
@echo "| Clean ok (keep install log files for uninsallation)."
@echo "+-----------------------------------------------------------------+"
remuco-source-0.9.6/adapter/amarok/remuco-amarok 0000755 0000000 0000000 00000006346 11700415064 021645 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter for Amarok, implemented as an executable script."""
import dbus
from dbus.exceptions import DBusException
import gobject
import remuco
from remuco import log
# =============================================================================
# player adapter
# =============================================================================
class AmarokAdapter(remuco.MPRISAdapter):
def __init__(self):
remuco.MPRISAdapter.__init__(self, "amarok", "Amarok",
mime_types=remuco.MIMETYPES_AUDIO,
rating=True)
self.__am = None
def start(self):
remuco.MPRISAdapter.start(self)
try:
bus = dbus.SessionBus()
proxy = bus.get_object("org.kde.amarok", "/amarok/MainWindow")
self.__am = dbus.Interface(proxy, "org.kde.KMainWindow")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
def stop(self):
remuco.MPRISAdapter.stop(self)
self.__am = None
def poll(self):
remuco.MPRISAdapter.poll(self)
# amarok does not signal change in shuffle state
self._poll_status()
# =========================================================================
# control interface
# =========================================================================
def ctrl_rate(self, rating):
rating = min(rating, 5)
rating = max(rating, 1)
action = "rate%s" % rating
try:
self.__am.activateAction(action)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_toggle_shuffle(self):
remuco.MPRISAdapter.ctrl_toggle_shuffle(self)
# amarok does not signal change in shuffle state
gobject.idle_add(self._poll_status)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = AmarokAdapter()
mg = remuco.Manager(pa, dbus_name="org.mpris.amarok")
mg.run()
remuco-source-0.9.6/adapter/amarok14/install-check.py 0000644 0000000 0000000 00000000576 11700415064 022414 0 ustar root root 0000000 0000000 import sys
try:
import eyeD3
except ImportError, e:
print("")
print("+-----------------------------------------------------------------+")
print("| Unsatisfied Python requirement: %s." % e)
print("| Please install the missing module and then retry.")
print("+-----------------------------------------------------------------+")
print("")
sys.exit(1) remuco-source-0.9.6/adapter/amarok14/remuco-amarok14 0000755 0000000 0000000 00000031105 11700415064 022146 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Amarok1.4 adapter for Remuco, implemented as an executable script."""
import commands
import os
import os.path
import sqlite3
import eyeD3
import gobject
import remuco
from remuco import log
# =============================================================================
# constants
# =============================================================================
IA_JUMP = remuco.ItemAction("Play Now")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
PLAYLIST_ACTIONS = (IA_JUMP, IA_REMOVE)
IA_ADD = remuco.ItemAction("Add to playlist", multiple=True)
MLIB_ITEM_ACTIONS = (IA_ADD,)
SEARCH_MASK = [ "Artist", "Album", "Title" ]
AMAROK_DB = os.path.expanduser('~/.kde/share/apps/amarok/collection.db')
# =============================================================================
# player adapter
# =============================================================================
class Amarok14Adapter(remuco.PlayerAdapter):
lastpath = ""
search_list = []
db = None
def __init__(self):
remuco.PlayerAdapter.__init__(self, "Amarok 1.4",
playback_known=True,
volume_known=True,
progress_known=True,
shuffle_known=True,
repeat_known=True,
mime_types=remuco.MIMETYPES_AUDIO,
max_rating=10,
poll=10,
search_mask=SEARCH_MASK
)
def start(self):
remuco.PlayerAdapter.start(self)
self.db = sqlite3.connect(AMAROK_DB).cursor()
def stop(self):
remuco.PlayerAdapter.stop(self)
self.db = None
def poll(self):
if not self._check_running():
return # manager will stop us
import random
current_state = self._amarok("player randomModeStatus")
if(current_state == "true"):
self.update_shuffle(True)
else:
self.update_shuffle(False)
current_state = self._amarok("player repeatPlaylistStatus")
if(current_state == "true"):
self.update_repeat(True)
else:
self.update_repeat(False)
volume = self._amarok("player getVolume", 50)
self.update_volume(volume)
playing = self._amarok("player isPlaying")
if playing == "true":
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_PAUSE)
#~ Get and calculate total time and progress
current = self._amarok("player currentTime")
if(len(current.split(":")) == 2):
cmin, csec = current.split(":")
else:
cmin, csec = 0,0
total = self._amarok("player totalTime")
if(len(total.split(":")) == 2):
tmin, tsec = total.split(":")
else:
tmin, tsec = 0,0
cmin, csec, tmin, tsec = int(cmin), int(csec), int(tmin), int(tsec)
current = (cmin*60)+csec
total = (tmin*60)+tsec
self.update_progress(progress=current, length=total)
curpath = self._amarok("player path")
if(self.lastpath != curpath and len(curpath) != 0):
info = {}
info[remuco.INFO_ARTIST] = self._amarok("player artist")
info[remuco.INFO_ALBUM] = self._amarok("player album")
info[remuco.INFO_TITLE] = self._amarok("player title")
info[remuco.INFO_GENRE] = self._amarok("player genre")
info[remuco.INFO_YEAR] = self._amarok("player year")
info[remuco.INFO_BITRATE] = int(self._amarok("player bitrate"))
info[remuco.INFO_RATING] = self._amarok("player rating")
self.update_item("", info, self.find_image(curpath, True))
self.lastpath = curpath
# =========================================================================
# control interface
# =========================================================================
def ctrl_rate(self, rate):
self._amarok("player setRating %s" % (rate))
def ctrl_volume(self, direction):
current = int(self._amarok("player getVolume", 50))
if(direction == 1 ):
self._amarok("player setVolume %s" % (current+10))
elif(direction == -1):
self._amarok("player setVolume %s" % (current-10))
elif(direction == 0):
self._amarok("player mute")
def ctrl_toggle_shuffle(self):
current_state = self._amarok("player randomModeStatus")
if(current_state == "true"):
self._amarok("player enableRandomMode false")
self.update_shuffle(False)
else:
self._amarok("player enableRandomMode true")
self.update_shuffle(True)
def ctrl_toggle_repeat(self):
current_state = self._amarok("player repeatPlaylistStatus")
if(current_state == "true"):
self._amarok("player enableRepeatPlaylist false")
self.update_repeat(False)
else:
self._amarok("player enableRepeatPlaylist true")
self.update_repeat(True)
def ctrl_toggle_playing(self):
self._amarok("player playPause")
gobject.idle_add(self.poll)
def ctrl_next(self):
self._amarok("player next")
gobject.timeout_add(150, self.poll)
def ctrl_previous(self):
self._amarok("player prev")
gobject.timeout_add(150, self.poll)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
path = os.path.expanduser("~/.cache/current.m3u")
self._amarok('playlist saveM3u "%s" false' % path)
f = open(path)
tag = eyeD3.Tag()
while 1:
line = f.readline()
if not line: break
if line.find("#") == -1:
tag.link(line.rstrip())
reply.ids.append(len(reply.names))
reply.names.append("%s - %s" % (tag.getArtist(), tag.getTitle()))
f.close
reply.item_actions = PLAYLIST_ACTIONS
reply.send()
def request_search(self, reply, query):
search = []
fields = ["artist.name", "album.name", "tags.title"]
for q, f in zip(query, fields):
if q:
search.append('lower(%s) like "%%%s%%"' % (f, q))
if not search:
reply.send()
return
sql = ("select tags.title as title, artist.name as artist, album.name "
"as album, tags.url as url from tags join artist on "
"(tags.artist = artist.id) join album on "
"(tags.album = album.id) where %s" % " and ".join(search))
res = self.db.execute(sql)
id = 0
self.search_list = []
for r in res:
self.search_list.append("")
self.search_list[len(reply.names)] = r[3]
reply.ids.append(len(reply.names))
reply.names.append(" ".join(r[0:3]))
reply.item_actions = MLIB_ITEM_ACTIONS
reply.send()
def request_mlib(self, reply, path):
reply.nested, reply.names, reply.ids = [], [], []
if not path:
sql = "select name from artist order by name asc"
res = self.db.execute(sql)
for r in res:
reply.nested.append(r[0])
elif(len(path) == 1):
sql = ('select distinct(album.name) from album '
'join tags on (tags.album = album.id) '
'join artist on (tags.artist = artist.id) '
'where artist.name = "%s"' % path[0])
res = self.db.execute(sql)
for r in res:
if(len(r[0]) == 0):
reply.nested.append("_no_album_")
else:
reply.nested.append(r[0])
elif(len(path) == 2):
album = (path[1] != "_no_album_" and
' and album.name = "%s"' % path[1] or "")
sql = ('select tags.title from tags '
'join album on (tags.album = album.id) '
'join artist on (tags.artist = artist.id) '
'where artist.name = "%s"%s '
'order by tags.url' % (path[0], album))
res = self.db.execute(sql)
for r in res:
reply.ids.append(len(reply.names))
reply.names.append(r[0])
reply.item_actions = MLIB_ITEM_ACTIONS
reply.send()
# =========================================================================
# Actions
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if(action_id == IA_JUMP.id):
self._amarok("playlist playByIndex %s" % (positions[0]))
elif(action_id == IA_REMOVE.id):
for i, v in enumerate(positions):
self._amarok("playlist removeByIndex %s" % (v))
else:
print "Unknown action %s" %s (action_id)
def action_search_item(self, action_id, positions, ids):
path = []
if action_id == IA_ADD.id: #Add to playlist
for pos in positions:
# Ugly hack but it works. Amarok prefixes all paths with .
# (period). Don't know why but this is a quickfix
path.append('"%s"' %
(os.path.abspath("/%s" %(self.search_list[pos]))))
self._amarok("playlist addMediaList [ %s ]" % " ".join(path))
def action_mlib_item(self, action_id, path, positions, ids):
if action_id == IA_ADD.id:
search = []
if(len(path[0]) > 0):
search.append('artist.name = "%s"' % path[0])
if(len(path[1]) > 0 and path[1] != "_no_album_"):
search.append('album.name = "%s"' % path[1])
sql = ('select tags.url from tags '
'join album on (album.id = tags.album) '
'join artist on (artist.id = tags.artist) '
'where %s '
'order by tags.url' % " AND ".join(search))
res = self.db.execute(sql)
list = []
for r in res:
list.append(r[0])
path = []
if(len(list) > 0):
for pos in positions:
# Ugly hack but it works. Amarok prefixes all paths with .
# (period). Don't know why but this is a quickfix
path.append(' "%s"' % os.path.abspath("/%s" % list[pos]))
self._amarok("playlist addMediaList [ %s ]" % " ".join(path))
# =========================================================================
# internal methods
# =========================================================================
def _amarok(self, cmd, default=""):
"""Shortcut for running command 'dcop amarok ...'."""
ret, out = commands.getstatusoutput("dcop amarok %s" % cmd)
if ret != os.EX_OK:
log.warning("'dcop amarok %s' failed (%s)" % (cmd, out))
return default
else:
return out
# =============================================================================
# main
# =============================================================================
def run_check():
"""Check if Amarok is running."""
return commands.getstatusoutput("dcop amarok")[0] == 0
if __name__ == '__main__':
pa = Amarok14Adapter()
mg = remuco.Manager(pa, poll_fn=run_check)
mg.run()
remuco-source-0.9.6/adapter/audacious/remuco-audacious 0000755 0000000 0000000 00000015240 11700415064 023044 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter for Audacious, implemented as an executable script."""
import dbus
from dbus.exceptions import DBusException
import gobject
import remuco
from remuco import log
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
PLAYLIST_ACTIONS = (IA_JUMP,)
# =============================================================================
# player adapter
# =============================================================================
class AudaciousAdapter(remuco.MPRISAdapter):
def __init__(self):
remuco.MPRISAdapter.__init__(self, "audacious", "Audacious",
mime_types=remuco.MIMETYPES_AUDIO,
extra_playlist_actions=PLAYLIST_ACTIONS)
self.__ad = None
self.__poll_for_repeat_and_shuffle = False
def start(self):
remuco.MPRISAdapter.start(self)
try:
bus = dbus.SessionBus()
proxy = bus.get_object("org.atheme.audacious", "/org/atheme/audacious")
self.__ad = dbus.Interface(proxy, "org.atheme.audacious")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
def stop(self):
remuco.MPRISAdapter.stop(self)
self.__ad = None
def poll(self):
remuco.MPRISAdapter.poll(self)
if self.__poll_for_repeat_and_shuffle:
self.__poll_repeat()
self.__poll_shuffle()
def __poll_repeat(self):
# used if audacious still does not provide this by signal "StatusChange"
try:
repeat = bool(self.__ad.Repeat())
except DBusException, e:
log.warning("dbus error: %s" % e)
repeat = False
self._repeat = repeat
self.update_repeat(repeat)
def __poll_shuffle(self):
# used if audacious still does not provide this by signal "StatusChange"
try:
shuffle = bool(self.__ad.Shuffle())
except DBusException, e:
log.warning("dbus error: %s" % e)
shuffle = False
self._shuffle = shuffle
self.update_shuffle(shuffle)
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_repeat(self):
# this works in more Audacious versions than the related MPRIS control
try:
self.__ad.ToggleRepeat(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
if self.__poll_for_repeat_and_shuffle:
self.__poll_repeat()
def ctrl_toggle_shuffle(self):
# this works in more Audacious versions than the related MPRIS control
try:
self.__ad.ToggleShuffle(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
if self.__poll_for_repeat_and_shuffle:
self.__poll_shuffle()
# =========================================================================
# actions interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
try:
self.__ad.Jump(positions[0],
reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
else:
remuco.MPRISAdapter.action_playlist_item(self, action_id,
positions, ids)
# =========================================================================
# internal methods which must be adapted to provide MPRIS conformity
# =========================================================================
def _notify_status(self, status):
if isinstance(status, int):
# audacious only provides playback status here (Audacious 1.5)
self.__poll_for_repeat_and_shuffle = True
status = (status, self._shuffle, self._repeat, self._repeat)
else:
# it looks like audacious has fixed its MPRIS interface
self.__poll_for_repeat_and_shuffle = False
remuco.MPRISAdapter._notify_status(self, status)
def _notify_track(self, track):
# audacious provides length in 'length', not in 'time' or 'mtime'
# (fixed by http://hg.atheme.org/audacious/rev/e6caff433c68)
if "length" in track and not "mtime" in track:
track["mtime"] = track["length"]
# audacious 1.5 provides length in 'URI', not in 'location'
if "URI" in track:
track["location"] = track["URI"]
remuco.MPRISAdapter._notify_track(self, track)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = AudaciousAdapter()
mg = remuco.Manager(pa, dbus_name="org.mpris.audacious")
mg.run()
remuco-source-0.9.6/adapter/banshee/remuco-banshee 0000755 0000000 0000000 00000027730 11700415064 022133 0 ustar root root 0000000 0000000 #!/usr/bin/env python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Banshee player adapter for Remuco, implemented as an executable script."""
import os.path
import dbus
from dbus.exceptions import DBusException
import gobject
from xdg.BaseDirectory import xdg_cache_home as xdg_cache
import remuco
from remuco import log
# =============================================================================
# banshee dbus names
# =============================================================================
DBUS_NAME = "org.bansheeproject.Banshee"
DBUS_PATH_ENGINE = "/org/bansheeproject/Banshee/PlayerEngine"
DBUS_IFACE_ENGINE = "org.bansheeproject.Banshee.PlayerEngine"
DBUS_PATH_CONTROLLER = "/org/bansheeproject/Banshee/PlaybackController"
DBUS_IFACE_CONTROLLER = "org.bansheeproject.Banshee.PlaybackController"
# =============================================================================
# banshee player adapter
# =============================================================================
class BansheeAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "Banshee",
max_rating=5,
playback_known=True,
volume_known=True,
repeat_known=True,
shuffle_known=True,
progress_known=True)
self.__dbus_signal_handler = ()
self.__repeat = False
self.__shuffle = False
self.__volume = 0
self.__progress = 0
self.__progress_length = 0
log.debug("init done")
def start(self):
remuco.PlayerAdapter.start(self)
try:
bus = dbus.SessionBus()
proxy = bus.get_object(DBUS_NAME, DBUS_PATH_ENGINE)
self.__bse = dbus.Interface(proxy, DBUS_IFACE_ENGINE)
proxy = bus.get_object(DBUS_NAME, DBUS_PATH_CONTROLLER)
self.__bsc = dbus.Interface(proxy, DBUS_IFACE_CONTROLLER)
except DBusException, e:
raise StandardError("dbus error: %s" % e)
try:
self.__dbus_signal_handler = (
self.__bse.connect_to_signal("EventChanged",
self.__notify_event),
self.__bse.connect_to_signal("StateChanged",
self.__notify_playback),
)
except DBusException, e:
raise StandardError("dbus error: %s" % e)
try:
self.__bse.GetCurrentTrack(reply_handler=self.__notify_track,
error_handler=self.__dbus_error)
self.__bse.GetCurrentState(reply_handler=self.__notify_playback,
error_handler=self.__dbus_error)
self.__bse.GetVolume(reply_handler=self.__notify_volume,
error_handler=self.__dbus_error)
except DBusException, e:
# this is not necessarily a fatal error
log.warning("dbus error: %s" % e)
log.debug("start done")
def stop(self):
remuco.PlayerAdapter.stop(self)
for handler in self.__dbus_signal_handler:
handler.remove()
self.__dbus_signal_handler = ()
self.__bsc = None
self.__bse = None
log.debug("stop done")
def poll(self):
self.__poll_repeat_shuffle()
self.__poll_progress()
def __poll_repeat_shuffle(self):
try:
self.__bsc.GetRepeatMode(reply_handler=self.__notify_repeat,
error_handler=self.__dbus_error)
self.__bsc.GetShuffleMode(reply_handler=self.__notify_shuffle,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def __poll_progress(self):
try:
self.__bse.GetPosition(reply_handler=self.__notify_progress,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
try:
self.__bse.TogglePlaying(reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_next(self):
try:
self.__bsc.Next(False,
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_previous(self):
try:
self.__bsc.Previous(False,
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_seek(self, direction):
if self.__progress_length == 0:
return
progress_new = self.__progress + direction * 5
progress_new = min(progress_new, self.__progress_length)
progress_new = max(progress_new, 0)
try:
self.__bse.SetPosition(dbus.UInt32(progress_new * 1000),
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
else:
# poll with a small delay, otherwise we get 0 as progress
gobject.timeout_add(100, self.__poll_progress)
def ctrl_rate(self, rating):
try:
self.__bse.SetRating(dbus.Byte(rating),
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_volume(self, direction):
if direction == 0:
volume = 0
else:
volume = self.__volume + direction * 5
volume = min(volume, 100)
volume = max(volume, 0)
try:
self.__bse.SetVolume(dbus.UInt16(volume),
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
self.__notify_event("volume", None, None)
def ctrl_toggle_repeat(self):
try:
self.__bsc.SetRepeatMode(int(not self.__repeat),
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
self.__poll_repeat_shuffle()
def ctrl_toggle_shuffle(self):
mode = self.__shuffle and "off" or self.config.getx("shuffle", "song")
try:
self.__bsc.SetShuffleMode(mode,
reply_handler=self.__dbus_ignore,
error_handler=self.__dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
self.__poll_repeat_shuffle()
# =========================================================================
# internal methods
# =========================================================================
def __notify_event(self, event, message, buff_percent):
try:
if event == "startofstream" or event == "trackinfoupdated":
self.__bse.GetCurrentTrack(reply_handler=self.__notify_track,
error_handler=self.__dbus_error)
elif event == "volume":
self.__bse.GetVolume(reply_handler=self.__notify_volume,
error_handler=self.__dbus_error)
else:
log.debug("event: %s (%s)" %(event, message))
except DBusException, e:
log.warning("dbus error: %s" % e)
def __notify_playback(self, state):
log.debug("state: %s" % state)
if state == "playing":
playback = remuco.PLAYBACK_PLAY
elif state == "idle":
playback = remuco.PLAYBACK_STOP
self.update_item(None, None, None)
else:
playback = remuco.PLAYBACK_PAUSE
self.update_playback(playback)
def __notify_volume(self, volume):
self.__volume = volume
self.update_volume(volume)
def __notify_track(self, track):
id = track.get("URI")
if not id:
self.update_item(None, None, None)
self.__progress_length = 0
return
info = {}
info[remuco.INFO_TITLE] = track.get("name")
info[remuco.INFO_ARTIST] = track.get("artist")
info[remuco.INFO_ALBUM] = track.get("album")
info[remuco.INFO_RATING] = track.get("rating", 0)
self.__progress_length = int(track.get("length", 0))
img = None
art_id = track.get("artwork-id")
if art_id:
file = "%s/album-art/%s.jpg" % (xdg_cache, art_id)
if os.path.isfile(file):
img = file
else:
img = self.find_image(id)
log.debug("track: %s" % info)
self.update_item(id, info, img)
def __notify_repeat(self, repeat):
self.__repeat = repeat > 0
self.update_repeat(self.__repeat)
def __notify_shuffle(self, shuffle):
self.__shuffle = shuffle != "off"
self.update_shuffle(self.__shuffle)
def __notify_progress(self, ms):
self.__progress = ms // 1000
self.update_progress(self.__progress, self.__progress_length)
def __dbus_error(self, error):
""" DBus error handler. """
if self.__bsc is None:
return # do not log errors when not stopped already
log.warning("dbus error: %s" % error)
def __dbus_ignore(self):
""" DBus reply handler for methods without reply. """
pass
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
ba = BansheeAdapter()
mg = remuco.Manager(ba, dbus_name=DBUS_NAME)
mg.run()
remuco-source-0.9.6/adapter/clementine/remuco-clementine 0000755 0000000 0000000 00000003364 11700415064 023364 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter for Clementine, implemented as an executable script."""
import remuco
# =============================================================================
# player adapter
# =============================================================================
class ClementineAdapter(remuco.MPRISAdapter):
def __init__(self):
remuco.MPRISAdapter.__init__(self, "clementine", "Clementine",
mime_types=remuco.MIMETYPES_AUDIO)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = ClementineAdapter()
mg = remuco.Manager(pa, dbus_name="org.mpris.clementine")
mg.run()
remuco-source-0.9.6/adapter/exaile/PLUGININFO 0000644 0000000 0000000 00000000160 11700415064 020533 0 ustar root root 0000000 0000000 Version="0.9.6"
Authors=["Remuco team"]
Name="Remuco"
Description="Exaile adapter for Remuco (and vice versa)."
remuco-source-0.9.6/adapter/exaile/__init__.py 0000644 0000000 0000000 00000054364 11700415064 021266 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Exaile adapter for Remuco, implemented as an Exaile plugin."""
from __future__ import with_statement
import os.path
import re
import gobject
import xl.event
import xl.settings
try:
from xl.cover import NoCoverFoundException # exaile 3.0
except ImportError:
class NoCoverFoundException(Exception): pass # exaile 3.1
try:
from xl import covers # exaile 3.2
except ImportError:
pass # exaile 3.1
import remuco
from remuco import log
# =============================================================================
# constants
# =============================================================================
PLAYLISTS_SMART = "Smart playlists"
PLAYLISTS_CUSTOM = "Custom playlists"
PLAYLISTS_OPEN = "Open playlists"
SEARCH_MASK = ("Artist", "Album", "Title", "Genre")
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
IA_ENQUEUE = remuco.ItemAction("Enqueue", multiple=True)
IA_APPEND = remuco.ItemAction("Append", multiple=True)
IA_REPLACE = remuco.ItemAction("Reset playlist", multiple=True)
IA_NEW_PLAYLIST = remuco.ItemAction("New playlist", multiple=True)
LA_OPEN = remuco.ListAction("Open")
LA_ACTIVATE = remuco.ListAction("Activate")
LA_CLOSE = remuco.ListAction("Close")
PLAYLIST_ACTIONS = (IA_JUMP, IA_REMOVE, IA_ENQUEUE)
QUEUE_ACTIONS = (IA_JUMP, IA_REMOVE)
MLIB_ITEM_ACTIONS = (IA_JUMP, IA_ENQUEUE, IA_APPEND, IA_REPLACE, IA_REMOVE,
IA_NEW_PLAYLIST)
MLIB_LIST_ACTIONS = (LA_OPEN,)
MLIB_LIST_OPEN_ACTIONS = (LA_ACTIVATE, LA_CLOSE)
SEARCH_ACTIONS = (IA_ENQUEUE, IA_APPEND, IA_REPLACE, IA_NEW_PLAYLIST)
# =============================================================================
# player adapter
# =============================================================================
class ExaileAdapter(remuco.PlayerAdapter):
def __init__(self, exaile):
remuco.PlayerAdapter.__init__(self, "Exaile",
max_rating=5,
playback_known=True,
volume_known=True,
repeat_known=True,
shuffle_known=True,
progress_known=True,
search_mask=SEARCH_MASK)
self.__ex = exaile
def start(self):
remuco.PlayerAdapter.start(self)
for event in ("playback_track_start", "playback_track_end"):
xl.event.add_callback(self.__notify_track_change, event)
for event in ("playback_player_end", "playback_player_start",
"playback_toggle_pause"):
xl.event.add_callback(self.__notify_playback_change, event)
self.__update_track(self.__ex.player.current)
self.__update_position()
self.__update_playback()
# other updates via poll()
log.debug("remuco exaile adapter started")
def stop(self):
remuco.PlayerAdapter.stop(self)
xl.event.remove_callback(self.__notify_track_change)
xl.event.remove_callback(self.__notify_playback_change)
log.debug("remuco exaile adapter stopped")
def poll(self):
self.__update_repeat_and_shuffle()
self.__update_volume()
self.__update_progress()
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
if self.__ex.player.is_playing() or self.__ex.player.is_paused():
self.__ex.player.toggle_pause()
else:
self.__ex.queue.play()
# when playing after stopped, the 'playback_player_start' is missed
gobject.idle_add(self.__update_playback)
def ctrl_toggle_repeat(self):
repeat = not self.__ex.queue.current_playlist.is_repeat()
self.__ex.queue.current_playlist.set_repeat(repeat)
gobject.idle_add(self.__update_repeat_and_shuffle)
def ctrl_toggle_shuffle(self):
shuffle = not self.__ex.queue.current_playlist.is_random()
self.__ex.queue.current_playlist.set_random(shuffle)
gobject.idle_add(self.__update_repeat_and_shuffle)
def ctrl_next(self):
"""Play the next item.
@note: Override if it is possible and makes sense.
"""
self.__ex.queue.next()
def ctrl_previous(self):
"""Play the previous item.
@note: Override if it is possible and makes sense.
"""
self.__ex.queue.prev()
def ctrl_seek(self, direction):
"""Seek forward or backward some seconds.
The number of seconds to seek should be reasonable for the current
item's length (if known).
If the progress of the current item is known, it should get
synchronized immediately with clients by calling update_progress().
@param direction:
* -1: seek backward
* +1: seek forward
@note: Override if it is possible and makes sense.
"""
if not self.__ex.player.is_playing():
return
track = self.__ex.player.current
if not track:
return
pos = self.__ex.player.get_time()
pos = pos + direction * 5
if self.__ex.get_version() < "0.3.1":
pos = min(pos, track.get_duration())
else:
pos = min(pos, int(track.get_tag_raw("__length")))
pos = max(pos, 0)
self.__ex.player.seek(pos)
gobject.idle_add(self.__update_progress)
def ctrl_rate(self, rating):
track = self.__ex.player.current
if not track:
return
track.set_rating(rating)
def ctrl_volume(self, direction):
if direction == 0:
self.__ex.player.set_volume(0)
else:
volume = self.__ex.player.get_volume() + direction * 5
volume = min(volume, 100)
volume = max(volume, 0)
self.__ex.player.set_volume(volume)
gobject.idle_add(self.__update_volume)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
tracks = self.__ex.queue.current_playlist.get_ordered_tracks()
reply.ids, reply.names = self.__tracklist_to_itemlist(tracks)
reply.item_actions = PLAYLIST_ACTIONS
reply.send()
def request_queue(self, reply):
tracks = self.__ex.queue.get_tracks()
reply.ids, reply.names = self.__tracklist_to_itemlist(tracks)
reply.item_actions = QUEUE_ACTIONS
reply.send()
def request_mlib(self, reply, path):
if not path:
reply.nested = (PLAYLISTS_OPEN, PLAYLISTS_SMART, PLAYLISTS_CUSTOM)
reply.send()
elif path[0] == PLAYLISTS_SMART:
if len(path) == 1:
reply.nested = self.__ex.smart_playlists.list_playlists()
reply.list_actions = MLIB_LIST_ACTIONS
else:
pl = self.__ex.smart_playlists.get_playlist(path[1])
reply.ids, reply.names = ["XXX"], ["This is a dynamic playlist!"]
elif path[0] == PLAYLISTS_CUSTOM:
if len(path) == 1:
reply.nested = self.__ex.playlists.list_playlists()
reply.list_actions = MLIB_LIST_ACTIONS
else:
pl = self.__ex.playlists.get_playlist(path[1])
tracks = pl.get_ordered_tracks()
reply.ids, reply.names = self.__tracklist_to_itemlist(tracks)
elif path[0] == PLAYLISTS_OPEN:
if len(path) == 1:
plo_list, pln_list = self.__get_open_playlists()
reply.nested = pln_list
reply.list_actions = MLIB_LIST_OPEN_ACTIONS
else:
pl, i = self.__get_open_playlist(path)
tracks = pl.get_ordered_tracks()
reply.ids, reply.names = self.__tracklist_to_itemlist(tracks)
reply.item_actions = MLIB_ITEM_ACTIONS
else:
log.error("** BUG ** unexpected mlib path")
reply.send()
def request_search(self, reply, query):
if self.__ex.get_version() < "0.3.1":
tracks = None
for key, val in zip(SEARCH_MASK, query):
val = val.strip()
if val:
expr = "%s=%s" % (key, val)
tracks = self.__ex.collection.search(expr, tracks=tracks)
if tracks is None: # empty query, return _all_ tracks
tracks = self.__ex.collection.search("", tracks=tracks)
else:
tml = []
for key, val in zip(SEARCH_MASK, query):
val = val.strip()
if val:
sexpr = "%s=%s" % (key.lower(), val)
tml.append(xl.trax.TracksMatcher(sexpr, case_sensitive=False))
tracks = xl.trax.search_tracks(self.__ex.collection, tml)
reply.ids, reply.names = self.__tracklist_to_itemlist(tracks)
reply.item_actions = SEARCH_ACTIONS
reply.send()
# =========================================================================
# action interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if self.__handle_generic_item_action(action_id, ids):
pass # we are done
elif action_id == IA_JUMP.id:
track = self.__ex.collection.get_track_by_loc(ids[0])
self.__ex.queue.next(track=track)
self.__ex.queue.current_playlist.set_current_pos(positions[0])
elif action_id == IA_REMOVE.id:
self.__remove_tracks_from_playlist(positions)
else:
log.error("** BUG ** unexpected playlist item action")
def action_queue_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
track = self.__ex.collection.get_track_by_loc(ids[0])
self.__ex.queue.next(track=track)
self.__remove_tracks_from_playlist(positions, pl=self.__ex.queue)
elif action_id == IA_REMOVE.id:
self.__remove_tracks_from_playlist(positions, pl=self.__ex.queue)
else:
log.error("** BUG ** unexpected queue item action")
def action_mlib_item(self, action_id, path, positions, ids):
if self.__handle_generic_item_action(action_id, ids):
pass # we are done
elif action_id == IA_JUMP.id:
self.action_mlib_list(LA_ACTIVATE.id, path)
track = self.__ex.collection.get_track_by_loc(ids[0])
self.__ex.queue.next(track=track)
self.__ex.queue.current_playlist.set_current_pos(positions[0])
elif action_id == IA_REMOVE.id:
pl, i = self.__get_open_playlist(path)
self.__remove_tracks_from_playlist(positions, pl=pl)
else:
log.error("** BUG ** unexpected mlib item action")
def action_mlib_list(self, action_id, path):
if action_id == LA_ACTIVATE.id:
if path[0] == PLAYLISTS_OPEN:
pl, i = self.__get_open_playlist(path)
self.__ex.gui.main.playlist_notebook.set_current_page(i)
else:
log.error("** BUG ** unexpected mlib path %s" % path)
elif action_id == LA_OPEN.id:
if path[0] == PLAYLISTS_SMART:
pl = self.__ex.smart_playlists.get_playlist(path[1])
pl = pl.get_playlist(self.__ex.collection)
self.__ex.gui.main.add_playlist(pl)
elif path[0] == PLAYLISTS_CUSTOM:
pl = self.__ex.playlists.get_playlist(path[1])
self.__ex.gui.main.add_playlist(pl)
else:
log.error("** BUG ** unexpected mlib path %s" % path)
elif action_id == LA_CLOSE.id:
pl, i = self.__get_open_playlist(path)
nb = self.__ex.gui.main.playlist_notebook
nb.remove_page(i)
else:
log.error("** BUG ** unexpected mlib list action")
def action_search_item(self, action_id, positions, ids):
if self.__handle_generic_item_action(action_id, ids):
pass # we are done
else:
log.error("** BUG ** unexpected search item action")
# =========================================================================
# callbacks
# =========================================================================
def __notify_track_change(self, type, object, data):
"""Callback on track change."""
self.__update_track(data)
self.__update_progress()
self.__update_position()
def __notify_playback_change(self, type, object, data):
"""Callback on playback change."""
self.__update_playback()
# =========================================================================
# helper methods
# =========================================================================
def __update_playback(self):
"""Update playback state."""
if self.__ex.player.is_playing():
playback = remuco.PLAYBACK_PLAY
elif self.__ex.player.is_paused():
playback = remuco.PLAYBACK_PAUSE
else:
playback = remuco.PLAYBACK_STOP
self.update_playback(playback)
def __update_repeat_and_shuffle(self):
"""Update repeat and shuffle state."""
repeat = self.__ex.queue.current_playlist.is_repeat()
self.update_repeat(repeat)
shuffle = self.__ex.queue.current_playlist.is_random()
self.update_shuffle(shuffle)
def __update_track(self, track):
"""Update meta information of current track."""
def get_tag(key):
if self.__ex.get_version() < "0.3.1":
val = track.get_tag(key)
else: # Exaile >= 0.3.1
val = track.get_tag_raw(key)
return val and (isinstance(val, list) and val[0] or val) or None
id = None
info = None
img = None
if track:
id = track.get_loc_for_io()
info = {}
info[remuco.INFO_ARTIST] = get_tag("artist")
info[remuco.INFO_ALBUM] = get_tag("album")
info[remuco.INFO_TITLE] = get_tag("title")
info[remuco.INFO_YEAR] = get_tag("date")
info[remuco.INFO_GENRE] = get_tag("genre")
info[remuco.INFO_RATING] = track.get_rating()
if self.__ex.get_version() < "0.3.1":
info[remuco.INFO_BITRATE] = track.get_bitrate().replace('k','')
info[remuco.INFO_LENGTH] = track.get_duration()
try:
img = self.__ex.covers.get_cover(track)
except NoCoverFoundException: # exaile 0.3.0 only
pass
else: # Exaile >= 0.3.1
info[remuco.INFO_BITRATE] = get_tag("__bitrate") // 1000
info[remuco.INFO_LENGTH] = int(get_tag("__length"))
if self.__ex.get_version() < "0.3.2":
idata = self.__ex.covers.get_cover(track, set_only=True)
else:
idata = covers.MANAGER.get_cover(track, set_only=True)
if idata:
img = os.path.join(self.config.cache, "exaile.cover")
with open(img, "w") as fp:
fp.write(idata)
if not img and track.local_file_name(): # loc_for_io may be != UTF-8
img = self.find_image(track.local_file_name())
self.update_item(id, info, img)
def __update_volume(self):
"""Update volume."""
self.update_volume(self.__ex.player.get_volume())
def __update_progress(self):
"""Update play progress of current track."""
track = self.__ex.player.current
if not track:
len = 0
pos = 0
else:
if self.__ex.get_version() < "0.3.1":
len = track.get_duration()
else: # Exaile >= 0.3.1
len = int(track.get_tag_raw("__length"))
pos = self.__ex.player.get_time()
self.update_progress(pos, len)
def __update_position(self):
"""Update current playlist position."""
pl = self.__ex.queue.current_playlist
self.update_position(pl.get_current_pos())
def __handle_generic_item_action(self, action_id, ids):
"""Process generic item actions.
Actions: IA_ENQUEUE, IA_APPEND, IA_REPLACE, IA_NEW_PLAYLIST
Generic item actions are processed independent of the list containing
the items (playlist, queue, mlib or search result).
@return: True if action has been handled, False other wise
"""
handled = True
tracks = self.__ex.collection.get_tracks_by_locs(ids)
tracks = [t for t in tracks if t is not None]
if action_id == IA_ENQUEUE.id:
self.__ex.queue.add_tracks(tracks)
elif action_id == IA_APPEND.id:
self.__ex.queue.current_playlist.add_tracks(tracks)
elif action_id == IA_REPLACE.id:
self.__ex.queue.current_playlist.set_tracks(tracks)
elif action_id == IA_NEW_PLAYLIST.id:
self.__ex.gui.main.add_playlist()
self.__ex.queue.current_playlist.add_tracks(tracks)
else:
handled = False
return handled
def __tracklist_to_itemlist(self, tracks):
"""Convert a list if track objects to a Remuco item list."""
ids = []
names = []
for track in tracks:
# first, check if track is a SearchResultTrack (since Exaile 0.3.1)
track = hasattr(track, "track") and track.track or track
ids.append(track.get_loc_for_io())
if self.__ex.get_version() < "0.3.1":
artist = track.get_tag("artist")
title = track.get_tag("title")
else:
title = track.get_tag_raw("title")
artist = track.get_tag_raw("artist")
artist = artist and artist[0] or None
title = title and title[0] or None
name = "%s - %s" % (artist or "???", title or "???")
names.append(name)
return ids, names
def __get_open_playlists(self):
"""Get open playlists.
Returns 2 lists, one with the playlist objects and one with the
playlist names (formatted appropriately to handle duplicates).
"""
nb = self.__ex.gui.main.playlist_notebook
plo_list = []
pln_list = []
for i in range(nb.get_n_pages()):
plo = nb.get_nth_page(i).playlist
plo_list.append(plo)
if plo == self.__ex.queue.current_playlist:
num = "(%d)" % (i+1)
else:
num = "[%d]" % (i+1)
pln_list.append("%s %s" % (num, plo.get_name()))
return plo_list, pln_list
def __get_open_playlist(self, path):
"""Get a playlist object and tab number of an open playlist."""
plo_list, pln_list = self.__get_open_playlists()
i = int(re.match("[\(\[](\d+)[\)\]]", path[1]).group(1)) - 1
try:
return plo_list[i], i
except IndexError:
return plo_list[0], 0
def __remove_tracks_from_playlist(self, positions, pl=None):
"""Remove tracks from a playlist given their positions. """
if pl is None:
pl = self.__ex.queue.current_playlist
revpos = positions[:]
revpos.reverse()
for pos in revpos:
pl.remove_tracks(pos, pos)
# =============================================================================
# plugin interface
# =============================================================================
epa = None
def enable(exaile):
if exaile.loading:
xl.event.add_callback(_enable, "exaile_loaded")
else:
_enable("exaile_loaded", exaile, None)
def _enable(event, exaile, nothing):
global epa
epa = ExaileAdapter(exaile)
epa.start()
def disable(exaile):
global epa
if epa:
epa.stop()
epa = None
def teardown(exaile):
# teardown and disable are the same here
disable(exaile) remuco-source-0.9.6/adapter/fooplay/remuco-fooplay 0000755 0000000 0000000 00000005720 11700415064 022236 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""FooPlay adapter for Remuco, implemented as an executable script."""
import remuco
from remuco import log
class FooPlayAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "FooPlay",
playback_known=True,
volume_known=True)
def start(self):
remuco.PlayerAdapter.start(self)
log.debug("here we go")
def stop(self):
remuco.PlayerAdapter.stop(self)
log.debug("bye, turning off the light")
def poll(self):
import random
volume = random.randint(0,100)
self.update_volume(volume)
playing = random.randint(0,1)
if playing:
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_PAUSE)
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
log.debug("toggle FooPlay's playing status")
# ...
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
reply.ids = ["1", "2"]
reply.names = ["Joe - Joe's Song", "Sue - Sue's Song"]
reply.send()
# ...
# =============================================================================
# main (example startup using remuco.Manager)
# =============================================================================
if __name__ == '__main__':
pa = FooPlayAdapter() # create the player adapter
mg = remuco.Manager(pa)# # pass it to a manager
mg.run() # run the manager (blocks until interrupt signal)
remuco-source-0.9.6/adapter/gmusicbrowser/remuco-gmusicbrowser 0000755 0000000 0000000 00000012001 11700415064 024670 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Gmusicbrowser adapter for Remuco, implemented as an executable script.
By Sayan "Riju" Chakrabarti
"""
import dbus
from dbus.exceptions import DBusException
import remuco
from remuco import log
class GmusicbrowserAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "Gmusicbrowser",
progress_known=True,
playback_known=True,
volume_known=True)
self.__gm = None
self.__songLen = 0
self.__pos = 0
# Following is the value (100%) to which both the player
# volume and the client volume is set when either the adapter
# started OR the client is unmuted.
self.__vol = 100
def start(self):
remuco.PlayerAdapter.start(self)
log.debug("here we go")
try:
bus = dbus.SessionBus()
obj = bus.get_object("org.gmusicbrowser","/org/gmusicbrowser")
self.__gm = dbus.Interface(obj, "org.gmusicbrowser")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
# Set Current Song at start
self.poll()
# Increase player vol until its 100% to match client vol
for i in range(0,10):
self.__gm.RunCommand("IncVolume")
self.update_volume(self.__vol)
def stop(self):
remuco.PlayerAdapter.stop(self)
log.debug("bye, turning off the light")
self.__gm = None
def poll(self):
item = self.__gm.CurrentSong()
self.__songLen = int(item['length'])
self.__pos = int(self.__gm.GetPosition())
playing = self.__gm.Playing()
# TODO: item['track'] may give us path we can use to find cover art
self.update_item('', item, '')
self.update_progress(self.__pos, self.__songLen)
if playing:
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_PAUSE)
self.update_volume(self.__vol)
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
log.debug("toggle FooPlay's playing status")
self.__gm.RunCommand("PlayPause")
self.poll()
def ctrl_next(self):
self.__gm.RunCommand("NextSongInPlaylist")
self.poll()
def ctrl_previous(self):
self.__gm.RunCommand("PrevSongInPlaylist")
self.poll()
def ctrl_volume(self, direction):
if direction == 0:
self.__gm.RunCommand("TogMute")
if self.__vol != 0:
self.__vol = 0
else:
self.__vol = 100
for i in range(0,10):
self.__gm.RunCommand("IncVolume")
elif direction == -1:
self.__gm.RunCommand("DecVolume")
self.__vol = (self.__vol - 10) if self.__vol >= 10 else 0
else:
self.__gm.RunCommand("IncVolume")
self.__vol = (self.__vol + 10) if self.__vol <= 90 else 100
self.update_volume(self.__vol)
def ctrl_seek(self, direction):
if direction == -1:
self.__gm.RunCommand("Rewind 5")
else:
self.__gm.RunCommand("Forward 5")
self.poll()
# =========================================================================
# request interface
# =========================================================================
# NOT YET IMPLEMENTED IN GMUSICBROWSER
# =============================================================================
# main (example startup using remuco.Manager)
# =============================================================================
if __name__ == '__main__':
pa = GmusicbrowserAdapter() # create the player adapter
mg = remuco.Manager(pa,dbus_name="org.gmusicbrowser")# # pass it to a manager
mg.run() # run the manager (blocks until interrupt signal
remuco-source-0.9.6/adapter/mpd/install-check.py 0000644 0000000 0000000 00000000574 11700415064 021553 0 ustar root root 0000000 0000000 import sys
try:
import mpd
except ImportError, e:
print("")
print("+-----------------------------------------------------------------+")
print("| Unsatisfied Python requirement: %s." % e)
print("| Please install the missing module and then retry.")
print("+-----------------------------------------------------------------+")
print("")
sys.exit(1) remuco-source-0.9.6/adapter/mpd/remuco-mpd 0000755 0000000 0000000 00000045752 11700415064 020465 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""MPD adapter for Remuco, implemented as an executable script."""
import os.path
import socket # python-mpd (0.2.0) does not fully abstract socket errors
import gobject
import mpd
import remuco
from remuco import log
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
PLAYLIST_ACTIONS = (IA_JUMP, IA_REMOVE)
IA_ADD = remuco.ItemAction("Add to playlist", multiple=True)
IA_SET = remuco.ItemAction("Set as playlist", multiple=True)
MLIB_ITEM_ACTIONS = (IA_ADD, IA_SET)
LA_PLAY = remuco.ListAction("Play")
LA_ENQUEUE = remuco.ListAction("Enqueue")
MLIB_LIST_ACTIONS = (LA_PLAY, LA_ENQUEUE)
DA_PLAY = remuco.ListAction("Play")
DA_ENQUEUE = remuco.ListAction("Enqueue")
MLIB_DIR_ACTIONS = (DA_ENQUEUE, DA_PLAY)
SEARCH_MASK = [ "Artist", "Album", "Title" ]
# =============================================================================
# constants
# =============================================================================
MLIB_FILES = "Files"
MLIB_PLAYLISTS = "Playlists"
# =============================================================================
# MPD player adapter
# =============================================================================
class MPDAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "MPD",
playback_known=True,
volume_known=True,
repeat_known=True,
shuffle_known=True,
progress_known=True,
search_mask=SEARCH_MASK)
self.__mpd = mpd.MPDClient()
self.__mpd_host = self.config.getx("mpd-host", "localhost")
self.__mpd_port = self.config.getx("mpd-port", "6600", int)
self.__mpd_pwd = self.config.getx("mpd-password", "")
self.__mpd_music = self.config.getx("mpd-music", "/var/lib/mpd/music")
log.debug("MPD is at %s:%d" % (self.__mpd_host, self.__mpd_port))
self.__playing = False
self.__shuffle = False
self.__repeat = False
self.__volume = 0
self.__position = -1
self.__progress = 0
self.__length = 0
self.__song = None
def start(self):
remuco.PlayerAdapter.start(self)
if not self.__check_and_refresh_connection():
raise StandardError("failed to connect to MPD")
try:
mpd_version = self.__mpd.mpd_version
except mpd.MPDError:
log.warning("failed to get MPD version")
mpd_version = "unknown"
log.info("MPD version: %s" % mpd_version)
def stop(self):
remuco.PlayerAdapter.stop(self)
try:
self.__mpd.disconnect()
except mpd.ConnectionError:
pass
log.debug("MPD adapter stopped")
def poll(self):
self.__poll_status()
self.__poll_item()
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
if not self.__check_and_refresh_connection():
return
try:
if self.__playing:
self.__mpd.pause(1)
else:
self.__mpd.play()
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_toggle_repeat(self):
if not self.__check_and_refresh_connection():
return
try:
self.__mpd.repeat(int(not self.__repeat))
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_toggle_shuffle(self):
if not self.__check_and_refresh_connection():
return
try:
self.__mpd.random(int(not self.__shuffle))
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_next(self):
if not self.__check_and_refresh_connection():
return
try:
self.__mpd.next()
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_previous(self):
if not self.__check_and_refresh_connection():
return
try:
self.__mpd.previous()
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_seek(self, direction):
if self.__length == 0:
return
if not self.__check_and_refresh_connection():
return
progress = self.__progress + direction * 5
progress = min(progress, self.__length)
progress = max(progress, 0)
try:
self.__mpd.seek(self.__position, progress)
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
def ctrl_volume(self, direction):
if not self.__check_and_refresh_connection():
return
try:
if direction == 0:
self.__mpd.setvol(0)
else:
volume = self.__volume + direction * 5
volume = min(volume, 100)
volume = max(volume, 0)
self.__mpd.setvol(volume)
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
else:
gobject.idle_add(self.__poll_status)
# =========================================================================
# action interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if not self.__check_and_refresh_connection():
return
if action_id == IA_JUMP.id:
try:
self.__mpd.play(positions[0])
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
elif action_id == IA_REMOVE.id:
positions.sort()
positions.reverse()
self.__batch_cmd(self.__mpd.delete, positions)
else:
log.error("** BUG ** unexpected playlist item action")
def action_mlib_item(self, action_id, path, positions, ids):
if not self.__check_and_refresh_connection():
return
if action_id == IA_ADD.id:
self.__batch_cmd(self.__mpd.add, ids)
elif action_id == IA_SET.id:
try:
self.__mpd.clear()
self.__batch_cmd(self.__mpd.add, ids)
if self.__playing:
self.__mpd.play(0)
except mpd.MPDError, e:
log.warning("failed to set playlist: %s" % e)
else:
log.error("** BUG ** unexpected mlib item action")
def action_mlib_list(self, action_id, path):
if not self.__check_and_refresh_connection():
return
try:
name = path[1]
except IndexError:
log.error("** BUG ** unexpected path for list actions: %s" % path)
return
if action_id == LA_ENQUEUE.id:
try:
self.__mpd.load(name)
except mpd.MPDError, e:
log.warning("failed to enqueue playlist (%s): %s" % (name, e))
elif action_id == LA_PLAY.id:
try:
self.__mpd.clear()
self.__mpd.load(name)
if self.__playing:
self.__mpd.play(0)
except mpd.MPDError, e:
log.warning("failed to play playlist (%s): %s" % (name, e))
elif action_id == DA_ENQUEUE.id:
try:
xpath = os.path.sep.join(path[1:])
self.__mpd.add(xpath)
except mpd.MPDERROR, e:
log.warning("failed to add in playlist (%s): %s" % (path_s, e))
elif action_id == DA_PLAY.id:
try:
xpath = os.path.sep.join(path[1:])
self.__mpd.clear()
self.__mpd.add(xpath)
except mpd.MPDERROR, e:
log.warning("failed to set playlist (%s): %s" % (path_s, e))
else:
log.error("** BUG ** unexpected mlib list action")
def action_search_item(self, action_id, positions, ids):
self.action_mlib_item(action_id, [], positions, ids)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
if not self.__check_and_refresh_connection():
return
try:
playlist = self.__mpd.playlistinfo()
except mpd.MPDError, e:
log.warning("failed to control MPD: %s" % e)
playlist = []
for song in playlist:
reply.ids.append(song.get("file", "XXX"))
artist = song.get("artist", "??")
title = song.get("title", "??")
reply.names.append("%s - %s" % (artist, title))
reply.item_actions = PLAYLIST_ACTIONS
reply.send()
def request_mlib(self, reply, path):
if not self.__check_and_refresh_connection():
return
if not path:
reply.nested = [MLIB_FILES, MLIB_PLAYLISTS]
elif path[0] == MLIB_FILES:
reply.nested, reply.ids, reply.names = self.__get_music_dir(path[1:])
reply.item_actions = MLIB_ITEM_ACTIONS
reply.list_actions = MLIB_DIR_ACTIONS
elif path[0] == MLIB_PLAYLISTS and len(path) == 1:
reply.nested = self.__get_playlists()
reply.list_actions = MLIB_LIST_ACTIONS
elif path[0] == MLIB_PLAYLISTS and len(path) == 2:
reply.ids, reply.names = self.__get_playlist_content(path[1])
reply.item_actions = MLIB_ITEM_ACTIONS
elif path[0] == MLIB_PLAYLISTS:
log.error("** BUG ** unexpected path depth for playlists")
else:
log.error("** BUG ** unexpected root list: %s" % path[0])
reply.send()
def request_search(self, reply, query):
if not self.__check_and_refresh_connection():
return
result_dicts = []
for field, value in zip(SEARCH_MASK, query):
if not value:
continue
songs = self.__mpd.search(field, value)
result = {}
for song in songs:
result[song.get("file", "unknown")] = song
result_dicts.append(result)
result = self.__intersect_dicts(result_dicts)
reply.ids, reply.names = self.__songs_to_item_list(result.values(), True)
reply.item_actions = MLIB_ITEM_ACTIONS
reply.send()
# =========================================================================
# internal methods
# =========================================================================
def __poll_status(self):
if not self.__check_and_refresh_connection():
return
status = self.__mpd.status()
self.__volume = int(status.get("volume", "0"))
self.update_volume(self.__volume)
self.__repeat = status.get("repeat", "0") != "0"
self.update_repeat(self.__repeat)
self.__shuffle = status.get("random", "0") != "0"
self.update_shuffle(self.__shuffle)
playback = status.get("state", "stop")
if playback == "play":
self.__playing = True
self.update_playback(remuco.PLAYBACK_PLAY)
elif playback == "pause":
self.__playing = False
self.update_playback(remuco.PLAYBACK_PAUSE)
else:
self.__playing = False
self.update_playback(remuco.PLAYBACK_STOP)
progress_length = status.get("time", "0:0").split(':')
self.__progress = int(progress_length[0])
self.__length = int(progress_length[1])
self.update_progress(self.__progress, self.__length)
self.__position = int(status.get("song", "-1"))
self.update_position(max(int(self.__position), 0))
def __poll_item(self):
if not self.__check_and_refresh_connection():
return
try:
song = self.__mpd.currentsong()
except mpd.MPDError, e:
log.warning("failed to query current song: %s" % e)
song = None
if self.__song == song:
return
self.__song = song
if not song:
self.update_item(None, None, None)
return
id = song.get("file", "XXX")
info = {}
info[remuco.INFO_ARTIST] = song.get("artist")
info[remuco.INFO_TITLE] = song.get("title")
info[remuco.INFO_ALBUM] = song.get("album")
info[remuco.INFO_GENRE] = song.get("genre")
info[remuco.INFO_LENGTH] = song.get("time")
info[remuco.INFO_YEAR] = song.get("year")
full_file_name = os.path.join(self.__mpd_music, id)
img = self.find_image(full_file_name)
self.update_item(id, info, img)
def __get_music_dir(self, path):
"""Client requests a certain path in MPD's music directory."""
path_s = ""
for elem in path:
path_s = os.path.join(path_s, elem)
try:
content = self.__mpd.lsinfo(path_s)
except mpd.MPDError, e:
log.warning("failed to get dir list (%s): %s" % (path_s, e))
content = []
dirs, files = [], []
for entry in content:
if "directory" in entry:
dirs.append(os.path.basename(entry["directory"]))
elif "file" in entry:
files.append(entry["file"])
else:
pass
songs = self.__batch_cmd(self.__mpd.listallinfo, files)
names = []
if songs and len(songs) == len(files):
for item in songs:
artist = item[0].get("artist", "??")
title = item[0].get("title", "??")
names.append("%s - %s" % (artist, title))
else:
files = []
return dirs, files, names
def __get_playlists(self):
try:
content = self.__mpd.lsinfo()
except mpd.MPDError, e:
log.warning("failed to get playlists: %s" % e)
content = []
names = []
for entry in content:
if "playlist" in entry:
names.append(os.path.basename(entry["playlist"]))
else:
pass
return names
def __get_playlist_content(self, name):
try:
songs = self.__mpd.listplaylistinfo(name)
except mpd.MPDError, e:
log.warning("failed to get playlist content (%s): %s" % (name, e))
songs = []
return self.__songs_to_item_list(songs)
def __songs_to_item_list(self, songs, sort=False):
ids, names = [], []
def skey(song): # sorting key for songs
album = song.get("album", "")
track = int(song.get("track", "0").split("/")[0])
disc = int(song.get("disc", "0").split("/")[0])
return "%s.%02d.%03d" % (album, disc, track)
if sort:
songs = sorted(songs, key=skey)
for item in songs:
ids.append(item.get("file", "XXX"))
artist = item.get("artist", "??")
title = item.get("title", "??")
names.append("%s - %s" % (artist, title))
return ids, names
def __check_and_refresh_connection(self):
"""Check the current MPD connection and reconnect if broken."""
try:
self.__mpd.ping()
except mpd.ConnectionError:
try:
self.__mpd.disconnect()
except mpd.ConnectionError:
pass
try:
self.__mpd.connect(self.__mpd_host, self.__mpd_port)
self.__mpd.ping()
if self.__mpd_pwd:
self.__mpd.password(self.__mpd_pwd)
log.debug("connected to MPD")
except (mpd.ConnectionError, socket.error), e:
log.error("failed to connect to MPD: %s" % e)
self.manager.stop()
return False
return True
def __intersect_dicts(self, dict_list):
"""Creates an intersection of dictionaries based on keys."""
if not dict_list:
return {}
first, rest = dict_list[0], dict_list[1:]
keys_intersection = set(first.keys())
for other_dict in rest:
keys_intersection.intersection_update(set(other_dict.keys()))
result = {}
for key in keys_intersection:
result[key] = first[key]
return result
def __batch_cmd(self, cmd, params):
try:
self.__mpd.command_list_ok_begin()
except mpd.MPDError, e:
log.warning("failed to start command list: %s" % e)
return
for param in params:
try:
cmd(param)
except mpd.MPDError, e:
log.warning("in-list command failed: %s" % e)
break
try:
return self.__mpd.command_list_end()
except mpd.MPDError, e:
log.warning("failed to end command list: %s" % e)
try:
self.__mpd.disconnect()
except mpd.ConnectionError:
pass
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = MPDAdapter()
mg = remuco.Manager(pa)
mg.run()
remuco-source-0.9.6/adapter/mplayer/remuco-mplayer 0000755 0000000 0000000 00000033251 11700415064 022236 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
# Some remote control related documentation for MPlayer is available at
# http://www.mplayerhq.hu/DOCS/tech/slave.txt
# To parse the status line, read
# http://www.mplayerhq.hu/DOCS/HTML/en/faq.html#id2734829
"""MPlayer adapter for Remuco, implemented as an executable script."""
import os
import os.path
import gobject
import re
import sys
import subprocess
import remuco
from remuco import log
from remuco.defs import *
FA_APPEND = remuco.ItemAction("Append")
FA_PLAY = remuco.ItemAction("Play")
FILE_ACTIONS = (FA_APPEND, FA_PLAY)
MODE_INDEPENDENT = 1 << 0
MODE_MASTER = 1 << 1
class MplayerAdapter(remuco.PlayerAdapter):
def __init__(self, filelist=None):
remuco.PlayerAdapter.__init__(self, "MPlayer",
progress_known=True,
file_actions=FILE_ACTIONS,
mime_types=remuco.MIMETYPES_AV)
self.__mode = MODE_INDEPENDENT
if filelist is not None:
self.filelist = filelist
self.__mode = MODE_MASTER
# TODO put this somewhere else
if self.__mode is MODE_INDEPENDENT:
self.cmdfifo = os.path.join(self.config.cache, "mplayer.cmdfifo")
self.cmdfifo = self.config.getx("cmdfifo", self.cmdfifo)
self.statusfifo = os.path.join(self.config.cache, "mplayer.statusfifo")
self.statusfifo = self.config.getx("statusfifo", self.statusfifo)
# file descriptors
[self.fd_cmd_stream_in, self.fd_cmd_stream_out,
self.fd_status_stream_in, self.fd_status_stream_out] = [None]*4
# gobject's watches:
self.watch_id = None # read from mplayer using statusfifo
self.watch_death = None # wait for mplayer exit (MODE_MASTER)
# compile regexes to be used when parsing the status string
# in __inputevent()
# TODO use a generic self.current_regex pointing to either one
# of video_regex or audio_regex and update it whenever the current
# file in the playlist changes.
# NOTE there is a space char after every information
# NOTE audio_regex contains a "pretty position" which looks like:
# ([[HH:]MM:]SS.d)
# i wonder how far does it go...
self.video_regex = re.compile(
r"""^A:\s*(?P\d+\.\d)\s # audio position (seconds)
V:\s*(?P\d+\.\d)\s # video position (seconds)
A-V:\s*(?P-?\d+\.\d+)\s # video position (seconds)
(?P) # identify this regex uniquely
""", re.VERBOSE)
self.audio_regex = re.compile(
r"""^A:\s*(?P\d+\.\d)\s # raw position (seconds)
\((?P(\d\d:)*\d\d\.\d)\)\s # pretty position
of (?P\d+\.\d)\s # total length in seconds
\((?P(\d\d:)*\d\d\.\d)\)\s # pretty total
(?P) # identifier
""", re.VERBOSE)
self.info_regex = re.compile(
r"""^ANS_(?P[A-Za-z_]+)= # information name
(?P('.*'|\d+\.\d+)) # if quoted, str. else, number
(?P) # identifier
""", re.VERBOSE)
self.pause_regex = re.compile(r" *===+ +\w+ +===+")
self.__set_default_info()
def start(self):
remuco.PlayerAdapter.start(self)
self.__setup_streams()
self.watch_id = gobject.io_add_watch(self.fd_status_stream_in,
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP,
self.__inputevent)
if self.__mode is MODE_MASTER:
args = ['mplayer', '-slave', '-input', 'file=/dev/null']
args += self.filelist
self.__mplayer_pid = subprocess.Popen(args, bufsize=1,
stdin=self.fd_cmd_stream_in, stdout=self.fd_status_stream_out).pid
def cb(pid, condition, user_data=None): #local callback for watch_death
self.manager.stop()
self.watch_death = gobject.child_watch_add(self.__mplayer_pid, cb)
log.debug("starting mplayer adapter")
def stop(self):
# FIXME probable bug: call manager.stop from here,
# and the manager will call stop_pa again
remuco.PlayerAdapter.stop(self)
if self.fd_cmd_stream_in is not None:
os.closerange(self.fd_cmd_stream_in, self.fd_status_stream_out)
[self.fd_cmd_stream_in, self.fd_cmd_stream_out,
self.fd_status_stream_in, self.fd_status_stream_out] = [None]*4
if self.watch_id is not None:
gobject.source_remove(self.watch_id)
self.watch_id = None
if self.watch_death is not None:
gobject.source_remove(self.watch_death)
self.watch_death = None
log.debug("stopping mplayer adapter")
def poll(self):
# fetch important information from player
# NOTE while this fixes most of our problems, it will always
# clutter mplayer's output
if self.__playback == PLAYBACK_PLAY:
self.__command('get_time_length\n')
self.__command('get_file_name\n')
self.__command('get_property volume\n')
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
self.__command('pause\n')
def ctrl_toggle_fullscreen(self):
self.__command('vo_fullscreen\n')
def ctrl_next(self):
self.__command('pt_step 1\n')
gobject.idle_add(self.poll)
def ctrl_previous(self):
self.__command('pt_step -1\n')
gobject.idle_add(self.poll)
def ctrl_volume(self, direction):
self.__command('volume %d %d\n' % (direction, direction==0))
gobject.idle_add(self.poll)
def ctrl_seek(self, direction):
self.__command('seek %s0\n' % direction)
def ctrl_navigate(self, action):
button = ['up', 'down', 'left', 'right', 'select', 'prev', 'menu']
if action >= 0 and action < button.__len__():
self.__command('dvdnav %s\n' % button[action])
# =========================================================================
# request interface
# =========================================================================
#def request_playlist(self, client):
# self.reply_playlist_request(client, ["1", "2"],
# ["Joe - Joe's Song", "Sue - Sue's Song"])
# ...
# =========================================================================
# actions interface
# =========================================================================
def action_files(self, action_id, files, uris):
if action_id == FA_APPEND.id:
for file in files:
self.__command('loadfile "%s" 1' % file)
elif action_id == FA_PLAY.id:
self.__command('loadfile "%s"' % files[0])
else:
log.error("** BUG ** unexpected action ID")
# =========================================================================
# internal methods
# =========================================================================
def __command(self, cmd):
"""Write a command to the MPlayer FIFO and handle errors."""
try:
os.write(self.fd_cmd_stream_out, cmd)
except OSError, e:
log.warning("FIFO to MPlayer broken (%s)", e)
self.manager.stop()
def __read_stream(self, fd):
"""Read from a file descriptor and handle errors."""
try:
str = os.read(fd, 200) #magic buflength number
return str
except OSError, e:
log.warning("FIFO from MPlayer broken (%s)", e)
self.manager.stop()
def __inputevent(self, fd, condition):
"""Parse incoming line from the player"""
if condition == gobject.IO_IN:
# read from fd (fd is self.fd_status_stream_in)
status_str = self.__read_stream(fd)
if self.pause_regex.search(status_str):
self.__playback = PLAYBACK_PAUSE
self.update_playback(PLAYBACK_PAUSE)
return True
matchobj = (self.video_regex.match(status_str) or
self.audio_regex.match(status_str) or
self.info_regex.match(status_str))
if matchobj is not None:
self.__playback = PLAYBACK_PLAY
self.update_playback(self.__playback)
current_progress = self.__progress
current_length = self.info[INFO_LENGTH]
identifier = matchobj.lastgroup
if identifier == 'videoregex': # we have a video regex
current_progress = float(matchobj.group('rawvideopos'))
elif identifier == 'inforegex': # info regex
self.__set_info(matchobj.group('infovar'),
matchobj.group('infovalue'))
current_length = self.info[INFO_LENGTH] #avoid a race condition
elif identifier == 'audioregex': # audio regex
current_progress = float(matchobj.group('rawaudiopos'))
current_length = float(matchobj.group('rawtotal'))
self.update_progress(current_progress, current_length)
self.__progress = current_progress
self.info[INFO_LENGTH] = current_length
return True
else:
if condition == gobject.IO_HUP: # mplayer has probably quit
log.info("statusfifo hangup. is mplayer still running?")
else:
log.error("statusfifo read error")
self.stop()
gobject.idle_add(self.start)
return False
def __set_info(self, name, value):
""" Set global information of current item
@param name:
The name of the variable in mplayer. (str)
@param value:
The value mplayer assigns. (str)
@see self.__inputevent() and self.info_regex for more information
"""
if name == 'LENGTH':
self.info[INFO_LENGTH] = float(value)
elif name == 'FILENAME':
self.info[INFO_TITLE] = value.strip("'")
self.update_item(self.info[INFO_TITLE], self.info, None)
elif name == 'volume': # lower case because we use the get_property command
self.update_volume(float(value))
def __set_default_info(self):
""" Set default playback information """
self.__progress = 0
self.__playback = PLAYBACK_STOP
self.info = { INFO_LENGTH: 100, INFO_TITLE: '' }
def __setup_streams(self):
""" Setup information streams.
This depends on the mode we are running:
MODE_INDEPENDENT - uses FIFOs to issue commands and fetch status
information
MODE_MASTER - uses a newly created pipe to connect to mplayer
directly
"""
if self.__mode is MODE_INDEPENDENT:
if not(os.path.exists(self.cmdfifo)):
os.mkfifo(self.cmdfifo)
if not(os.path.exists(self.statusfifo)):
os.mkfifo(self.statusfifo)
else:
log.warning('statusfifo file already exists. this is not a ' +
'problem, but you might end up with a big file if ' +
self.statusfifo + ' is not a FIFO.')
log.debug("opening cmdfifo and waiting for mplayer to read it")
self.fd_cmd_stream_out = os.open(self.cmdfifo, os.O_WRONLY)
self.fd_cmd_stream_in = self.fd_cmd_stream_out
log.debug("opening statusfifo")
self.fd_status_stream_in = os.open(self.statusfifo, os.O_RDONLY | os.O_NONBLOCK)
self.fd_status_stream_out = self.fd_status_stream_in
else: # MODE_MASTER
log.debug("creating pipes to communicate with mplayer")
self.fd_cmd_stream_in, self.fd_cmd_stream_out = os.pipe()
self.fd_status_stream_in, self.fd_status_stream_out = os.pipe()
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
filepaths = None
# TODO use 'optparser' module instead
if sys.argv.__len__() > 1:
filepaths = []
# TODO parse arguments looking for mplayer options
#parse arguments looking for video files
for arg in sys.argv[1:]:
if os.path.isfile(arg): filepaths.append(arg)
pa = MplayerAdapter(filepaths) # create the player adapter
mg = remuco.Manager(pa) # pass it to a manager
mg.run() # run the manager (blocks until interrupt signal)
remuco-source-0.9.6/adapter/okular/.wip 0000644 0000000 0000000 00000000000 11700415064 017755 0 ustar root root 0000000 0000000 remuco-source-0.9.6/adapter/okular/remuco-okular 0000755 0000000 0000000 00000013563 11700415064 021712 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Okular adapter for Remuco, implemented as an executable script."""
import commands
import os.path
import re
import dbus
from dbus.exceptions import DBusException
import remuco
from remuco import log
IA_GOTO = remuco.ItemAction("Go to page", multiple=False)
IA_OPEN = remuco.ItemAction("Open file", multiple=False)
class OkularAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "Okular",
playback_known=True,
volume_known=True,
search_mask=["Page"],
file_actions=[IA_OPEN],
mime_types=["application/pdf"])
self.__am = None
self.__image = os.path.join(self.config.cache, "okular.thumbnail")
def start(self):
remuco.PlayerAdapter.start(self)
try:
bus = dbus.SessionBus()
proxy = bus.get_object("org.freedesktop.DBus", "/")
dbusiface = dbus.Interface(proxy, "org.freedesktop.DBus")
okulariface = None
for iface in dbusiface.ListNames():
if "okular" in iface:
okulariface = iface
proxy = bus.get_object(okulariface, "/okular")
self.__am = dbus.Interface(proxy, "org.kde.okular")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
def stop(self):
remuco.PlayerAdapter.stop(self)
log.debug("bye, turning off the light")
def poll(self):
self.refreshdata(refresh_image=True)
def refreshdata(self, refresh_image=False):
doc = self.__am.currentDocument()
page = self.__am.currentPage()
page_max = self.__am.pages()
info = {}
info[remuco.INFO_TITLE] = os.path.basename(doc)
info[remuco.INFO_ARTIST] = "%d/%d" % (page, page_max)
if refresh_image:
commands.getoutput('convert "%s"[%s] -thumbnail 150 %s' %
(doc, page, self.__image))
image = self.__image
else:
image = None
self.update_item(None, info, image)
self.update_volume((float(page) / page_max * 100))
# =========================================================================
# control interface
# =========================================================================
def ctrl_next(self):
self.__am.slotNextPage()
self.refreshdata()
def ctrl_previous(self):
self.__am.slotPreviousPage()
self.refreshdata()
def ctrl_volume(self, direction):
if direction == 0:
self.__am.goToPage(1)
elif direction == -1:
if self.__am.currentPage() - 10 < 1:
self.__am.slotGotoFirst()
else:
self.__am.goToPage(self.__am.currentPage() - 10)
elif direction == 1:
if self.__am.currentPage() + 10 > self.__am.pages():
self.__am.slotGotoLast()
else:
self.__am.goToPage(self.__am.currentPage() + 10)
self.refreshdata()
def ctrl_toggle_fullscreen(self):
self.__am.slotTogglePresentation()
# =========================================================================
# actions interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_GOTO.id:
self.__am.goToPage(ids[0])
def action_files(self, action_id, files, uris):
if action_id == IA_OPEN.id:
self.__am.openDocument(uris[0])
self.refreshdata(True)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
reply.ids = range(1, self.__am.pages()+1)
reply.names = range(1, self.__am.pages()+1)
reply.item_actions = [IA_GOTO]
reply.send()
def request_search(self, reply, query):
if (re.match("^[0-9]+$", query[0]) and
int(query[0]) < self.__am.pages() and int(query[0]) > 0):
reply.ids = [query[0]]
reply.names = ["Page %s opened" % query[0]]
self.__am.goToPage(int(query[0]))
else:
reply.ids = ["0"]
reply.names = ["No such page"]
reply.send()
# =============================================================================
# main (example startup using remuco.Manager)
# =============================================================================
if __name__ == '__main__':
pa = OkularAdapter()
mg = remuco.Manager(pa)
mg.run()
remuco-source-0.9.6/adapter/quodlibet/remuco-quodlibet 0000755 0000000 0000000 00000016534 11700415064 023101 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""QuodLibet adapter for Remuco, implemented as an executable script."""
# TODO: This adapter currently is a script. An adapter implemented as a QL
# plugin probably would provide significantly more features, especially
# concerning media library integration.
import commands
import dbus
from dbus.exceptions import DBusException
import gobject
import remuco
from remuco import log
class QuodLibetAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self,
"QuodLibet",
progress_known=True,
playback_known=True,
repeat_known=True,
shuffle_known=True,
max_rating=4)
self.__dbus_signal_handler = ()
self.__ql_dbus = None
self.__song_len = 0 # need this when polling progress
def start(self):
remuco.PlayerAdapter.start(self)
# set up DBus connection
try:
bus = dbus.SessionBus()
proxy = bus.get_object("net.sacredchao.QuodLibet",
"/net/sacredchao/QuodLibet")
self.__ql_dbus = dbus.Interface(proxy, "net.sacredchao.QuodLibet")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
# connect to DBus signals
try:
self.__dbus_signal_handler = (
self.__ql_dbus.connect_to_signal("Paused",
self.__on_paused),
self.__ql_dbus.connect_to_signal("Unpaused",
self.__on_unpaused),
self.__ql_dbus.connect_to_signal("SongStarted",
self.__on_song_started),
self.__ql_dbus.connect_to_signal("SongEnded",
self.__on_song_ended),
)
except DBusException, e:
raise StandardError("dbus error: %s" % e)
# misc initialization
self.__song_len = 0
# initial info retrieval
self.poll()
if self.__ql_dbus.IsPlaying():
self.__on_song_started(self.__ql_dbus.CurrentSong())
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_PAUSE)
def stop(self):
remuco.PlayerAdapter.stop(self)
# disconnect DBus stuff
for handler in self.__dbus_signal_handler:
handler.remove()
self.__dbus_signal_handler = ()
self.__ql_dbus = None
def poll(self):
self.__poll_status()
self.__poll_progress()
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
self.__ql_dbus.PlayPause()
def ctrl_next(self):
self.__ql_dbus.Next()
def ctrl_previous(self):
self.__ql_dbus.Previous()
def ctrl_toggle_shuffle(self):
self.__ql_cmd("--order=toggle")
gobject.idle_add(self.__poll_status)
def ctrl_toggle_repeat(self):
self.__ql_cmd("--repeat=t")
gobject.idle_add(self.__poll_status)
def ctrl_volume(self, direction):
if direction == 0:
self.__ql_cmd("--volume=0")
elif direction == -1:
self.__ql_cmd("--volume-down")
else:
self.__ql_cmd("--volume-up")
gobject.idle_add(self.__poll_status)
def ctrl_rate(self, rating):
self.__ql_cmd("--set-rating=%1.1f" % (float(rating) / 4))
# =========================================================================
# request interface
# =========================================================================
# ... not yet supported, see TODO on top
# =========================================================================
# helper
# =========================================================================
def __ql_cmd(self, action):
"""QL command line interaction."""
return commands.getoutput("quodlibet " + action)
def __on_paused(self):
"""DBus signal handler."""
self.update_playback(remuco.PLAYBACK_PAUSE)
def __on_unpaused(self):
"""DBus signal handler."""
self.update_playback(remuco.PLAYBACK_PLAY)
def __on_song_started(self, song):
"""DBus signal handler."""
info = {}
info[remuco.INFO_ALBUM] = song.get("album", None)
info[remuco.INFO_ARTIST] = song.get("artist", None)
info[remuco.INFO_GENRE] = song.get("genre", None)
info[remuco.INFO_LENGTH] = song.get("~#length", 0)
info[remuco.INFO_RATING] = int(float(song.get("~#rating", 0.5)) * 4)
info[remuco.INFO_TITLE] = song.get("title", None)
info[remuco.INFO_YEAR] = song.get("date", 0)
info[remuco.INFO_BITRATE] = int(song.get("~#bitrate", 0)) / 1000
self.__song_len = info[remuco.INFO_LENGTH]
# QL does not provide an ID/URI -> no chance to get local cover art
self.update_item(None, info, None)
def __on_song_ended(self, song, skipped):
"""DBus signal handler."""
self.update_item(None, None, None)
self.__song_len = 0
self.update_progress(0, 0)
def __poll_status(self):
"""Poll volume, repeat and shuffle."""
status = self.__ql_cmd("--status").split()
if status and len(status) == 5:
self.update_volume(float(status[2]) * 100)
self.update_shuffle(status[3] != "inorder")
self.update_repeat(status[4] == "on")
else:
self.update_volume(50)
self.update_shuffle(False)
self.update_repeat(False)
def __poll_progress(self):
"""Poll playback progress."""
if self.__ql_dbus.IsPlaying():
self.update_progress(self.__ql_dbus.GetPosition() / 1000,
self.__song_len)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = QuodLibetAdapter()
mg = remuco.Manager(pa, dbus_name="net.sacredchao.QuodLibet")
mg.run()
remuco-source-0.9.6/adapter/rhythmbox/install-check.py 0000644 0000000 0000000 00000000576 11700415064 023021 0 ustar root root 0000000 0000000 import sys
try:
import gconf
except ImportError, e:
print("")
print("+-----------------------------------------------------------------+")
print("| Unsatisfied Python requirement: %s." % e)
print("| Please install the missing module and then retry.")
print("+-----------------------------------------------------------------+")
print("")
sys.exit(1) remuco-source-0.9.6/adapter/rhythmbox/remuco.rb-plugin 0000644 0000000 0000000 00000000345 11700415064 023033 0 ustar root root 0000000 0000000 [RB Plugin]
Loader=python
Module=remythm
IAge=1
Name=Remuco
Description=Rhythmbox adapter for Remuco (and vice versa).
Authors=Remuco team
Copyright=Copyright © 2006-2010 by the Remuco team
Website=http://remuco.googlecode.com/
remuco-source-0.9.6/adapter/rhythmbox/remythm.py 0000644 0000000 0000000 00000056500 11700415064 021763 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Rhythmbox player adapter for Remuco, implemented as a Rhythmbox plugin."""
import time
import gconf
import gobject
import rb, rhythmdb
import remuco
from remuco import log
# =============================================================================
# plugin
# =============================================================================
class RemucoPlugin(rb.Plugin):
def __init__(self):
rb.Plugin.__init__(self)
self.__rba = None
def activate(self, shell):
if self.__rba is not None:
return
print("create RhythmboxAdapter")
self.__rba = RhythmboxAdapter()
print("RhythmboxAdapter created")
print("start RhythmboxAdapter")
self.__rba.start(shell)
print("RhythmboxAdapter started")
def deactivate(self, shell):
if self.__rba is None:
return
print("stop RhythmboxAdapter")
self.__rba.stop()
print("RhythmboxAdapter stopped")
self.__rba = None
# =============================================================================
# constants
# =============================================================================
PLAYORDER_SHUFFLE = "shuffle"
PLAYORDER_SHUFFLE_ALT = "random-by-age-and-rating"
PLAYORDER_REPEAT = "linear-loop"
PLAYORDER_NORMAL = "linear"
PLAYERORDER_TOGGLE_MAP_REPEAT = {
PLAYORDER_SHUFFLE: PLAYORDER_SHUFFLE_ALT,
PLAYORDER_SHUFFLE_ALT: PLAYORDER_SHUFFLE,
PLAYORDER_REPEAT: PLAYORDER_NORMAL,
PLAYORDER_NORMAL: PLAYORDER_REPEAT
}
PLAYERORDER_TOGGLE_MAP_SHUFFLE = {
PLAYORDER_SHUFFLE: PLAYORDER_NORMAL,
PLAYORDER_NORMAL: PLAYORDER_SHUFFLE,
PLAYORDER_SHUFFLE_ALT: PLAYORDER_REPEAT,
PLAYORDER_REPEAT: PLAYORDER_SHUFFLE_ALT
}
SEARCH_MASK = ("Any", "Artist", "Title", "Album", "Genre")
SEARCH_PROPS = ("Any", rhythmdb.PROP_ARTIST, rhythmdb.PROP_TITLE,
rhythmdb.PROP_ALBUM, rhythmdb.PROP_GENRE)
SEARCH_PROPS_ANY = (rhythmdb.PROP_ARTIST, rhythmdb.PROP_TITLE,
rhythmdb.PROP_ALBUM, rhythmdb.PROP_GENRE,
rhythmdb.PROP_LOCATION)
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
LA_PLAY = remuco.ListAction("Play")
IA_ENQUEUE = remuco.ItemAction("Enqueue", multiple=True)
PLAYLIST_ACTIONS = (IA_JUMP, IA_ENQUEUE)
QUEUE_ACTIONS = (IA_JUMP, IA_REMOVE)
MLIB_LIST_ACTIONS = (LA_PLAY,)
MLIB_ITEM_ACTIONS = (IA_ENQUEUE, IA_JUMP)
SEARCH_ACTIONS = (IA_ENQUEUE,)
# =============================================================================
# player adapter
# =============================================================================
class RhythmboxAdapter(remuco.PlayerAdapter):
def __init__(self):
self.__shell = None
self.__gconf = None
remuco.PlayerAdapter.__init__(self, "Rhythmbox",
max_rating=5,
playback_known=True,
volume_known=True,
repeat_known=True,
shuffle_known=True,
progress_known=True,
search_mask=SEARCH_MASK)
self.__item_id = None
self.__item_entry = None
self.__playlist_sc = None
self.__queue_sc = None
self.__signal_ids = ()
log.debug("init done")
def start(self, shell):
if self.__shell is not None:
log.warning("already started")
return
remuco.PlayerAdapter.start(self)
self.__shell = shell
sp = self.__shell.get_player()
# gconf is used to adjust repeat and shuffle
self.__gconf = gconf.client_get_default()
# shortcuts to RB data
self.__item_id = None
self.__item_entry = None
self.__playlist_sc = sp.get_playing_source()
self.__queue_sc = self.__shell.props.queue_source
# connect to shell player signals
self.__signal_ids = (
sp.connect("playing_changed", self.__notify_playing_changed),
sp.connect("playing_uri_changed", self.__notify_playing_uri_changed),
sp.connect("playing-source-changed", self.__notify_source_changed)
)
# state sync will happen by timeout
# trigger item sync:
self.__notify_playing_uri_changed(sp, sp.get_playing_path()) # item sync
log.debug("start done")
def stop(self):
remuco.PlayerAdapter.stop(self)
if self.__shell is None:
return
# disconnect from shell player signals
sp = self.__shell.get_player()
for sid in self.__signal_ids:
sp.disconnect(sid)
self.__signal_ids = ()
# release shell
self.__shell = None
self.__gconf = None
log.debug("stop done")
def poll(self):
sp = self.__shell.get_player()
# check repeat and shuffle
order = sp.props.play_order
repeat = order == PLAYORDER_REPEAT or order == PLAYORDER_SHUFFLE_ALT
self.update_repeat(repeat)
shuffle = order == PLAYORDER_SHUFFLE or order == PLAYORDER_SHUFFLE_ALT
self.update_shuffle(shuffle)
# check volume
volume = int(sp.get_volume() * 100)
self.update_volume(volume)
# check progress
try:
progress = sp.get_playing_time()
length = sp.get_playing_song_duration()
except gobject.GError:
progress = 0
length = 0
else:
self.update_progress(progress, length)
# =========================================================================
# control interface
# =========================================================================
def ctrl_next(self):
sp = self.__shell.get_player()
try:
sp.do_next()
except gobject.GError, e:
log.debug("do next failed: %s" % str(e))
def ctrl_previous(self):
sp = self.__shell.get_player()
try:
sp.set_playing_time(0)
time.sleep(0.1)
sp.do_previous()
except gobject.GError, e:
log.debug("do previous failed: %s" % str(e))
def ctrl_rate(self, rating):
if self.__item_entry is not None:
db = self.__shell.props.db
try:
db.set(self.__item_entry, rhythmdb.PROP_RATING, rating)
except gobject.GError, e:
log.debug("rating failed: %s" % str(e))
def ctrl_toggle_playing(self):
sp = self.__shell.get_player()
try:
sp.playpause()
except gobject.GError, e:
log.debug("toggle play pause failed: %s" % str(e))
def ctrl_toggle_repeat(self):
sp = self.__shell.get_player()
now = sp.props.play_order
next = PLAYERORDER_TOGGLE_MAP_REPEAT.get(now, now)
self.__gconf.set_string("/apps/rhythmbox/state/play_order", next)
# update state within a short time (don't wait for scheduled poll)
gobject.idle_add(self.poll)
def ctrl_toggle_shuffle(self):
sp = self.__shell.get_player()
now = sp.props.play_order
next = PLAYERORDER_TOGGLE_MAP_SHUFFLE.get(now, now)
self.__gconf.set_string("/apps/rhythmbox/state/play_order", next)
# update state within a short time (don't wait for scheduled poll)
gobject.idle_add(self.poll)
def ctrl_seek(self, direction):
sp = self.__shell.get_player()
try:
sp.seek(direction * 5)
except gobject.GError, e:
log.debug("seek failed: %s" % str(e))
else:
# update volume within a short time (don't wait for scheduled poll)
gobject.idle_add(self.poll)
def ctrl_volume(self, direction):
sp = self.__shell.get_player()
if direction == 0:
sp.set_volume(0)
else:
try:
sp.set_volume_relative(direction * 0.05)
except gobject.GError, e:
log.debug("set volume failed: %s" % str(e))
# update volume within a short time (don't wait for scheduled poll)
gobject.idle_add(self.poll)
# =========================================================================
# action interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
try:
self.__jump_in_plq(self.__playlist_sc, positions[0])
except gobject.GError, e:
log.debug("playlist jump failed: %s" % e)
elif action_id == IA_ENQUEUE.id:
self.__enqueue_items(ids)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_queue_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
try:
self.__jump_in_plq(self.__queue_sc, positions[0])
except gobject.GError, e:
log.debug("queue jump failed: %s" % e)
elif action_id == IA_REMOVE.id:
for id in ids:
self.__shell.remove_from_queue(id)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_mlib_item(self, action_id, path, positions, ids):
if action_id == IA_ENQUEUE.id:
self.__enqueue_items(ids)
if action_id == IA_JUMP.id:
self.action_mlib_list(LA_PLAY.id, path)
# delay jump, otherwise sync with clients sometimes fails
gobject.timeout_add(100, self.action_playlist_item, IA_JUMP.id,
positions, ids)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_mlib_list(self, action_id, path):
if action_id == LA_PLAY.id:
sc = self.__mlib_path_to_source(path)
if sc is None:
log.warning("no source for path %s" % path)
return
sp = self.__shell.get_player()
if sc != self.__playlist_sc:
try:
sp.set_selected_source(sc)
sp.set_playing_source(sc)
self.__jump_in_plq(sc, 0)
except gobject.GError, e:
log.debug("switching source failed: %s" % str(e))
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_search_item(self, action_id, positions, ids):
if action_id == IA_ENQUEUE.id:
self.__enqueue_items(ids)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
if self.__playlist_sc is None:
reply.send()
return
try:
qm = self.__playlist_sc.get_entry_view().props.model
reply.ids, reply.names = self.__get_item_list_from_qmodel(qm)
except gobject.GError, e:
log.warning("failed to get playlist items: %s" % e)
reply.item_actions = PLAYLIST_ACTIONS
reply.send()
def request_queue(self, reply):
sc = self.__queue_sc
qm = sc.props.query_model
try:
reply.ids, reply.names = self.__get_item_list_from_qmodel(qm)
except gobject.GError, e:
log.warning("failed to get queue items: %s" % e)
reply.item_actions = QUEUE_ACTIONS
reply.send()
def request_mlib(self, reply, path):
slm = self.__shell.props.sourcelist_model
### root ? ###
if not path:
for group in slm:
group_name = group[2]
reply.nested.append(group_name)
reply.send()
return
### group ? ### Library, Playlists
if len(path) == 1:
for group in slm:
group_name = group[2]
if path[0] == group_name:
for sc in group.iterchildren():
source_name = sc[2]
# FIXME: how to be l10n independent here?
if source_name.startswith("Play Queue"):
continue
if source_name.startswith("Import Error"):
continue
log.debug("append %s" % source_name)
reply.nested.append(source_name)
break
reply.list_actions = MLIB_LIST_ACTIONS
reply.send()
return
### regular playlist (source) ! ### Library/???, Playlists/???
sc = self.__mlib_path_to_source(path)
if sc is None:
reply.send()
return
qm = sc.get_entry_view().props.model
try:
reply.ids, reply.names = self.__get_item_list_from_qmodel(qm)
except gobject.GError, e:
log.warning("failed to list items: %s" % e)
reply.item_actions = MLIB_ITEM_ACTIONS
reply.send()
def request_search(self, reply, query):
def eval_entry(entry):
match = True
for key in query_stripped:
if key == "Any":
props = SEARCH_PROPS_ANY
else:
props = [key]
for prop in props:
val = db.entry_get(entry, prop).lower()
if val.find(query_stripped[key]) >= 0:
break
else:
match = False
break
if match:
id, name = self.__get_list_item_from_entry(entry)
reply.ids.append(id)
reply.names.append(name)
query_stripped = {} # stripped query dict
for key, val in zip(SEARCH_PROPS, query):
if val.strip():
query_stripped[key] = val.lower()
if query_stripped:
db = self.__shell.props.db
db.entry_foreach(eval_entry)
reply.item_actions = SEARCH_ACTIONS
reply.send()
# ==========================================================================
# callbacks
# ==========================================================================
def __notify_playing_uri_changed(self, sp, uri):
"""Shell player signal callback to handle an item change."""
log.debug("playing uri changed: %s" % uri)
db = self.__shell.props.db
entry = sp.get_playing_entry()
if entry is None:
id = None
else:
id = db.entry_get(entry, rhythmdb.PROP_LOCATION)
self.__item_id = id
self.__item_entry = entry
if entry is not None and id is not None:
info = self.__get_item_from_entry(entry)
img_data = db.entry_request_extra_metadata(entry, "rb:coverArt")
if img_data is None:
img_file = self.find_image(id)
else:
try:
img_file = "%s/rhythmbox.cover" % self.config.cache
img_data.save(img_file, "png")
except IOError, e:
log.warning("failed to save cover art (%s)" % e)
img_file = None
else:
id = None
img_file = None
info = None
self.update_item(id, info, img_file)
# a new item may result in a new position:
pfq = self.__shell.get_player().props.playing_from_queue
self.update_position(self.__get_position(), queue=pfq)
def __notify_playing_changed(self, sp, b):
"""Shell player signal callback to handle a change in playback."""
log.debug("playing changed: %s" % str(b))
if b:
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_PAUSE)
def __notify_source_changed(self, sp, source_new):
"""Shell player signal callback to handle a playlist switch."""
log.debug("source changed: %s" % str(source_new))
self.__playlist_sc = source_new
# =========================================================================
# helper methods
# =========================================================================
def __jump_in_plq(self, sc, position):
"""Do a jump within the playlist or queue.
@param sc:
either current playlist or queue source
@param position:
position to jump to
"""
if sc is None:
return
qm = sc.get_entry_view().props.model
id_to_remove_from_queue = None
sp = self.__shell.get_player()
if sp.props.playing_from_queue:
id_to_remove_from_queue = self.__item_id
found = False
i = 0
for row in qm:
if i == position:
sp.set_selected_source(sc)
sp.set_playing_source(sc)
sp.play_entry(row[0])
found = True
break
i += 1
if not found:
sp.do_next()
if id_to_remove_from_queue != None:
log.debug("remove %s from queue" % id_to_remove_from_queue)
self.__shell.remove_from_queue(id_to_remove_from_queue)
def __get_item_list_from_qmodel(self, qmodel):
"""Get all items in a query model.
@return: 2 lists - IDs and names of the items
"""
ids = []
names = []
if qmodel is None:
return (ids, names)
for row in qmodel:
id, name = self.__get_list_item_from_entry(row[0])
ids.append(id)
names.append(name)
return (ids, names)
def __get_list_item_from_entry(self, entry):
"""Get Remuco list item from a Rhythmbox entry.
@return: ID and name
"""
db = self.__shell.props.db
id = db.entry_get(entry, rhythmdb.PROP_LOCATION)
artist = db.entry_get(entry, rhythmdb.PROP_ARTIST)
title = db.entry_get(entry, rhythmdb.PROP_TITLE)
if artist and title:
name = "%s - %s" % (artist, title)
else:
name = title or artist or "Unknown"
return id, name
def __get_item_from_entry(self, entry):
"""Get a Remuco item from a Rhythmbox entry.
@return: meta information (dictionary) - also if entry is None (in this
case dummy information is returned)
"""
if entry is None:
return { remuco.INFO_TITLE : "No information" }
db = self.__shell.props.db
meta = {
remuco.INFO_TITLE : str(db.entry_get(entry, rhythmdb.PROP_TITLE)),
remuco.INFO_ARTIST: str(db.entry_get(entry, rhythmdb.PROP_ARTIST)),
remuco.INFO_ALBUM : str(db.entry_get(entry, rhythmdb.PROP_ALBUM)),
remuco.INFO_GENRE : str(db.entry_get(entry, rhythmdb.PROP_GENRE)),
remuco.INFO_BITRATE : str(db.entry_get(entry, rhythmdb.PROP_BITRATE)),
remuco.INFO_LENGTH : str(db.entry_get(entry, rhythmdb.PROP_DURATION)),
remuco.INFO_RATING : str(int(db.entry_get(entry, rhythmdb.PROP_RATING))),
remuco.INFO_TRACK : str(db.entry_get(entry, rhythmdb.PROP_TRACK_NUMBER)),
remuco.INFO_YEAR : str(db.entry_get(entry, rhythmdb.PROP_YEAR))
}
return meta
def __mlib_path_to_source(self, path):
"""Get the source object related to a library path.
@param path: must contain the source' group and name (2 element list)
"""
if len(path) != 2:
log.error("** BUG ** invalid path length: %s" % path)
return None
group_name, source_name = path
if group_name is None or source_name is None:
return None
slm = self.__shell.props.sourcelist_model
for group in slm:
if group_name == group[2]:
for source in group.iterchildren():
if source_name == source[2]:
return source[3]
def __enqueue_items(self, ids):
for id in ids:
self.__shell.add_to_queue(id)
def __get_position(self):
sp = self.__shell.get_player()
db = self.__shell.props.db
position = 0
id_now = self.__item_id
if id_now is not None:
if sp.props.playing_from_queue:
qmodel = self.__queue_sc.props.query_model
elif self.__playlist_sc is not None:
qmodel = self.__playlist_sc.get_entry_view().props.model
else:
qmodel = None
if qmodel is not None:
for row in qmodel:
id = db.entry_get(row[0], rhythmdb.PROP_LOCATION)
if id_now == id:
break
position += 1
log.debug("position: %i" % position)
return position
remuco-source-0.9.6/adapter/songbird/remuco-songbird 0000755 0000000 0000000 00000006332 11700415064 022532 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter for Songbird, implemented as an executable script."""
import dbus
from dbus.exceptions import DBusException
import gobject
import urllib
import remuco
from remuco import log
# =============================================================================
# player adapter
# =============================================================================
class SongbirdAdapter(remuco.MPRISAdapter):
def __init__(self):
remuco.MPRISAdapter.__init__(self, "songbird", "Songbird")
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
# Getting the playlist from Songbird is very slow. Further there are no
# playlist actions possible right now -> don't send the playlist.
reply.ids = ["X"]
reply.names = ["Not yet available, maybe in a later version of the Songbird adapter"]
reply.send() # no playlist actions right now,
# =========================================================================
# internal methods (overridden to fix MPRIS issues)
# =========================================================================
def _notify_track(self, track):
# The Songbird MPRIS extension does not provide real URLs in the
# meta data's location entry. Until this is fixed, quote the location
# here.
if track and "location" in track:
loc = track["location"]
try:
loc = loc.encode("UTF8")
except UnicodeDecodeError:
pass # not a unicode string
loc = urllib.quote(loc, ":/%") # '%' prevents double quoting
track["location"] = loc
remuco.MPRISAdapter._notify_track(self, track)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = SongbirdAdapter()
mg = remuco.Manager(pa, dbus_name="org.mpris.songbird")
mg.run()
remuco-source-0.9.6/adapter/totem/remotem.py 0000644 0000000 0000000 00000026110 11700415064 021044 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Totem player adapter for Remuco, implemented as a Totem plugin."""
import mimetypes
import os
import os.path
import subprocess
import gobject
import totem
import remuco
from remuco import log
# =============================================================================
# totem plugin interface
# =============================================================================
class RemucoPlugin(totem.Plugin):
def __init__(self):
totem.Plugin.__init__(self)
self.__ta = None
def activate(self, totem):
if self.__ta is not None:
return
print("create TotemAdapter")
self.__ta = TotemAdapter()
print("TotemAdapter created")
print("start TotemboxAdapter")
self.__ta.start(totem)
print("TotemAdapter started")
def deactivate(self, totem):
if self.__ta is None:
return
print("stop TotemboxAdapter")
self.__ta.stop()
print("TotemAdapter stopped")
self.__ta = None
# =============================================================================
# supported file actions
# =============================================================================
FA_SETPL = remuco.ItemAction("Set as playlist", multiple=True)
FA_ENQUEUE = remuco.ItemAction("Enqueue", multiple=True)
FILE_ACTIONS=(FA_ENQUEUE, FA_SETPL)
# =============================================================================
# totem player adapter
# =============================================================================
class TotemAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "Totem",
mime_types=remuco.MIMETYPES_AV,
volume_known=True,
playback_known=True,
progress_known=True,
file_actions=FILE_ACTIONS)
self.__to = None
self.__signal_ids = ()
self.__update_item = False
self.__md_album = None
self.__md_artist = None
self.__md_title = None
self.__last_mrl = None
self.__seek_step_initial = 5000
self.__seek_step = self.__seek_step_initial
self.__css_sid = 0
if not mimetypes.inited:
mimetypes.init()
# -------------------------------------------------------------------------
# player adapter interface
# -------------------------------------------------------------------------
def start(self, totem):
remuco.PlayerAdapter.start(self)
self.__to = totem
self.__vw = totem.get_video_widget()
self.__signal_ids = (
self.__to.connect("file-opened", self.__notify_file_opened),
self.__to.connect("file-closed", self.__notify_file_closed),
self.__to.connect("metadata-updated", self.__notify_metadata_updated)
)
self.__css_sid = 0
def stop(self):
remuco.PlayerAdapter.stop(self)
for sid in self.__signal_ids:
self.__to.disconnect(sid)
self.__signal_ids = ()
self.__to = None
self.__vw = None
def poll(self):
self.__poll_item()
self.__poll_state()
self.__poll_progress()
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
self.__to.action_play_pause()
gobject.idle_add(self.__poll_state)
def ctrl_next(self):
self.__to.action_next()
def ctrl_previous(self):
self.__to.action_previous()
def ctrl_seek(self, direction):
if not self.__to.is_seekable() or self.__seek_step == 0:
return
progress = self.__to.get_current_time()
try:
self.__to.action_seek_relative(self.__seek_step * direction) # Before accurate
except TypeError:
self.__to.action_seek_relative(self.__seek_step * direction, False) # After accurate
gobject.idle_add(self.__poll_progress)
if direction < 0 and progress < self.__seek_step:
return # no seek check
if self.__css_sid == 0:
# in 1.5 seconds, at least 3 x initial seek step should be elapsed:
self.__css_sid = gobject.timeout_add(1500, self.__check_seek_step,
progress, self.__seek_step_initial * 3, self.__seek_step + 5000)
def ctrl_volume(self, direction):
# FIXME: action_volume_relative() in 2.24 says it needs an int but it
# behaves as if it gets a float. Internally volume is set via
# the video widget, so we do it the same way here:
if direction == 0:
volume = 0
else:
volume = self.__get_volume() + (direction * 5)
volume = min(volume, 100)
volume = max(volume, 0)
self.__vw.set_property("volume", volume / 100.0)
gobject.idle_add(self.__poll_state)
def ctrl_toggle_fullscreen(self):
self.__to.action_fullscreen_toggle()
# =========================================================================
# actions interface
# =========================================================================
def action_files(self, action_id, files, uris):
if action_id == FA_ENQUEUE.id:
subprocess.Popen(["totem", "--enqueue"] + uris)
elif action_id == FA_SETPL.id:
subprocess.Popen(["totem", "--replace"] + uris)
else:
log.error("** BUG ** unexpected action ID")
# =========================================================================
# internal methods
# =========================================================================
def __get_title_from_window(self):
# FIXME: In C plugins there is a function totem_get_short_title(). I
# could not find something similar in the Python bindings that
# works for all types of media played in Totem.
# Here we grab the window title as a work around.
title = self.__to.get_main_window().get_title()
type, enc = mimetypes.guess_type(title)
if type: # looks like a file name
title = os.path.splitext(title)[0]
return title
def __check_seek_step(self, progress_before, exp_min_diff, new_step):
"""Check if a seek had some effect and adjust seek step if not.
@param progress_before:
playback progress before seeking
@param new_step:
new seek step to set if progress did not change significantly
"""
progress_now = self.__to.get_current_time()
log.debug("seek diff: %d" % abs(progress_now - progress_before))
if abs(progress_now - progress_before) < exp_min_diff:
log.debug("adjust seek step to %d" % new_step)
self.__seek_step = new_step
self.__css_sid = 0
def __poll_item(self):
try:
mrl = self.__to.get_current_mrl()
except AttributeError: # totem < 2.24
mrl = self.__to.get_main_window().get_title() # <- fake mrl
if not self.__update_item and mrl == self.__last_mrl:
return
# reset seek step
len = self.__to.get_property("stream-length")
if len < 10000:
self.__seek_step_initial = 0
else:
self.__seek_step_initial = max(5000, len // 200)
self.__seek_step = self.__seek_step_initial
log.debug("reset seek step to %d" % self.__seek_step)
# update meta information
log.debug("update item")
self.__update_item = False
self.__last_mrl = mrl
info = {}
if ((self.__md_artist, self.__md_title, self.__md_album) ==
(None, None, None)):
info[remuco.INFO_TITLE] = self.__get_title_from_window()
else:
info[remuco.INFO_ARTIST] = self.__md_artist
info[remuco.INFO_TITLE] = self.__md_title
info[remuco.INFO_ALBUM] = self.__md_album
info[remuco.INFO_LENGTH] = int(len / 1000)
img = self.find_image(mrl)
self.update_item(mrl, info, img)
def __poll_state(self):
if self.__to.is_playing():
playback = remuco.PLAYBACK_PLAY
else:
playback = remuco.PLAYBACK_PAUSE
self.update_playback(playback)
self.update_volume(self.__get_volume())
def __poll_progress(self):
progress = self.__to.get_current_time() / 1000
length = self.__to.get_property("stream-length") / 1000
self.update_progress(progress, length)
def __get_volume(self):
return int(self.__vw.get_property("volume") * 100)
def __notify_metadata_updated(self, totem, artist, title, album, track=0):
# 'track' has been added in Totem 2.26
log.debug("metadata updated: %s, %s, %s" % (artist, title, album))
# in Totem < 2.26 meta data is always None
self.__md_artist = artist
self.__md_title = title
self.__md_album = album
self.__update_item = True
def __notify_file_opened(self, totem, file):
# XXX: does not get called for podcasts from BBC plugin
log.debug("file opened: %s" % file)
self.__update_item = True
def __notify_file_closed(self, totem):
log.debug("file closed")
self.__update_item = True
remuco-source-0.9.6/adapter/totem/remuco.totem-plugin 0000644 0000000 0000000 00000000316 11700415064 022662 0 ustar root root 0000000 0000000 [Totem Plugin]
Loader=python
Module=remotem
IAge=1
Name=Remuco
Description=Remuco Totem Adapter
Authors=Remuco team
Copyright=Copyright © 2006-2010 by the Remuco team
Website=http://remuco.googlecode.com/
remuco-source-0.9.6/adapter/tvtime/remuco-tvtime 0000755 0000000 0000000 00000013525 11700415064 021736 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""TVtime adapter for Remuco, implemented as an executable script."""
import commands
import os
import os.path
from xml.dom import minidom
import remuco
from remuco import log
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
PLAYLIST_ACTIONS = (IA_JUMP,)
# =============================================================================
# MPD player adapter
# =============================================================================
class TVtimeAdapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "TVtime", poll=10,
playback_known=True)
stations_file = os.path.join(os.getenv("HOME", "/"), ".tvtime",
"stationlist.xml")
if not os.path.exists(stations_file):
log.warning("station list file %s does not exist" % stations_file)
self.__stations_file = None
else:
self.__stations_file = stations_file
def start(self):
remuco.PlayerAdapter.start(self)
self.update_item("XYZ", {remuco.INFO_TITLE: "TVtime"}, None)
def stop(self):
remuco.PlayerAdapter.stop(self)
log.debug("bye, turning off the light")
def poll(self):
status, output = commands.getstatusoutput("tvtime-command NOOP")
if status == os.EX_OK:
self.update_playback(remuco.PLAYBACK_PLAY)
else:
self.update_playback(remuco.PLAYBACK_STOP)
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
self.__tvcmd("MENU_ENTER")
def ctrl_next(self):
self.__tvcmd("RIGHT")
def ctrl_previous(self):
self.__tvcmd("LEFT")
def ctrl_volume(self, volume):
if volume == 0:
self.__tvcmd("TOGGLE_MUTE")
elif volume < 0:
self.__tvcmd("DOWN")
else:
self.__tvcmd("UP")
def ctrl_toggle_repeat(self):
self.__tvcmd("SHOW_MENU")
def ctrl_toggle_shuffle(self):
self.__tvcmd("SHOW_MENU")
def ctrl_toggle_fullscreen(self):
self.__tvcmd("TOGGLE_FULLSCREEN")
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
if not self.__stations_file:
reply.send()
return
xdoc = minidom.parse(self.__stations_file)
node = xdoc.getElementsByTagName("stationlist")[0]
node = node.getElementsByTagName("list")[0]
station_nodes = node.getElementsByTagName("station")
for node in station_nodes:
active = node.getAttribute("active")
if active == "0":
continue
reply.ids.append(node.getAttribute("position"))
reply.names.append(node.getAttribute("name"))
reply.item_actions = PLAYLIST_ACTIONS
reply.send()
# =========================================================================
# action interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
channel = ids[0]
cmd = ""
for number in channel:
cmd += "CHANNEL_%s " % number
cmd += "ENTER"
self.__tvcmd(cmd)
else:
log.error("** BUG ** unexpected playlist item action")
# =========================================================================
# internal methods
# =========================================================================
def __tvcmd(self, cmd):
retval, output = commands.getstatusoutput("tvtime-command %s" % cmd)
if retval != os.EX_OK:
log.warning("command '%s' failed: %s" % (cmd, output))
# =============================================================================
# main
# =============================================================================
def run_check():
"""Check if TVTime is running."""
return commands.getstatusoutput("tvtime-command NOOP")[0] == 0
if __name__ == '__main__':
pa = TVtimeAdapter()
mg = remuco.Manager(pa, poll_fn=run_check)
mg.run()
remuco-source-0.9.6/adapter/vlc/remuco-vlc 0000755 0000000 0000000 00000006273 11700415064 020470 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter for VLC, implemented as an executable script."""
import os.path
import gobject
import remuco
from remuco import log
# =============================================================================
# player adapter
# =============================================================================
class VLCAdapter(remuco.MPRISAdapter):
def __init__(self):
remuco.MPRISAdapter.__init__(self, "vlc", "VLC",
mime_types=remuco.MIMETYPES_AV)
self.__retry_track_active = False
def start(self):
self.__retry_track_active = False
remuco.MPRISAdapter.start(self)
def _notify_track(self, track={}):
# vlc passes no argument after startup -> make argument a keyword
# vlc may need some extra time to get length of a track
if (not "length" in track and not "mtime" in track and
not self.__retry_track_active):
self.__retry_track_active = True
gobject.timeout_add(500, self.__retry_track)
return
self.__retry_track_active = False
# vlc provides length in 'length', not in 'time' or 'mtime'
if "length" in track and not "mtime" in track:
track["mtime"] = track["length"]
# vlc may provide title in 'nowplaying'
if "nowplaying" in track and not "title" in track:
track["title"] = track["nowplaying"]
remuco.MPRISAdapter._notify_track(self, track)
def __retry_track(self):
log.debug("retry to get track data")
try:
self._mp_p.GetMetadata(reply_handler=self._notify_track,
error_handler=self._dbus_error)
except DBusException, e:
# this is not necessarily a fatal error
log.warning("dbus error: %s" % e)
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = VLCAdapter()
mg = remuco.Manager(pa, dbus_name="org.mpris.vlc")
mg.run()
remuco-source-0.9.6/adapter/xmms2/install-check.py 0000644 0000000 0000000 00000000603 11700415064 022032 0 ustar root root 0000000 0000000 import sys
try:
import xmmsclient
except ImportError, e:
print("")
print("+-----------------------------------------------------------------+")
print("| Unsatisfied Python requirement: %s." % e)
print("| Please install the missing module and then retry.")
print("+-----------------------------------------------------------------+")
print("")
sys.exit(1) remuco-source-0.9.6/adapter/xmms2/remuco-xmms2 0000755 0000000 0000000 00000065437 11700415064 021243 0 ustar root root 0000000 0000000 #!/usr/bin/python
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""XMMS2 adapter for Remuco, implemented as an XMMS2 startup script."""
import os
import gobject
import xmmsclient
try:
from xmmsclient import XMMSValue as X2RV # XMMS2 >= 0.6
except ImportError:
from xmmsclient import XMMSResult as X2RV # XMMS2 < 0.6
import xmmsclient.glib
import xmmsclient.collections as xc
import remuco
from remuco import log
# =============================================================================
# XMMS2 related constants
# =============================================================================
MINFO_KEYS_ART = ("picture_front", "album_front_large", "album_front_small",
"album_front_thumbnail")
MINFO_KEY_TAGS = "tag"
MINFO_KEY_RATING = "rating"
BIN_DATA_DIR = "%s/bindata" % xmmsclient.userconfdir_get()
ERROR_DISCONNECTED = "disconnected"
PLAYLIST_ID_ACTIVE = "_active"
SEARCH_MASK = ["Artist", "Title", "Album", "Genre"]
VARIOUS_ARTISTS = "Various Artists"
BLANK_ARTIST = "[unknown]"
BLANK_TITLE = "[untitled]"
# =============================================================================
# actions
# =============================================================================
IA_JUMP = remuco.ItemAction("Jump to")
IA_REMOVE = remuco.ItemAction("Remove", multiple=True)
PLAYLIST_ITEM_ACTIONS = (IA_JUMP, IA_REMOVE)
LA_LOAD = remuco.ListAction("Load")
MLIB_LIST_ACTIONS = (LA_LOAD,)
IA_APPEND = remuco.ItemAction("Enqueue", multiple=True)
IA_PLAY_NEXT = remuco.ItemAction("Play next", multiple=True)
MLIB_ITEM_ACTIONS = (IA_APPEND, IA_PLAY_NEXT)
# =============================================================================
# helper classes
# =============================================================================
class ItemListRequest():
def __init__(self, reply, pa, path, search=False):
"""Create a new item list request.
@param reply: the request's ListReply object
@param pa: XMMS2Adapter
@param path: path of the requested item list
@keyword search: if True then path is a search query
"""
self.__reply = reply
self.__pa = pa
self.__path = path
self.__pl_ids = []
self.__pl_tracks = []
x2 = self.__pa._x2
roots = [ 'Playlists', 'Collections', 'Albums', 'Artists', 'Tracks' ]
if search:
self.__reply.item_actions = MLIB_ITEM_ACTIONS
match = None
for field, value in zip(SEARCH_MASK, path):
value = value.strip()
if not value:
continue
value = "*%s*" % value
field = field.lower()
match = xc.Match(match, field=field, value=value)
if match is None:
reply.send()
else:
x2.coll_query_infos(match, ['artist', 'title', 'id'],
cb=self.__bil_list_of_tracks)
elif not path:
self.__reply.nested = roots
self.__reply.send()
elif path[0] not in roots:
log.error("** BUG ** unexpected path: %s" % path)
elif len(path) == 1:
if path[0] == "Playlists":
self.__reply.list_actions = MLIB_LIST_ACTIONS
x2.coll_list(path[0], cb=self.__bil_list_of_colls)
if path[0] == "Collections":
x2.coll_list(path[0], cb=self.__bil_list_of_colls)
elif path[0] == "Albums":
match = xc.Has(xc.Universe(), 'album')
x2.coll_query_infos(match, ['album', 'artist', 'compilation'],
groupby=['album', 'artist'],
cb=self.__bil_list_of_albums)
elif path[0] == 'Artists':
artists = xc.Has(xc.Universe(), 'artist')
match = xc.Intersection(xc.Complement(xc.Has(xc.Universe(),
'compilation')), artists)
match = xc.Union(match, xc.Match(artists, field='compilation',
value='0'))
x2.coll_query_infos(match, ['artist'], groupby=['artist'],
cb=self.__bil_list_of_artists)
elif path[0] == 'Tracks':
match = xc.Has(xc.Universe(), 'title')
x2.coll_query_infos(match, ['title', 'artist', 'id'],
groupby=['title', 'artist'],
cb=self.__bil_list_of_tracks)
elif len(path) == 2:
if path[0] == 'Playlists':
if path[1] == PLAYLIST_ID_ACTIVE:
self.__reply.item_actions = PLAYLIST_ITEM_ACTIONS
else:
self.__reply.item_actions = MLIB_ITEM_ACTIONS
x2.playlist_list_entries(playlist=path[1],
cb=self.__handle_pl_ids)
elif path[0] == 'Collections':
self.__reply.item_actions = MLIB_ITEM_ACTIONS
x2.coll_get(path[1], ns="Collections",
cb=self.__handle_collection)
elif path[0] == 'Albums':
self.__reply.item_actions = MLIB_ITEM_ACTIONS
album, artist = path[1].split("\n")
if artist == BLANK_ARTIST:
artist = ""
elif artist == VARIOUS_ARTISTS:
artist = None
match = xc.Match(xc.Universe(), field='album', value=album)
if artist is not None:
match = xc.Match(match, field='artist', value=artist)
x2.coll_query_infos(match, ['title', 'artist', 'id',
'compilation', 'tracknr'], order=['tracknr'],
cb=self.__bil_items_in_album)
elif path[0] == 'Artists':
self.__reply.item_actions = MLIB_ITEM_ACTIONS
match = xc.Match(xc.Universe(), field='artist', value=path[1])
x2.coll_query_infos(match, ['title', 'album', 'id',
'compilation'],
cb=self.__bil_items_and_albums_of_artist)
elif len(path) == 3:
if path[0] == 'Artists':
self.__reply.item_actions = MLIB_ITEM_ACTIONS
match = xc.Match(xc.Universe(), field='artist', value=path[1])
match = xc.Match(match, field='album', value=path[2])
x2.coll_query_infos(match, ['title', 'id', 'tracknr'],
order=['tracknr'],
cb=self.__bil_items_in_album)
else:
log.error("** BUG ** unexpected path: %s" % path)
def __handle_pl_ids(self, result):
"""Collects track infos for the tracks in the playlist ID list."""
if not self.__pa._check_result(result):
return
self.__pl_ids = result.value()
log.debug("playlist ids: %s" % self.__pl_ids)
self.__request_next_pl_track()
def __request_next_pl_track(self):
"""Requests track info for the next track in the playlist ID list."""
if len(self.__pl_tracks) < len(self.__pl_ids):
# proceed in getting item names
id = self.__pl_ids[len(self.__pl_tracks)]
self.__pa._x2.medialib_get_info(id, cb=self.__handle_pl_track)
else:
# have all item names
self.__bil_list_of_tracks(self.__pl_tracks)
def __handle_pl_track(self, result):
"""Adds a track to the playlist track list and requests the next."""
if not self.__pa._check_result(result):
return
self.__pl_tracks.append(result.value())
self.__request_next_pl_track()
def __handle_collection(self, result):
"""Requests a track info list for a collection."""
if not self.__pa._check_result(result):
return
coll = result.value()
self.__pa._x2.coll_query_infos(coll, ['title', 'artist', 'id'],
cb=self.__bil_list_of_tracks)
def __bil_list_of_colls(self, result):
"""Builds an item list with all collections of a specific namespace."""
if not self.__pa._check_result(result):
return
colls = result.value()
self.__reply.nested = [ i for i in colls if not i.startswith("_") ]
self.__reply.send()
def __bil_list_of_tracks(self, result):
"""Builds an item list of a non-specific list of tracks."""
if isinstance(result, X2RV):
if not self.__pa._check_result(result):
return
tracks = result.value()
else:
tracks = result
for minfo in tracks:
self.__reply.ids.append(minfo['id'])
self.__reply.names.append(self.__get_item_name(minfo))
self.__reply.send()
def __bil_list_of_albums(self, result):
"""Builds an item list of all albums."""
if not self.__pa._check_result(result):
return
albums = set()
for x in result.value():
if not x['album']:
continue
elif x['compilation']:
albums.add("%s\n%s" % (x['album'], VARIOUS_ARTISTS))
else:
albums.add("%s\n%s" % (x['album'], x['artist'] or BLANK_ARTIST))
self.__reply.nested = sorted(albums)
self.__reply.send()
def __bil_list_of_artists(self, result):
"""Builds an item list of all artists."""
if not self.__pa._check_result(result):
return
self.__reply.nested = map(lambda x: x['artist'], result.value())
self.__reply.send()
def __bil_items_in_album(self, result):
"""Builds an item list for a specific album."""
if not self.__pa._check_result(result):
return
for minfo in result.value():
number = minfo.get('tracknr', 0) and "%s. " % minfo['tracknr'] or ""
track = "%s%s" % (number, minfo.get('title', BLANK_TITLE))
if minfo.get('compilation', False):
track += " / %s" % minfo.get('artist', BLANK_ARTIST)
self.__reply.names.append(track)
self.__reply.ids.append(minfo['id'])
self.__reply.send()
def __bil_items_and_albums_of_artist(self, result):
"""Builds an item list for a specific artist."""
if not self.__pa._check_result(result):
return
albums = set()
for minfo in result.value():
if not minfo.get('album') or minfo.get('compilation'):
name = minfo.get('title', BLANK_TITLE)
if minfo.get('album'):
name += " / %s" % minfo['album']
self.__reply.names.append(name)
self.__reply.ids.append(minfo['id'])
else:
albums.add(minfo['album'])
self.__reply.nested = sorted(albums)
self.__reply.send()
def __get_item_name(self, minfo, need=None):
"""Get a standard item name.
@param minfo: track info dict
@keyword need: list of required tags (artist, title)
@return: a name composed of artist and title or None if one the tags
in 'need' is None or the empty string
"""
if need and 'title' in need and not minfo.get('title'):
return None
if need and 'artist' in need and not minfo.get('artist'):
return None
artist = minfo.get('artist', BLANK_ARTIST)
title = minfo.get('title', BLANK_TITLE)
return "%s / %s" % (title, artist)
# =============================================================================
# player adapter
# =============================================================================
class XMMS2Adapter(remuco.PlayerAdapter):
def __init__(self):
remuco.PlayerAdapter.__init__(self, "XMMS2",
max_rating=5,
shuffle_known=True,
playback_known=True,
volume_known=True,
progress_known=True,
search_mask=SEARCH_MASK)
self.__state_playback = remuco.PLAYBACK_STOP
self.__state_volume = 0
self.__state_position = 0
self.__item_id_int = None # id as integer
self.__item_id = None # id as string
self.__item_meta = None
self.__item_len = 0 # for update_progress()
self.__shuffle_off_sid = 0
self._x2 = xmmsclient.XMMS("remuco")
self.__x2_glib_connector = None
def start(self):
remuco.PlayerAdapter.start(self)
try:
self._x2.connect(path=os.getenv("XMMS_PATH"),
disconnect_func=self.__notify_disconnect)
except IOError, e:
raise StandardError("failed to connect to XMMS2: %s" % e)
self.__x2_glib_connector = xmmsclient.glib.GLibConnector(self._x2)
self._x2.broadcast_playback_current_id(self.__notify_id)
self._x2.broadcast_playback_status(self.__notify_playback)
self._x2.broadcast_playback_volume_changed(self.__notify_volume)
self._x2.broadcast_playlist_current_pos(self.__notify_position)
# to dectect all posistion changes:
self._x2.broadcast_playlist_changed(self.__notify_playlist_change)
self._x2.signal_playback_playtime(self.__notify_progress)
# get initial player state (broadcasts only work on changes):
self._x2.playback_current_id(cb=self.__notify_id)
self._x2.playback_status(cb=self.__notify_playback)
self._x2.playback_volume_get(cb=self.__notify_volume)
self._x2.playlist_current_pos(cb=self.__notify_position)
self.__item_len = 0
def stop(self):
remuco.PlayerAdapter.stop(self)
if self.__shuffle_off_sid > 0:
gobject.source_remove(self.__shuffle_off_sid)
self.__shuffle_off_sid = 0
self._x2 = None
self.__x2_glib_connector = None
def poll(self):
self._x2.playback_playtime(cb=self.__notify_progress)
# =========================================================================
# control interface
# =========================================================================
def ctrl_next(self):
self._x2.playlist_set_next_rel(1, cb=self.__ignore_result)
self._x2.playback_tickle(cb=self.__ignore_result)
def ctrl_previous(self):
if self.__state_position > 0:
self._x2.playlist_set_next_rel(-1, cb=self.__ignore_result)
self._x2.playback_tickle(cb=self.__ignore_result)
def ctrl_toggle_playing(self):
if (self.__state_playback == remuco.PLAYBACK_STOP or
self.__state_playback == remuco.PLAYBACK_PAUSE):
self._x2.playback_start(cb=self.__ignore_result)
else:
self._x2.playback_pause(cb=self.__ignore_result)
def ctrl_toggle_shuffle(self):
self._x2.playlist_shuffle(cb=self.__ignore_result)
self.update_shuffle(True)
# emulate shuffle mode: show shuffle state for a second
if self.__shuffle_off_sid > 0:
gobject.source_remove(self.__shuffle_off_sid)
self.__shuffle_off_sid = gobject.timeout_add(1000, self.__shuffle_off)
def ctrl_seek(self, direction):
self._x2.playback_seek_ms_rel(direction * 5000, cb=self.__ignore_result)
self.poll()
def ctrl_volume(self, direction):
# TODO: currently this fails, problem relates to xmms2 installation
if direction == 0:
volume = 0
else:
volume = self.__state_volume + 5 * direction
volume = min(volume, 100)
volume = max(volume, 0)
for chan in ("right", "left"):
self._x2.playback_volume_set(chan, volume, cb=self.__ignore_result)
def ctrl_rate(self, rating):
if self.__item_id_int == 0:
return
self._x2.medialib_property_set(self.__item_id_int, MINFO_KEY_RATING,
rating, cb=self.__ignore_result)
def ctrl_tag(self, id, tags):
try:
id_int = int(id)
except ValueError:
log.error("** BUG ** id is not an int")
return
s = ""
for tag in tags:
s = "%s,%s" % (s, tag)
self._x2.medialib_property_set(id_int, MINFO_KEY_TAGS, s,
cb=self.__ignore_result)
# =========================================================================
# actions interface
# =========================================================================
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_JUMP.id:
self._x2.playlist_set_next(positions[0], cb=self.__ignore_result)
self._x2.playback_tickle(cb=self.__ignore_result)
if self.__state_playback != remuco.PLAYBACK_PLAY:
self._x2.playback_start(cb=self.__ignore_result)
elif action_id == IA_REMOVE.id:
positions.sort()
positions.reverse()
for pos in positions:
log.debug("remove %d from playlist" % pos)
self._x2.playlist_remove_entry(pos, cb=self.__ignore_result)
else:
log.error("** BUG ** unexpected playlist item action")
def action_mlib_item(self, action_id, path, positions, ids):
if action_id == IA_APPEND.id:
for id in ids:
id = int(id)
self._x2.playlist_add_id(id, cb=self.__ignore_result)
elif action_id == IA_PLAY_NEXT.id:
pos = self.__state_position + 1
ids.reverse()
for id in ids:
id = int(id)
self._x2.playlist_insert_id(pos, id, cb=self.__ignore_result)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_mlib_list(self, action_id, path):
if action_id == LA_LOAD.id:
if len(path) == 2 and path[0] == "Playlists":
self._x2.playlist_load(path[1], cb=self.__ignore_result)
self._x2.playlist_set_next(0, cb=self.__ignore_result)
self._x2.playback_tickle(cb=self.__ignore_result)
if self.__state_playback != remuco.PLAYBACK_PLAY:
self._x2.playback_start(cb=self.__ignore_result)
else:
log.error("** BUG ** unexpected path: %s" % path)
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_search_item(self, action_id, positions, ids):
self.action_mlib_item(action_id, None, positions, ids)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
ItemListRequest(reply, self, ['Playlists', PLAYLIST_ID_ACTIVE])
def request_mlib(self, reply, path):
ItemListRequest(reply, self, path)
def request_search(self, reply, query):
ItemListRequest(reply, self, query, search=True)
# =========================================================================
# internal methods
# =========================================================================
def _check_result(self, result):
""" Check the result of a request sent to XMMS2. """
try:
ie = result.is_error() # XMMS2 >= 0.6
except AttributeError:
ie = result.iserror() # XMMS2 < 0.6
if not ie:
return True
err = result.get_error()
if err.lower() == ERROR_DISCONNECTED:
log.warning("lost connection to XMMS2")
self.manager.stop()
else:
log.warning("error result: %s" % err)
return False
def __notify_id(self, result):
if not self._check_result(result):
self.update_item(None, None, None)
return
self.__item_id_int = result.value()
self.__item_id = str(self.__item_id_int)
log.debug("new item id: %u" % self.__item_id_int)
if self.__item_id_int == 0:
self.update_item(None, None, None)
return
self._x2.medialib_get_info(self.__item_id_int, cb=self.__handle_info)
def __handle_info(self, result):
"""Callback to handle meta data requested for the current item."""
if not self._check_result(result):
self.__item_id_int = 0
self.__item_id = str(self.__item_id_int)
self.update_item(None, None, None)
return
minfo = result.value()
info = {}
info[remuco.INFO_ARTIST] = minfo.get('artist', "")
info[remuco.INFO_ALBUM] = minfo.get('album', "")
info[remuco.INFO_TITLE] = minfo.get('title', "")
info[remuco.INFO_GENRE] = minfo.get('genre', "")
info[remuco.INFO_YEAR] = minfo.get('year', "")
info[remuco.INFO_BITRATE] = int(minfo.get('bitrate', 0) / 1000)
info[remuco.INFO_RATING] = minfo.get(MINFO_KEY_RATING, 0)
info[remuco.INFO_TAGS] = minfo.get(MINFO_KEY_TAGS, "")
self.__item_len = int(minfo.get('duration', 0) // 1000)
info[remuco.INFO_LENGTH] = self.__item_len
img = None
for img_key in MINFO_KEYS_ART:
img = minfo.get(img_key)
if img:
img = "%s/%s" % (BIN_DATA_DIR, img)
break
if not img:
url = minfo.get('url').replace("+", "%20")
img = self.find_image(url)
self.update_item(self.__item_id, info, img)
self.poll() # update progress
def __notify_playback(self, result):
if not self._check_result(result):
return
val = result.value()
if val == xmmsclient.PLAYBACK_STATUS_PAUSE:
self.__state_playback = remuco.PLAYBACK_PAUSE
elif val == xmmsclient.PLAYBACK_STATUS_PLAY:
self.__state_playback = remuco.PLAYBACK_PLAY
elif val == xmmsclient.PLAYBACK_STATUS_STOP:
self.__state_playback = remuco.PLAYBACK_STOP
else:
log.error("** BUG ** unknown XMMS2 playback status: %d", val)
return
self.update_playback(self.__state_playback)
def __notify_progress(self, result):
if not self._check_result(result):
return
progress = int(result.value() // 1000)
progress = min(progress, self.__item_len)
progress = max(progress, 0)
self.update_progress(progress, self.__item_len)
def __notify_volume(self, result):
if not self._check_result(result):
return
val = result.value()
volume = 0
i = 0
for v in val.values():
volume += v
i += 1
volume = volume / i
self.__state_volume = volume
self.update_volume(self.__state_volume)
def __notify_position(self, result):
if not self._check_result(result):
return
self.__state_position = result.value()['position']
self.update_position(self.__state_position)
def __notify_playlist_change(self, result):
if not self._check_result(result):
return
# change in playlist may result in position change:
self._x2.playlist_current_pos(cb=self.__notify_position)
def __notify_disconnect(self, result):
log.info("xmms2 disconnected")
self.manager.stop()
def __ignore_result(self, result):
"""Handle an XMMS2 result which is not of interest."""
self._check_result(result)
def __shuffle_off(self):
"""Timeout callback to disable the pseudo shuffle."""
self.update_shuffle(False)
self.__shuffle_off_sid = 0
# =============================================================================
# main
# =============================================================================
if __name__ == '__main__':
pa = XMMS2Adapter()
mg = remuco.Manager(pa)
mg.run()
remuco-source-0.9.6/base/module/install-check.py 0000644 0000000 0000000 00000001027 11700415064 021544 0 ustar root root 0000000 0000000 import sys
try:
import gobject
if sys.platform.startswith("linux"):
import xdg.BaseDirectory
import dbus
import Image
import logging
import bluetooth
except ImportError, e:
print("")
print("+-----------------------------------------------------------------+")
print("| Unsatisfied Python requirement: %s." % e)
print("| Please install the missing module and then retry.")
print("+-----------------------------------------------------------------+")
print("")
sys.exit(1) remuco-source-0.9.6/base/module/remuco/__init__.py 0000644 0000000 0000000 00000007047 11700415064 022064 0 ustar root root 0000000 0000000 # -*- coding: UTF-8 -*-
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco player adapter module.
The module 'remuco' provides classes and constants for Remuco player adapters.
Class PlayerAdapter:
Base class for player adapters (start reading here).
Class MPRISAdapter:
Base class for player adapters for MPRIS players.
Classes ItemAction and ListAction:
Classes to define actions clients may execute in their media browser.
Class ListReply:
Used by player adapters to reply requests for playlists and other lists.
Class Manager:
Helper class for managing the life cycle of a player adapter.
Constants:
The constants starting with 'INFO' are keys to be used for the dictionary
describing an item (a playable object: song, video, slide, picture, ...).
The constants starting with 'PLAYBACK' are the values used by Remuco to
describe a playback state.
Logging:
It is recommended to use the remuco logging system within player adapters.
To do so, import the module 'remuco.log' and use the functions
* remuco.log.debug(),
* remuco.log.info(),
* remuco.log.warning() and
* remuco.log.error().
Then all messages of the player adapter will be written into the same file
as used internally by the remuco module - that makes debugging a lot easier.
Internally Remuco uses the module 'logging' for all its logging messages.
Messages go into a player specific log file (usually
~/.cache/remuco/PLAYER/log). The log level is defined in a player specific
configuration file (usually ~/.config/remuco/PLAYER/conf).
"""
#==============================================================================
# imports
#==============================================================================
from remuco.adapter import PlayerAdapter, ItemAction, ListAction, ListReply
from remuco.adapter import MIMETYPES_AUDIO, MIMETYPES_VIDEO, MIMETYPES_AV
from remuco.config import Config
from remuco.defs import *
from remuco.manager import Manager
from remuco.mpris import MPRISAdapter
#==============================================================================
# exports
#==============================================================================
__all__ = ["PlayerAdapter", "ListReply", "MPRISAdapter",
"ItemAction", "ListAction", "Manager", "Config",
"INFO_ALBUM", "INFO_ARTIST", "INFO_GENRE", "INFO_LENGTH",
"INFO_RATING", "INFO_TAGS", "INFO_TITLE", "INFO_YEAR",
"PLAYBACK_PAUSE", "PLAYBACK_PLAY", "PLAYBACK_STOP",
"MIMETYPES_AUDIO", "MIMETYPES_VIDEO", "MIMETYPES_AV",
]
__version__ = REMUCO_VERSION
remuco-source-0.9.6/base/module/remuco/adapter.py 0000644 0000000 0000000 00000145445 11700415064 021752 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import commands
import inspect
import math # for ceiling
import os
import os.path
import urllib
import urlparse
import gobject
from remuco import art
from remuco import config
from remuco import files
from remuco import log
from remuco import message
from remuco import net
from remuco import serial
from remuco.defs import *
from remuco.features import *
from remuco.data import PlayerInfo, PlayerState, Progress, ItemList, Item
from remuco.data import Control, Action, Tagging, Request
from remuco.manager import NoManager
# =============================================================================
# reply class for requests
# =============================================================================
class ListReply(object):
"""Reply object for an item list request.
A ListReply is the first parameter of the request methods
PlayerAdapter.request_playlist(), PlayerAdapter.request_queue(),
PlayerAdapter.request_mlib() and PlayerAdapter.request_search().
Player adapters are supposed to use the list reply object to set the
reply data (using properties 'ids', 'names', 'item_actions' and
'nested', 'list_actions') and to send the reply to clients (using send()).
"""
def __init__(self, client, request_id, reply_msg_id, page, path=None):
"""Create a new list reply.
Used internally, not needed within player adapters.
@param client: the client to send the reply to
@param request_id: the request's ID
@param reply_msg_id: the message ID of the client's request
@param page: page of the requested list
@keyword path: path of the requested list, if there is one
"""
self.__client = client
self.__request_id = request_id
self.__reply_msg_id = reply_msg_id
self.__page = page
self.__path = path
self.__nested = []
self.__ids = []
self.__names = []
self.__list_actions = []
self.__item_actions = []
def send(self):
"""Send the requested item list to the requesting client."""
### paging ###
page_size = self.__client.info.page_size
len_all = len(self.__ids or []) + len(self.__nested or [])
# P3K: remove float() and int()
page_max = int(max(math.ceil(float(len_all) / page_size) - 1, 0))
# number of pages may have changed since client sent the request
self.__page = min(self.__page, page_max)
index_start = self.__page * page_size
index_end = index_start + page_size
nested, ids, names = [], [], []
item_offset = 0
if self.__nested and index_start < len(self.__nested):
# page contains nested lists and maybe items
nested = self.__nested[index_start:index_end]
if len(nested) < page_size:
# page contains nested lists and items
num_items = page_size - len(nested)
ids = self.__ids[0:num_items]
names = self.__names[0:num_items]
else:
# page contains only items
index_start -= len(self.__nested)
index_end -= len(self.__nested)
ids = self.__ids[index_start:index_end]
names = self.__names[index_start:index_end]
item_offset = index_start
### sending ###
ilist = ItemList(self.__request_id,
self.__path, nested, ids, names, item_offset,
self.__page, page_max,
self.__item_actions, self.__list_actions)
msg = net.build_message(self.__reply_msg_id, ilist)
gobject.idle_add(self.__client.send, msg)
# === property: ids ===
def __pget_ids(self):
"""IDs of the items contained in a list.
Player adapters should set this to a list of IDs of the items contained
in the requested list.
"""
return self.__ids
def __pset_ids(self, value):
self.__ids = value
ids = property(__pget_ids, __pset_ids, None, __pget_ids.__doc__)
# === property: names ===
def __pget_names(self):
"""Names of the items contained in a list.
Player adapters should set this to a list of names of the items
contained in the requested list. Good choice for a name is combination
of artist and title.
"""
return self.__names
def __pset_names(self, value):
self.__names = value
names = property(__pget_names, __pset_names, None, __pget_names.__doc__)
# === property: nested ===
def __pget_nested(self):
"""Names of nested lists contained in a list.
Player adapters should set this to a list of names of the nested lists
contained in the requested list. To be used only for mlib requests (see
PlayerAdapter.request_mlib()).
"""
return self.__nested
def __pset_nested(self, value):
self.__nested = value
nested = property(__pget_nested, __pset_nested, None, __pget_nested.__doc__)
# === property: item_actions ===
def __pget_item_actions(self):
"""A list of actions clients can apply to items in the list.
The list must contain ItemAction objects.
"""
return self.__item_actions
def __pset_item_actions(self, value):
self.__item_actions = value
item_actions = property(__pget_item_actions, __pset_item_actions, None,
__pget_item_actions.__doc__)
# === property: list_actions ===
def __pget_list_actions(self):
"""A list of actions clients can apply to nested lists in the list.
The list must contain ListAction objects.
"""
return self.__list_actions
def __pset_list_actions(self, value):
self.__list_actions = value
list_actions = property(__pget_list_actions, __pset_list_actions, None,
__pget_list_actions.__doc__)
# =============================================================================
# media browser actions
# =============================================================================
class ListAction(object):
"""List related action for a client's media browser.
A list action defines an action a client may apply to a list from the
player's media library. If possible, player adapters may define list
actions and send them to clients via PlayerAdapter.replay_mlib_request()
Clients may then use these actions which results in a call to
PlayerAdapter.action_mlib_list().
@see: PlayerAdapter.action_mlib_list()
"""
__id_counter = 0
def __init__(self, label):
"""Create a new action for lists from a player's media library.
@param label:
label of the action (keep short, ideally this is just a single word
like 'Load', ..)
"""
ListAction.__id_counter -= 1
self.__id = ListAction.__id_counter
self.label = label
def __str__(self):
return "(%d, %s)" % (self.__id, self.label)
# === property: id ===
def __pget_id(self):
"""ID of the action (auto-generated, read only)"""
return self.__id
id = property(__pget_id, None, None, __pget_id.__doc__)
class ItemAction(object):
"""Item related action for a client's media browser.
An item action defines an action a client may apply to a file from the
local file system, to an item from the playlist, to an item from the play
queue or to an item from the player's media library.
If possible, player adapters should define item actions and send them to
clients by setting the keyword 'file_actions' in PlayerAdapter.__init__(),
via PlayerAdapter.reply_playlist_request(), via
PlayerAdapter.reply_queue_request() or via
PlayerAdapter.reply_mlib_request(). Clients may then use these actions
which results in a call to PlayerAdapter.action_files(),
PlayerAdapter.action_playlist_item(), PlayerAdapter.action_queue_item() or
PlayerAdapter.action_mlib_item().
@see: PlayerAdapter.action_files()
@see: PlayerAdapter.action_playlist()
@see: PlayerAdapter.action_queue()
@see: PlayerAdapter.action_mlib_item()
"""
__id_counter = 0
def __init__(self, label, multiple=False):
"""Create a new action for items or files.
@param label:
label of the action (keep short, ideally this is just a single word
like 'Enqueue', 'Play', ..)
@keyword multiple:
if the action may be applied to multiple items/files or only to a
single item/file
"""
ItemAction.__id_counter += 1
self.__id = ItemAction.__id_counter
self.label = label
self.multiple = multiple
def __str__(self):
return "(%d, %s, %s)" % (self.id, self.label, self.multiple)
# === property: id ===
def __pget_id(self):
"""ID of the action (auto-generated, read only)"""
return self.__id
id = property(__pget_id, None, None, __pget_id.__doc__)
# =============================================================================
# player adapter
# =============================================================================
MIMETYPES_AUDIO = ("audio", "application/ogg")
MIMETYPES_VIDEO = ("video",)
MIMETYPES_AV = MIMETYPES_AUDIO + MIMETYPES_VIDEO
class PlayerAdapter(object):
'''Base class for Remuco player adapters.
Remuco player adapters must subclass this class and override certain
methods to implement player specific behavior. Additionally PlayerAdapter
provides methods to interact with Remuco clients. Following is a summary
of all relevant methods, grouped by functionality.
===========================================================================
Methods to extend to manage life cycle
===========================================================================
* start()
* stop()
A PlayerAdapter can be started and stopped with start() and stop().
The same instance of a PlayerAdapter should be startable and stoppable
multiple times.
Subclasses of PlayerAdapter may override these methods as needed but
must always call the super class implementations too!
===========================================================================
Methods to override to control the media player:
===========================================================================
* ctrl_toggle_playing()
* ctrl_toggle_repeat()
* ctrl_toggle_shuffle()
* ctrl_toggle_fullscreen()
* ctrl_next()
* ctrl_previous()
* ctrl_seek()
* ctrl_volume()
* ctrl_rate()
* ctrl_tag()
* ctrl_navigate()
* action_files()
* action_playlist_item()
* action_queue_item()
* action_mlib_item()
* action_mlib_list()
* action_search_item()
Player adapters only need to implement only a *subset* of these
methods - depending on what is possible and what makes sense.
Remuco checks which methods have been overridden and uses this
information to notify Remuco clients about capabilities of player
adapters.
===========================================================================
Methods to override to provide information from the media player:
===========================================================================
* request_playlist()
* request_queue()
* request_mlib()
* request_search()
As above, only override the methods which make sense for the
corresponding media player.
===========================================================================
Methods to call to synchronize media player state information with clients:
===========================================================================
* update_playback()
* update_repeat()
* update_shuffle()
* update_item()
* update_position()
* update_progress()
These methods should be called whenever the corresponding information
has changed in the media player (it is safe to call these methods also
if there actually is no change, internally a change check is done
before sending any data to clients).
Subclasses of PlayerAdapter may override the method poll() to
periodically check a player's state.
===========================================================================
Finally some utility methods:
===========================================================================
* find_image()
'''
manager = NoManager()
# =========================================================================
# constructor
# =========================================================================
def __init__(self, name, playback_known=False, volume_known=False,
repeat_known=False, shuffle_known=False, progress_known=False,
max_rating=0, poll=2.5, file_actions=None, mime_types=None,
search_mask=None):
"""Create a new player adapter and configure its capabilities.
Just does some early initializations. Real job starts with start().
@param name:
name of the media player
@keyword playback_known:
indicates if the player's playback state can be provided (see
update_playback())
@keyword volume_known:
indicates if the player's volume can be provided (see
update_volume())
@keyword repeat_known:
indicates if the player's repeat mode can be provided (see
update_repeat())
@keyword shuffle_known:
indicates if the player's shuffle mode can be provided (see
update_shuffle())
@keyword progress_known:
indicates if the player's playback progress can be provided (see
update_progress())
@keyword max_rating:
maximum possible rating value for items
@keyword poll:
interval in seconds to call poll()
@keyword file_actions:
list of ItemAction which can be applied to files from the local
file system (actions like play a file or append files to the
playlist) - this keyword is only relevant if the method
action_files() gets overridden
@keyword mime_types:
list of mime types specifying the files to which the actions given
by the keyword 'file_actions' can be applied, this may be general
types like 'audio' or 'video' but also specific types like
'audio/mp3' or 'video/quicktime' (setting this to None means all
mime types are supported) - this keyword is only relevant if the
method action_files() gets overridden
@keyword search_mask:
list of fields to search the players library for (e.g. artist,
genre, any, ...) - if set method request_search() should be
overridden
@attention: When overriding, call super class implementation first!
"""
self.__name = name
# init config (config inits logging)
self.config = config.Config(self.__name)
# init misc fields
serial.Bin.HOST_ENCODING = self.config.player_encoding
self.__clients = []
self.__state = PlayerState()
self.__progress = Progress()
self.__item_id = None
self.__item_info = None
self.__item_img = None
flags = self.__util_calc_flags(playback_known, volume_known,
repeat_known, shuffle_known, progress_known)
self.__info = PlayerInfo(name, flags, max_rating, file_actions,
search_mask)
self.__sync_triggers = {}
self.__poll_ival = max(500, int(poll * 1000))
self.__poll_sid = 0
self.stopped = True
self.__server_bluetooth = None
self.__server_wifi = None
if self.config.fb_root_dirs:
self.__filelib = files.FileSystemLibrary(
self.config.fb_root_dirs, mime_types,
self.config.fb_show_extensions, False)
else:
log.info("file browser is disabled")
if "REMUCO_TESTSHELL" in os.environ:
from remuco import testshell
testshell.setup(self)
log.debug("init done")
def start(self):
"""Start the player adapter.
@attention: When overriding, call super class implementation first!
"""
if not self.stopped:
log.debug("ignore start, already running")
return
self.stopped = False
# set up server
if self.config.bluetooth_enabled:
self.__server_bluetooth = net.BluetoothServer(self.__clients,
self.__info, self.__handle_message, self.config)
else:
self.__server_bluetooth = None
if self.config.wifi_enabled:
self.__server_wifi = net.WifiServer(self.__clients,
self.__info, self.__handle_message, self.config)
else:
self.__server_wifi = None
# set up polling
if self.__poll_ival > 0:
log.debug("poll every %d milli seconds" % self.__poll_ival)
self.__poll_sid = gobject.timeout_add(self.__poll_ival, self.__poll)
log.debug("start done")
def stop(self):
"""Shutdown the player adapter.
Disconnects all clients and shuts down the Bluetooth and WiFi server.
Also ignores any subsequent calls to an update or reply method (e.g.
update_volume(), ..., reply_playlist_request(), ...).
@note: The same player adapter instance can be started again with
start().
@attention: When overriding, call super class implementation first!
"""
if self.stopped: return
self.stopped = True
for c in self.__clients:
c.disconnect(remove_from_list=False, send_bye_msg=True)
self.__clients = []
if self.__server_bluetooth is not None:
self.__server_bluetooth.down()
self.__server_bluetooth = None
if self.__server_wifi is not None:
self.__server_wifi.down()
self.__server_wifi = None
for sid in self.__sync_triggers.values():
if sid is not None:
gobject.source_remove(sid)
self.__sync_triggers = {}
if self.__poll_sid > 0:
gobject.source_remove(self.__poll_sid)
log.debug("stop done")
def poll(self):
"""Does nothing by default.
If player adapters override this method, it gets called periodically
in the interval specified by the keyword 'poll' in __init__().
A typical use case of this method is to detect the playback progress of
the current item and then call update_progress(). It can also be used
to poll any other player state information when a player does not
provide signals for all or certain state information changes.
"""
raise NotImplementedError
def __poll(self):
if self.config.master_volume_enabled:
self.__update_volume_master()
try:
self.poll()
except NotImplementedError:
# poll again if master volume is used, otherwise not
return self.config.master_volume_enabled
return True
# =========================================================================
# utility methods which may be useful for player adapters
# =========================================================================
def find_image(self, resource):
"""Find a local art image file related to a resource.
This method first looks in the resource' folder for typical art image
files (e.g. 'cover.png', 'front.jpg', ...). If there is no such file it
then looks into the user's thumbnail directory (~/.thumbnails).
@param resource:
resource to find an art image for (may be a file name or URI)
@keyword prefer_thumbnail:
True means first search in thumbnails, False means first search in
the resource' folder
@return: an image file name (which can be used for update_item()) or
None if no image file has been found or if 'resource' is not local
"""
file = art.get_art(resource)
log.debug("image for '%s': %s" % (resource, file))
return file
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
"""Toggle play and pause.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_toggle_repeat(self):
"""Toggle repeat mode.
@note: Override if it is possible and makes sense.
@see: update_repeat()
"""
log.error("** BUG ** in feature handling")
def ctrl_toggle_shuffle(self):
"""Toggle shuffle mode.
@note: Override if it is possible and makes sense.
@see: update_shuffle()
"""
log.error("** BUG ** in feature handling")
def ctrl_toggle_fullscreen(self):
"""Toggle full screen mode.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_next(self):
"""Play the next item.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_previous(self):
"""Play the previous item.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_seek(self, direction):
"""Seek forward or backward some seconds.
The number of seconds to seek should be reasonable for the current
item's length (if known).
If the progress of the current item is known, it should get
synchronized immediately with clients by calling update_progress().
@param direction:
* -1: seek backward
* +1: seek forward
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_rate(self, rating):
"""Rate the currently played item.
@param rating:
rating value (int)
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_tag(self, id, tags):
"""Attach some tags to an item.
@param id:
ID of the item to attach the tags to
@param tags:
a list of tags
@note: Tags does not mean ID3 tags or similar. It means the general
idea of tags (e.g. like used at last.fm).
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_navigate(self, action):
"""Navigate through menus (typically DVD menus).
@param action:
A number selecting one of these actions: UP, DOWN, LEFT, RIGHT,
SELECT, RETURN, TOPMENU (e.g. 0 is UP and 6 is TOPMENU).
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def ctrl_volume(self, direction):
"""Adjust volume.
@param volume:
* -1: decrease by some percent (5 is a good value)
* 0: mute volume
* +1: increase by some percent (5 is a good value)
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def __ctrl_volume_master(self, direction):
"""Adjust volume using custom volume command (instead of player)."""
if direction < 0:
cmd = self.config.master_volume_down_cmd
elif direction > 0:
cmd = self.config.master_volume_up_cmd
else:
cmd = self.config.master_volume_mute_cmd
ret, out = commands.getstatusoutput("sh -c '%s'" % cmd)
if ret != os.EX_OK:
log.error("master-volume-... failed: %s" % out)
else:
gobject.idle_add(self.__update_volume_master)
def __ctrl_shutdown_system(self):
if self.config.system_shutdown_enabled:
log.debug("run system shutdown command")
cmd = "sh -c '%s'" % self.config.system_shutdown_cmd
ret, out = commands.getstatusoutput(cmd)
if ret != os.EX_OK:
log.error("system-shutdown failed: %s" % out)
return
self.stop()
# =========================================================================
# actions interface
# =========================================================================
def action_files(self, action_id, files, uris):
"""Do an action on one or more files.
The files are specified redundantly by 'files' and 'uris' - use
whatever fits better. If the specified action is not applicable to
multiple files, then 'files' and 'uris' are one element lists.
The files in 'files' and 'uris' may be any files from the local file
system that have one of the mime types specified by the keyword
'mime_types' in __init__().
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to __init__() by the keyword 'file_actions'
@param files:
list of files to apply the action to (regular path names)
@param uris:
list of files to apply the action to (URI notation)
@note: Override if file item actions gets passed to __init__().
"""
log.error("** BUG ** action_files() not implemented")
def action_playlist_item(self, action_id, positions, ids):
"""Do an action on one or more items from the playlist.
The items are specified redundantly by 'positions' and 'ids' - use
whatever fits better. If the specified action is not applicable to
multiple items, then 'positions' and 'ids' are one element lists.
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to reply_playlist_request() by the keyword 'item_actions'
@param positions:
list of positions to apply the action to
@param ids:
list of IDs to apply the action to
@note: Override if item actions gets passed to reply_playlist_request().
"""
log.error("** BUG ** action_item() not implemented")
def action_queue_item(self, action_id, positions, ids):
"""Do an action on one or more items from the play queue.
The items are specified redundantly by 'positions' and 'ids' - use
whatever fits better. If the specified action is not applicable to
multiple items, then 'positions' and 'ids' are one element lists.
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to reply_queue_request() by the keyword 'item_actions'
@param positions:
list of positions to apply the action to
@param ids:
list of IDs to apply the action to
@note: Override if item actions gets passed to reply_queue_request().
"""
log.error("** BUG ** action_item() not implemented")
def action_mlib_item(self, action_id, path, positions, ids):
"""Do an action on one or more items from the player's media library.
The items are specified redundantly by 'positions' and 'ids' - use
whatever fits better. If the specified action is not applicable to
multiple items, then 'positions' and 'ids' are one element lists.
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to reply_mlib_request() by the keyword 'item_actions'
@param path:
the library path that contains the items
@param positions:
list of positions to apply the action to
@param ids:
list of IDs to apply the action to
@note: Override if item actions gets passed to reply_mlib_request().
"""
log.error("** BUG ** action_mlib_item() not implemented")
def action_mlib_list(self, action_id, path):
"""Do an action on a list from the player's media library.
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to reply_mlib_request() by the keyword 'list_actions'
@param path:
path specifying the list to apply the action to
@note: Override if list actions gets passed to reply_mlib_request().
"""
log.error("** BUG ** action_mlib_list() not implemented")
def action_search_item(self, action_id, positions, ids):
"""Do an action on one or more items from a search result.
@param action_id:
ID of the action to do - this specifies one of the actions passed
previously to reply_search_request() by the keyword 'item_actions'
@param positions:
list of positions to apply the action to
@param ids:
list of IDs to apply the action to
@note: Override if list actions gets passed to reply_search_request().
"""
log.error("** BUG ** action_search_item() not implemented")
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
"""Request the content of the currently active playlist.
@param reply:
a ListReply object
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def request_queue(self, reply):
"""Request the content of the play queue.
@param reply:
a ListReply object
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def request_mlib(self, reply, path):
"""Request the content of a playlist from the player's media library.
@param reply:
a ListReply object
@param path:
a path within a player's media library
If path is an empty list, the root of the library (all top level
playlists) are requested. Otherwise path is set as illustrated in this
example:
Consider a player with a media library structure like this:
|- Radio
|- Genres
|- Jazz
|- ...
|- Dynamic
|- Never played
|- Played recently
|- ...
|- Playlists
|- Party
|- Sue's b-day
|- ...
|- ...
If path is the empty list, all top level playlists are requests, e.g.
['Radio', 'Genres', 'Dynamic', 'Playlists', ...]. Otherwise path may
specify a specific level in the library tree, e.g. [ 'Radio' ] or
[ 'Playlists', 'Party', 'Sue's b-day' ] or etc.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
def request_search(self, reply, query):
"""Request a list of items matching a search query.
@param reply:
a ListReply object
@param query:
a list of search query values corresponding with the search mask
specified with keyword 'search_mask' in PlayerAdapter.__init__()
Example: If search mask was [ 'Artist', 'Title', 'Album' ], then
a query may look like this: [ 'Blondie', '', 'Best' ]. It is up to
player adapters how to interpret these values. However, good practice
is to interpret them as case insensitive, and-connected, non exact
matching search values. The given example would then reply a list
with all items where 'Blondie' is contained in the artist field and
'Best' is contained in the Album field.
@note: Override if it is possible and makes sense.
"""
log.error("** BUG ** in feature handling")
# =========================================================================
# player side synchronization
# =========================================================================
def update_position(self, position, queue=False):
"""Set the current item's position in the playlist or queue.
@param position:
position of the currently played item (starting at 0)
@keyword queue:
True if currently played item is from the queue, False if it is
from the currently active playlist
@note: Call to synchronize player state with remote clients.
"""
change = self.__state.queue != queue
change |= self.__state.position != position
if change:
self.__state.queue = queue
self.__state.position = position
self.__sync_trigger(self.__sync_state)
def update_playback(self, playback):
"""Set the current playback state.
@param playback:
playback mode
@see: remuco.PLAYBACK_...
@note: Call to synchronize player state with remote clients.
"""
change = self.__state.playback != playback
if change:
self.__state.playback = playback
self.__sync_trigger(self.__sync_state)
def update_repeat(self, repeat):
"""Set the current repeat mode.
@param repeat: True means play indefinitely, False means stop after the
last playlist item
@note: Call to synchronize player state with remote clients.
"""
repeat = bool(repeat)
change = self.__state.repeat != repeat
if change:
self.__state.repeat = repeat
self.__sync_trigger(self.__sync_state)
def update_shuffle(self, shuffle):
"""Set the current shuffle mode.
@param shuffle: True means play in non-linear order, False means play
in linear order
@note: Call to synchronize player state with remote clients.
"""
shuffle = bool(shuffle)
change = self.__state.shuffle != shuffle
if change:
self.__state.shuffle = shuffle
self.__sync_trigger(self.__sync_state)
def update_volume(self, volume):
"""Set the current volume.
@param volume: the volume in percent
@note: Call to synchronize player state with remote clients.
"""
if self.config.master_volume_enabled:
# ignore if custom command has been set
return
volume = int(volume)
if volume < 0 or volume > 100:
log.warning("bad volume from player adapter: %d" % volume)
volume = 50
change = self.__state.volume != volume
if change:
self.__state.volume = volume
self.__sync_trigger(self.__sync_state)
def __update_volume_master(self):
"""Set the current volume (use custom command instead of player)."""
cmd = "sh -c '%s'" % self.config.master_volume_get_cmd
ret, out = commands.getstatusoutput(cmd)
if ret != os.EX_OK:
log.error("master-volume-get failed: '%s'" % out)
return
try:
volume = int(out)
if volume < 0 or volume > 100:
raise ValueError
except ValueError:
log.error("output of master-volume-get malformed: '%s'" % out)
return
change = self.__state.volume != volume
if change:
self.__state.volume = volume
self.__sync_trigger(self.__sync_state)
def update_progress(self, progress, length):
"""Set the current playback progress.
@param progress:
number of currently elapsed seconds
@keyword length:
item length in seconds (maximum possible progress value)
@note: Call to synchronize player state with remote clients.
"""
# sanitize progress (to a multiple of 5)
length = max(0, int(length))
progress = max(0, int(progress))
off = progress % 5
if off < 3:
progress -= off
else:
progress += (5 - off)
if length > 0:
progress = min(length, progress)
change = self.__progress.length != length
change |= self.__progress.progress != progress
if change:
self.__progress.progress = progress
self.__progress.length = length
self.__sync_trigger(self.__sync_progress)
def update_item(self, id, info, img):
"""Set currently played item.
@param id:
item ID (str)
@param info:
meta information (dict)
@param img:
image / cover art (either a file name or URI or an instance of
Image.Image)
@note: Call to synchronize player state with remote clients.
@see: find_image() for finding image files for an item.
@see: remuco.INFO_... for keys to use for 'info'
"""
log.debug("new item: (%s, %s %s)" % (id, info, img))
change = self.__item_id != id
change |= self.__item_info != info
change |= self.__item_img != img
if change:
self.__item_id = id
self.__item_info = info
self.__item_img = img
self.__sync_trigger(self.__sync_item)
# =========================================================================
# synchronization (outbound communication)
# =========================================================================
def __sync_trigger(self, sync_fn):
if self.stopped:
return
if sync_fn in self.__sync_triggers:
log.debug("trigger for %s already active" % sync_fn.func_name)
return
self.__sync_triggers[sync_fn] = \
gobject.idle_add(sync_fn, priority=gobject.PRIORITY_LOW)
def __sync_state(self):
del self.__sync_triggers[self.__sync_state]
log.debug("broadcast new state to clients: %s" % self.__state)
msg = net.build_message(message.SYNC_STATE, self.__state)
if msg is None:
return
for c in self.__clients: c.send(msg)
return False
def __sync_progress(self):
del self.__sync_triggers[self.__sync_progress]
log.debug("broadcast new progress to clients: %s" % self.__progress)
msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
if msg is None:
return
for c in self.__clients: c.send(msg)
return False
def __sync_item(self):
del self.__sync_triggers[self.__sync_item]
log.debug("broadcast new item to clients: %s" % self.__item_id)
for c in self.__clients:
msg = net.build_message(message.SYNC_ITEM, self.__item(c))
if msg is not None:
c.send(msg)
return False
# =========================================================================
# handling client message (inbound communication)
# =========================================================================
def __handle_message(self, client, id, bindata):
if message.is_control(id):
log.debug("control from client %s" % client)
self.__handle_message_control(id, bindata)
elif message.is_action(id):
log.debug("action from client %s" % client)
self.__handle_message_action(id, bindata)
elif message.is_request(id):
log.debug("request from client %s" % client)
self.__handle_message_request(client, id, bindata)
elif id == message.PRIV_INITIAL_SYNC:
msg = net.build_message(message.SYNC_STATE, self.__state)
client.send(msg)
msg = net.build_message(message.SYNC_PROGRESS, self.__progress)
client.send(msg)
msg = net.build_message(message.SYNC_ITEM, self.__item(client))
client.send(msg)
else:
log.error("** BUG ** unexpected message: %d" % id)
def __handle_message_control(self, id, bindata):
if id == message.CTRL_PLAYPAUSE:
self.ctrl_toggle_playing()
elif id == message.CTRL_NEXT:
self.ctrl_next()
elif id == message.CTRL_PREV:
self.ctrl_previous()
elif id == message.CTRL_SEEK:
control = serial.unpack(Control, bindata)
if control is None:
return
self.ctrl_seek(control.param)
elif id == message.CTRL_VOLUME:
control = serial.unpack(Control, bindata)
if control is None:
return
if self.config.master_volume_enabled:
self.__ctrl_volume_master(control.param)
else:
self.ctrl_volume(control.param)
elif id == message.CTRL_REPEAT:
self.ctrl_toggle_repeat()
elif id == message.CTRL_SHUFFLE:
self.ctrl_toggle_shuffle()
elif id == message.CTRL_RATE:
control = serial.unpack(Control, bindata)
if control is None:
return
self.ctrl_rate(control.param)
elif id == message.CTRL_TAG:
tag = serial.unpack(Tagging, bindata)
if tag is None:
return
self.ctrl_tag(tag.id, tag.tags)
elif id == message.CTRL_NAVIGATE:
control = serial.unpack(Control, bindata)
if control is None:
return
self.ctrl_navigate(control.param)
elif id == message.CTRL_FULLSCREEN:
self.ctrl_toggle_fullscreen()
elif id == message.CTRL_SHUTDOWN:
self.__ctrl_shutdown_system()
else:
log.error("** BUG ** unexpected control message: %d" % id)
def __handle_message_action(self, id, bindata):
a = serial.unpack(Action, bindata)
if a is None:
return
if id == message.ACT_PLAYLIST:
self.action_playlist_item(a.id, a.positions, a.items)
elif id == message.ACT_QUEUE:
self.action_queue_item(a.id, a.positions, a.items)
elif id == message.ACT_MLIB and a.id < 0: # list action id
self.action_mlib_list(a.id, a.path)
elif id == message.ACT_MLIB and a.id > 0: # item action id
self.action_mlib_item(a.id, a.path, a.positions, a.items)
elif id == message.ACT_FILES:
uris = self.__util_files_to_uris(a.items)
self.action_files(a.id, a.items, uris)
elif id == message.ACT_SEARCH:
self.action_search_item(a.id, a.positions, a.items)
else:
log.error("** BUG ** unexpected action message: %d" % id)
def __handle_message_request(self, client, id, bindata):
request = serial.unpack(Request, bindata)
if request is None:
return
reply = ListReply(client, request.request_id, id, request.page,
path=request.path)
if id == message.REQ_PLAYLIST:
self.request_playlist(reply)
elif id == message.REQ_QUEUE:
self.request_queue(reply)
elif id == message.REQ_MLIB:
self.request_mlib(reply, request.path)
elif id == message.REQ_FILES:
reply.nested, reply.ids, reply.names = \
self.__filelib.get_level(request.path)
reply.send()
elif id == message.REQ_SEARCH:
self.request_search(reply, request.path)
else:
log.error("** BUG ** unexpected request message: %d" % id)
# =========================================================================
# miscellaneous
# =========================================================================
def __item(self, client):
"""Creates a client specific item object."""
return Item(self.__item_id, self.__item_info, self.__item_img,
client.info.img_size, client.info.img_type)
def __util_files_to_uris(self, files):
def file_to_uri(file):
url = urllib.pathname2url(file)
return urlparse.urlunparse(("file", None, url, None, None, None))
if not files:
return []
uris = []
for file in files:
uris.append(file_to_uri(file))
return uris
def __util_calc_flags(self, playback_known, volume_known, repeat_known,
shuffle_known, progress_known):
"""Check player adapter capabilities.
Most capabilities get detected by testing which methods have been
overridden by a subclassing player adapter.
"""
def ftc(cond, feature):
if inspect.ismethod(cond): # check if overridden
enabled = cond.__module__ != __name__
else:
enabled = cond
if enabled:
return feature
else:
return 0
features = (
# --- 'is known' features ---
ftc(playback_known, FT_KNOWN_PLAYBACK),
ftc(volume_known, FT_KNOWN_VOLUME),
ftc(self.config.master_volume_enabled, FT_KNOWN_VOLUME),
ftc(repeat_known, FT_KNOWN_REPEAT),
ftc(shuffle_known, FT_KNOWN_SHUFFLE),
ftc(progress_known, FT_KNOWN_PROGRESS),
# --- misc control features ---
ftc(self.ctrl_toggle_playing, FT_CTRL_PLAYBACK),
ftc(self.ctrl_volume, FT_CTRL_VOLUME),
ftc(self.config.master_volume_enabled, FT_CTRL_VOLUME),
ftc(self.ctrl_seek, FT_CTRL_SEEK),
ftc(self.ctrl_tag, FT_CTRL_TAG),
ftc(self.ctrl_rate, FT_CTRL_RATE),
ftc(self.ctrl_toggle_repeat, FT_CTRL_REPEAT),
ftc(self.ctrl_toggle_shuffle, FT_CTRL_SHUFFLE),
ftc(self.ctrl_next, FT_CTRL_NEXT),
ftc(self.ctrl_previous, FT_CTRL_PREV),
ftc(self.ctrl_toggle_fullscreen, FT_CTRL_FULLSCREEN),
ftc(self.ctrl_navigate, FT_CTRL_NAVIGATE),
# --- request features ---
ftc(self.request_playlist, FT_REQ_PL),
ftc(self.request_queue, FT_REQ_QU),
ftc(self.request_mlib, FT_REQ_MLIB),
ftc(self.config.system_shutdown_enabled, FT_SHUTDOWN),
)
flags = 0
for feature in features:
flags |= feature
log.debug("flags: %X" % flags)
return flags
remuco-source-0.9.6/base/module/remuco/art.py 0000644 0000000 0000000 00000007074 11700415064 021113 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import glob
import hashlib
import os.path
import re
import urllib
import urlparse
from remuco import log
from remuco.remos import user_home
_RE_IND = r'(?:front|album|cover|folder|art)' # words indicating art files
_RE_EXT = r'\.(?:png|jpeg|jpg|gif)' # art file extensions
_RE_FILE = (r'^%s%s$' % (_RE_IND,_RE_EXT), # typical name (e.g. front.jpg)
r'^.*%s.*%s$' % (_RE_IND,_RE_EXT), # typical name with noise
r'^.*%s$' % _RE_EXT) # any image file
_RE_FILE = [re.compile(rx, re.IGNORECASE) for rx in _RE_FILE]
# =============================================================================
# various methods to find local cover art / media images
# =============================================================================
_TN_DIR = os.path.join(user_home, ".thumbnails")
def _try_thumbnail(resource):
"""Try to find a thumbnail for a resource (path or URI)."""
if not os.path.isdir(_TN_DIR):
return None
# we need a file://... URI
elems = urlparse.urlparse(resource)
if elems[0] and elems[0] != "file": # not local
return None
if not elems[0]: # resource is a path
elems = list(elems) # make elems assignable
elems[0] = "file"
if isinstance(resource, unicode):
resource = resource.encode("utf-8")
elems[2] = urllib.pathname2url(resource)
resource = urlparse.urlunparse(elems)
hex = hashlib.md5(resource).hexdigest()
for subdir in ("large", "normal"):
file = os.path.join(_TN_DIR, subdir, "%s.png" % hex)
if os.path.isfile(file):
return file
return None
def _try_folder(resource):
"""Try to find an image in the resource's folder."""
# we need a local path
elems = urlparse.urlparse(resource)
if elems[0] and elems[0] != "file": # resource is not local
return None
rpath = elems[0] and urllib.url2pathname(elems[2]) or elems[2]
rpath = os.path.dirname(rpath)
log.debug("looking for art image in %s" % rpath)
files = glob.glob(os.path.join(rpath, "*"))
files = [os.path.basename(f) for f in files if os.path.isfile(f)]
for rx in _RE_FILE:
for file in files:
if rx.match(file):
return os.path.join(rpath, file)
return None
# =============================================================================
def get_art(resource, prefer_thumbnail=False):
if resource is None:
return None
fname = None
methods = (_try_thumbnail, _try_folder)
for meth in methods:
fname = meth(resource)
if fname:
break
return fname
remuco-source-0.9.6/base/module/remuco/config.py 0000644 0000000 0000000 00000027221 11700415064 021566 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
from __future__ import with_statement
import ConfigParser
from datetime import datetime
from glob import glob
import os
from os.path import join, isdir, exists, pathsep, basename
import re
import shutil
import sys
import textwrap
from remuco import log
from remuco import defs
from remuco.remos import user_config_dir
from remuco.remos import user_cache_dir
# =============================================================================
# Ordered dictionary, makes CP write sorted config files
# =============================================================================
class _odict(dict):
def keys(self):
kl = list(super(_odict, self).keys())
kl.sort()
return kl
def items(self):
il = list(super(_odict, self).items())
il.sort(cmp=lambda a,b: cmp(a[0], b[0]))
return il
# =============================================================================
# Constants and definitions
# =============================================================================
DEVICE_FILE = join(user_cache_dir, "remuco", "devices")
_DOC_HEADER = """# Player Adapter Configuration
# ============================
#
# Options defined in section DEFAULT affect *all* player adapters. Individual
# option values can be defined in each player's section.
#
# Options starting with `x-` are player specific options, i.e. they don't
# appear in section DEFAULT because they only make sense for specific players.
#
# Options
# =======
#"""
# should be updated on major changes in config backend (simple removal or
# additions of options do not require a version update)
_CONFIG_VERSION = "3"
# standard options with default values, converter functions and documentation
_OPTIONS = {
"config-version": ("0", None,
"Used internally, don't edit."),
"bluetooth-enabled": ("1", int,
"Enable or disable Bluetooth."),
"bluetooth-channel": ("0", int,
"Bluetooth channel to use. 0 mean the next free channel."),
"wifi-enabled": ("1", int,
"Enable or disable WiFi (Inet)."),
"wifi-port": ("34271", int,
"WiFi port to use. Should be changed if Remuco is used for multiple "
"players simultaneously to prevent port conflicts among adapters."),
"player-encoding": ("UTF8", None,
"Encoding of text coming from the player (i.e. artist, title, ...)."),
"log-level": ("INFO", lambda v: getattr(log, v),
"Log verbosity. Possible values: ERROR, WARNING, INFO, DEBUG."),
"fb-show-extensions": ("0", int,
"If to show file name extensions in a client's file browser."),
"fb-root-dirs": ("auto", lambda v: v.split(pathsep),
"List of directories (separated by `%s`) to show in a client's file "
"browser. `auto` expands to all directories which typically contain "
"files of the mime types a player supports (e.g. `~/Music` for audio "
"players)." % pathsep),
"master-volume-enabled": ("0", int,
"Enable or disable master volume. By default a player's volume level "
"is controlled by and displayed on clients. By setting this to `1` "
"the system's master volume is used instead - in that case the "
"following options *may* need to get adusted."),
"master-volume-get-cmd": (r'amixer get Master | grep -E "\[[0-9]+%\]" | '
'sed -re "s/^.*\[([0-9]+)%\].*$/\\1/"', None,
"Command to get the master volume level in percent."),
"master-volume-up-cmd": ("amixer set Master 5%+", None,
"Command to increase the master volume."),
"master-volume-down-cmd": ("amixer set Master 5%-", None,
"Command to decrease the master volume."),
"master-volume-mute-cmd": ("amixer set Master 0%", None,
"Command to mute the master volume."),
"system-shutdown-enabled": ("0", int,
"Enable or disable system shutdown by clients. If enabled, the "
"following option *may* need to get adjusted."),
"system-shutdown-cmd": ("dbus-send --session --type=method_call "
"--dest=org.freedesktop.PowerManagement "
"/org/freedesktop/PowerManagement "
"org.freedesktop.PowerManagement.Shutdown", None,
"Command to shut down the system."),
}
# defaults-only version of _OPTIONS to pass to config parser
_DEFAULTS = _odict()
for k, v in _OPTIONS.items():
_DEFAULTS[k] = v[0]
# timestamp (used for backups of old config data)
_TS = datetime.now().strftime("%Y%m%d-%H%M%S")
# =============================================================================
# Config class
# =============================================================================
class Config(object):
"""Class for getting and setting player adapter specific configurations.
An instance of Config mirrors the configuration of a specific player
adapter (usually ~/.config/remuco/PLAYER/conf).
Player adapters are not supposed to create instances of Config. Instead
use the 'config' attribute of a PlayerAdapter instance to access the
currently used Config instance.
"""
def __init__(self, player_name):
"""Create a new instance for the given player (adapter)."""
super(Config, self).__init__()
# convert descriptive name to a plain canonical one
self.player = re.sub(r'[^\w-]', '', player_name).lower()
# paths
self.dir = join(user_config_dir, "remuco")
self.cache = join(user_cache_dir, "remuco")
self.file = join(self.dir, "remuco.cfg")
# remove old stuff
self.__cleanup()
# create directories
for dname in (self.dir, self.cache):
try:
if not isdir(dname):
os.makedirs(dname)
except OSError, e:
log.error("failed to make dir: %s", e)
if not "REMUCO_LOG_STDOUT" in os.environ and isdir(self.cache):
log.set_file(join(self.cache, "%s.log" % self.player))
# load
cp = ConfigParser.RawConfigParser(_DEFAULTS, _odict)
if not cp.has_section(self.player):
cp.add_section(self.player)
if exists(self.file):
try:
cp.read(self.file)
except ConfigParser.Error, e:
log.warning("failed to read config %s (%s)" % (self.file, e))
# reset on version change
if cp.get(ConfigParser.DEFAULTSECT, "config-version") != _CONFIG_VERSION:
sections = cp.sections() # keep already existing player sections
cp = ConfigParser.RawConfigParser(_DEFAULTS, _odict)
for sec in sections:
cp.add_section(sec)
if exists(self.file):
bak = "%s.%s.backup" % (self.file, _TS)
log.info("reset config (major changes, backup: %s)" % bak)
shutil.copy(self.file, bak)
# remove unknown options in all sections
for sec in cp.sections() + [ConfigParser.DEFAULTSECT]:
for key, value in cp.items(sec):
if key not in _DEFAULTS and not key.startswith("x-"):
cp.remove_option(sec, key)
# add not yet existing options to default section
for key, value in _DEFAULTS.items():
if not cp.has_option(ConfigParser.DEFAULTSECT, key):
cp.set(ConfigParser.DEFAULTSECT, key, value)
# update version
cp.set(ConfigParser.DEFAULTSECT, "config-version", _CONFIG_VERSION)
self.__cp = cp
# save to always have a clean file
self.__save()
log.set_level(self.log_level)
log.info("remuco version: %s" % defs.REMUCO_VERSION)
def __getattribute__(self, attr):
"""Attribute-style access to standard options."""
try:
return super(Config, self).__getattribute__(attr)
except AttributeError, e:
_attr = attr.replace("_", "-")
if _attr in _OPTIONS:
attr = _attr
elif attr not in _OPTIONS:
raise e
value = self.__cp.get(self.player, attr)
converter = _OPTIONS[attr][1] or (lambda v: v)
try:
return converter(value)
except Exception, e:
log.error("malformed option '%s: %s' (%s)" % (attr, e))
return converter(_DEFAULTS[attr])
def getx(self, key, default, converter=None, save=True):
"""Get the value of a non-standard, player specific option.
@param key:
config option name
@param default:
default value (as string!)
@keyword converter:
value converter function, e.g. `int`
@keyword save:
save default value in config file if not yet set
@return:
option value, optionally converted
"""
key = "x-%s" % key
if not self.__cp.has_option(self.player, key) and save:
self.__cp.set(self.player, key, default)
self.__save()
try:
value = self.__cp.get(self.player, key)
except ConfigParser.NoOptionError:
value = default
converter = converter or (lambda v: v)
try:
return converter(value)
except Exception, e:
log.error("malformed option '%s: %s' (%s)" % (key, value, e))
return converter(default) # if this fails then, it's a bug
def __save(self):
"""Save config to it's file."""
doc = [_DOC_HEADER]
for key in _DEFAULTS.keys():
idoc = "# %s:" % key
idoc = [idoc] + textwrap.wrap(_OPTIONS[key][2], 73)
idoc = "\n# ".join(idoc)
doc.append(idoc)
doc = "\n".join(doc)
try:
with open(self.file, 'w') as fp:
fp.write(doc)
fp.write("\n\n")
self.__cp.write(fp)
except IOError, e:
log.warning("failed to save config to %s (%s)" % (self.file, e))
def __cleanup(self):
"""Trash obsolete config and cache data from older versions."""
def obsolete(fn):
"""Check if a config or cache item may be trashed."""
obs = isdir(fn)
obs |= basename(fn) in ("shutdown-system", "volume")
obs &= not basename(fn).startswith("old-")
return obs
for dname, dtype in ((self.dir, "config"), (self.cache, "cache")):
trash = join(dname, "old-%s.backup" % _TS)
fnames = [f for f in glob(join(dname, "*")) if obsolete(f)]
if fnames:
log.info("moving old %s data to %s" % (dtype, trash))
if not exists(trash):
os.makedirs(trash)
for fn in fnames:
shutil.move(fn, trash)
remuco-source-0.9.6/base/module/remuco/data.py 0000644 0000000 0000000 00000024126 11700415064 021233 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Data containers to send to and receive from clients."""
import tempfile
import Image
import urlparse
import urllib
from remuco import log
from remuco import serial
# =============================================================================
# outgoing data (to clients)
# =============================================================================
class PlayerInfo(serial.Serializable):
""" Parameter of the player info message sent to clients."""
def __init__(self, name, flags, max_rating, file_item_actions, search_mask):
self.name = name
self.flags = flags
self.max_rating = max_rating
self.fia_ids = []
self.fia_labels = []
self.fia_multiples = []
for action in file_item_actions or []:
self.fia_ids.append(action.id);
self.fia_labels.append(action.label);
self.fia_multiples.append(action.multiple);
self.search_mask = search_mask or []
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_S, serial.TYPE_I, serial.TYPE_Y,
serial.TYPE_AI, serial.TYPE_AS, serial.TYPE_AB,
serial.TYPE_AS)
def get_data(self):
return (self.name, self.flags, self.max_rating,
self.fia_ids, self.fia_labels, self.fia_multiples,
self.search_mask)
class PlayerState(serial.Serializable):
""" Parameter of the state sync message sent to clients."""
def __init__(self):
self.playback = 0
self.volume = 0
self.position = 0
self.repeat = False
self.shuffle = False
self.queue = False
def __str__(self):
return "(%d, %d, %d, %s, %s, %s)" % (
self.playback, self.volume, self.position,
self.repeat, self.shuffle, self.queue)
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_Y, serial.TYPE_Y, serial.TYPE_I,
serial.TYPE_B, serial.TYPE_B, serial.TYPE_B)
def get_data(self):
return (self.playback, self.volume, self.position,
self.repeat, self.shuffle, self.queue)
class Progress(serial.Serializable):
""" Parameter of the progress sync message sent to clients."""
def __init__(self):
self.progress = 0
self.length = 0
def __str__(self):
return "(%d/%d)" % (self.progress, self.length)
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I, serial.TYPE_I)
def get_data(self):
return (self.progress, self.length)
class Item(serial.Serializable):
""" Parameter of the item sync message sent to clients."""
def __init__(self, id, info, img, img_size, img_type):
self.__id = id
self.__info = self.__flatten_info(info)
self.__img = self.__thumbnail_img(img, img_size, img_type)
def __str__(self):
return "(%s, %s, %s)" % (self.__id, self.__info, self.__img)
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_S, serial.TYPE_AS, serial.TYPE_AY)
def get_data(self):
return (self.__id, self.__info, self.__img)
# === misc ===
def __flatten_info(self, info_dict):
info_list = []
if not info_dict:
return info_list
for key in info_dict.keys():
val = info_dict.get(key)
if val is not None:
info_list.append(key)
if not isinstance(val, basestring):
val = str(val)
info_list.append(val)
return info_list
def __thumbnail_img(self, img, img_size, img_type):
if img_size == 0:
return []
if isinstance(img, basestring) and img.startswith("file://"):
img = urlparse.urlparse(img)[2]
img = urllib.url2pathname(img)
if not img:
return []
try:
if not isinstance(img, Image.Image):
img = Image.open(img)
img.thumbnail((img_size, img_size))
file_tmp = tempfile.TemporaryFile()
if img_type == "JPEG" and img.mode == "P":
img = img.convert("RGB")
img.save(file_tmp, img_type)
file_tmp.seek(0)
thumb = file_tmp.read()
file_tmp.close()
return thumb
except IOError, e:
log.warning("failed to thumbnail %s (%s)" % (img, e))
return []
class ItemList(serial.Serializable):
""" Parameter of a request reply message sent to clients."""
def __init__(self, request_id, path, nested, item_ids, item_names,
item_offset, page, page_max, item_actions, list_actions):
self.request_id = request_id
self.path = path or []
self.nested = nested or []
self.item_ids = item_ids or []
self.item_names = item_names or []
self.item_offset = item_offset
self.page = page or 0
self.page_max = page_max or 0
self.ia_ids = []
self.ia_labels = []
self.ia_multiples = []
for action in item_actions or []:
self.ia_ids.append(action.id);
self.ia_labels.append(action.label);
self.ia_multiples.append(action.multiple);
self.la_ids = []
self.la_labels = []
for action in list_actions or []:
self.la_ids.append(action.id);
self.la_labels.append(action.label);
def __str__(self):
return "(%d, %s, %s, %s, %s, %d, %d, %d, %s, %s, %s, %s, %s, %s, %s)" % (
self.request_id,
self.path, self.nested, self.item_ids, self.item_names,
self.item_offset, self.page, self.page_max,
self.ia_ids, self.ia_labels, self.ia_multiples,
self.la_ids, self.la_labels)
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I,
serial.TYPE_AS, serial.TYPE_AS, serial.TYPE_AS, serial.TYPE_AS,
serial.TYPE_I, serial.TYPE_I, serial.TYPE_I,
serial.TYPE_AI, serial.TYPE_AS, serial.TYPE_AB,
serial.TYPE_AI, serial.TYPE_AS)
def get_data(self):
return (self.request_id,
self.path, self.nested, self.item_ids, self.item_names,
self.item_offset, self.page, self.page_max,
self.ia_ids, self.ia_labels, self.ia_multiples,
self.la_ids, self.la_labels)
# =============================================================================
# incoming data (from clients)
# =============================================================================
class ClientInfo(serial.Serializable):
""" Parameter of a client info messages from a client."""
def __init__(self):
self.img_size = 0
self.img_type = None
self.page_size = 0
self.device = {}
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I, serial.TYPE_S, serial.TYPE_I,
serial.TYPE_AS, serial.TYPE_AS)
def set_data(self, data):
self.img_size, self.img_type, self.page_size, dev_keys, dev_vals = data
for key, value in zip(dev_keys, dev_vals):
self.device[key] = value
class Control(serial.Serializable):
""" Parameter of control messages from clients with integer arguments."""
def __init__(self):
self.param = 0
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I,)
def set_data(self, data):
self.param, = data
class Action(serial.Serializable):
""" Parameter of an action message from a client."""
def __init__(self):
self.id = 0
self.path = None
self.positions = None
self.items = None # item ids or file names
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I, serial.TYPE_AS, serial.TYPE_AI, serial.TYPE_AS)
def set_data(self, data):
self.id, self.path, self.positions, self.items = data
class Tagging(serial.Serializable):
""" Parameter of a tagging message from a client."""
def __init__(self):
self.id = None
self.tags = None
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_S, serial.TYPE_AS)
def set_data(self, data):
self.id, self.tags = data
class Request(serial.Serializable):
""" Parameter of a request message from a client."""
def __init__(self):
self.request_id = -2
self.id = None # item id
self.path = None # list path
self.page = 0 # list page
# === serial interface ===
def get_fmt(self):
return (serial.TYPE_I, serial.TYPE_S, serial.TYPE_AS, serial.TYPE_I)
def set_data(self, data):
self.request_id, self.id, self.path, self.page = data
remuco-source-0.9.6/base/module/remuco/defs.py 0000644 0000000 0000000 00000002453 11700415064 021242 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
PLAYBACK_PAUSE = 1
PLAYBACK_PLAY = 2
PLAYBACK_STOP = 0
INFO_ABSTRACT = "__abstract__"
INFO_ALBUM = "album"
INFO_ARTIST = "artist"
INFO_BITRATE = "bitrate"
INFO_COMMENT = "comment"
INFO_GENRE = "genre"
INFO_LENGTH = "length"
INFO_RATING = "rating"
INFO_TAGS = "tags"
INFO_TITLE = "title"
INFO_TRACK = "track"
INFO_YEAR = "year"
REMUCO_VERSION = "0.9.6"
remuco-source-0.9.6/base/module/remuco/dictool.py 0000644 0000000 0000000 00000010260 11700415064 021751 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Utility functions to read/write simple str<->str dicts from/to files."""
from __future__ import with_statement
import os.path
from remuco import log
def dict_to_string(dic, keys=None):
"""Flatten a dictionary.
@param dic: The dictionary to flatten.
@keyword only: A list of keys to include in the flattened string. This list
also gives the order in which items are flattened. If only is None,
all items are flattened in an arbitrary order.
@return: the flattened dictionary as a string
"""
flat = ""
keys = keys or dic.keys()
for key in keys:
value = dic.get(key, "")
value = value.replace(",", "_")
flat += "%s:%s," % (key, value)
flat = flat.strip(",")
return flat
def string_to_dict(s, keys=None):
"""Create a dictionary from a flattened string representation.
@param s: The string to build dictionary from.
@keyword keys: A list of keys to include in dictionary. If keys is None,
all items are flattened in an arbitrary order.
@return: the dictionary
"""
dic = {}
items = s.split(",")
for item in items:
try:
key, value = item.split(":", 1)
except ValueError:
key, value = item, ""
if keys is None or key in keys:
dic[key] = value
return dic
def read_dicts_from_file(filename, flat=False, keys=None):
"""Read a list of dictionaries from a file.
@param filename: Name of the file to read.
@keyword flat: If True, the dictionaries are returned flattened, i.e. as
strings.
@keyword keys: See string_to_dict(). Only used if flat is False.
@return: the list of dictionaries
"""
if not os.path.exists(filename):
return []
lines = []
try:
fp = open(filename, "r")
lines = fp.readlines()
fp.close()
except IOError, e:
log.warning("failed to open %s (%s)" % (filename, e))
dicts_flat = []
for line in lines:
line = line.replace("\n", "")
line = line.strip(" ")
if line.startswith("#") or len(line) == 0:
continue
dicts_flat.append(line)
if flat:
return dicts_flat
dicts = []
for dic_flat in dicts_flat:
dicts.append(string_to_dict(dic_flat, keys=keys))
return dicts
def write_dicts_to_file(filename, dicts, keys=None, comment=None):
"""Write a list of dictionaries into a file.
@param filename: Name of the file to write into.
@param dicts: Either a list of dictionaries or a list of strings, i.e.
already flattened dictionaries.
@keyword keys: See dict_to_string(). Only used if dictionaries are not yet
flattened.
@keyword comment: A comment text to put at the beginning of the file.
"""
lines = []
if comment:
lines.append("%s\n" % comment)
for dic in dicts:
if not isinstance(dic, basestring):
dic = dict_to_string(dic, keys=keys)
lines.append("%s\n" % dic)
try:
with open(filename, "w") as fp:
fp.writelines(lines)
except IOError, e:
log.warning("failed to write to %s (%s)" % (filename, e))
remuco-source-0.9.6/base/module/remuco/features.py 0000644 0000000 0000000 00000003157 11700415064 022141 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
# --- 'is known' features ---
FT_KNOWN_VOLUME = 1 << 0
FT_KNOWN_REPEAT = 1 << 1
FT_KNOWN_SHUFFLE = 1 << 2
FT_KNOWN_PLAYBACK = 1 << 3
FT_KNOWN_PROGRESS = 1 << 4
# --- control features ---
FT_CTRL_PLAYBACK = 1 << 9
FT_CTRL_VOLUME = 1 << 10
FT_CTRL_SEEK = 1 << 11
FT_CTRL_TAG = 1 << 12
#FT_CTRL_ = 1 << 13
#FT_CTRL_ = 1 << 14
FT_CTRL_RATE = 1 << 15
FT_CTRL_REPEAT = 1 << 16
FT_CTRL_SHUFFLE = 1 << 17
FT_CTRL_NEXT = 1 << 18
FT_CTRL_PREV = 1 << 19
FT_CTRL_FULLSCREEN = 1 << 20
FT_CTRL_NAVIGATE = 1 << 21
# --- request features ---
FT_REQ_ITEM = 1 << 25 # obsolete
FT_REQ_PL = 1 << 26
FT_REQ_QU = 1 << 27
FT_REQ_MLIB = 1 << 28
# --- misc features
FT_SHUTDOWN = 1 << 30
remuco-source-0.9.6/base/module/remuco/files.py 0000644 0000000 0000000 00000013721 11700415064 021423 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import os
import os.path
import mimetypes
import sys
from remuco import log
from remuco.remos import media_dirs, user_home
class FileSystemLibrary(object):
def __init__(self, root_dirs, mime_types, show_extensions, show_hidden):
self.__mime_types = mime_types
self.__show_extensions = show_extensions
self.__show_hidden = show_hidden
if not sys.getfilesystemencoding() in ("UTF8", "UTF-8", "UTF_8"):
log.warning("file system encoding is not UTF-8, this may cause " +
"problems with file browser features")
root_dirs = root_dirs or []
# mimetype dependent root dirs
if "auto" in root_dirs:
root_dirs.remove("auto")
if mime_types:
for mtype in mime_types:
if mtype in media_dirs:
root_dirs += media_dirs[mtype]
mtype = mtype.split("/")[0] # use main mimetype
if mtype in media_dirs:
root_dirs += media_dirs[mtype]
root_dirs = self.__trim_root_dirs(root_dirs) or [user_home]
# map root dirs to names
self.__roots = {}
for dir in root_dirs:
name = os.path.basename(dir)
if name == dir: # == "/"
name = "Root"
else:
name = name.capitalize()
counter = 2
name_x = name
while name_x in self.__roots:
name_x = "%s (%d)" % (name, counter)
counter += 1
self.__roots[name_x] = dir
log.info("file browser root dirs: %s " % self.__roots)
if not mimetypes.inited:
mimetypes.init()
def __trim_root_dirs(self, dirs):
"""Trim a directory list.
Expands variables and '~' and removes duplicate, relative, non
existent and optionally hidden directories.
@return: a trimmed directory list
"""
trimmed = []
for dir in dirs:
dir = os.path.expandvars(dir)
dir = os.path.expanduser(dir)
if not self.__show_hidden and dir.startswith("."):
continue
if not os.path.isabs(dir):
log.warning("path %s not absolute, ignore" % dir)
continue
if not os.path.isdir(dir):
log.warning("path %s not a directory, ignore" % dir)
continue
if dir not in trimmed:
trimmed.append(dir)
return trimmed
def get_level(self, path):
def is_hidden(name):
return name.startswith(".") or name.endswith("~")
def mimetype_is_supported(name):
type = mimetypes.guess_type(name)[0] or ""
type_main = type.split("/")[0]
return (not self.__mime_types or type_main in self.__mime_types or
type in self.__mime_types)
nested = []
ids = []
names = []
if not path:
nested = list(self.__roots.keys()) # Py3K
nested.sort()
return (nested, ids, names)
label = path[0] # root dir label
dir = self.__roots[label] # root dir
path = path[1:] # path elements relative to root dir
for elem in path:
dir = os.path.join(dir, elem)
try:
x, dirs, files = os.walk(dir).next()
except StopIteration:
return (nested, ids, names)
dirs.sort()
files.sort()
for entry in dirs:
entry_abs = os.path.join(dir, entry)
if not self.__show_hidden and is_hidden(entry):
log.debug("ignore %s (hidden)" % entry_abs)
continue
if not os.access(entry_abs, os.X_OK | os.R_OK):
log.debug("ignore %s (no access)" % entry_abs)
continue
nested.append(entry)
for entry in files:
entry_abs = os.path.join(dir, entry)
if not self.__show_hidden and is_hidden(entry):
log.debug("ignore %s (hidden)" % entry_abs)
continue
if not os.access(entry_abs, os.R_OK):
log.debug("ignore %s (no access)" % entry_abs)
continue
if not os.path.isfile(entry_abs):
log.debug("ignore %s (no regular file)" % entry_abs)
continue
if not mimetype_is_supported(entry):
log.debug("ignore %s (wrong mime type)" % entry_abs)
continue
ids.append(entry_abs)
if not self.__show_extensions:
entry = os.path.splitext(entry)[0]
names.append(entry)
return (nested, ids, names)
remuco-source-0.9.6/base/module/remuco/log.py 0000644 0000000 0000000 00000006751 11700415064 021107 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import logging
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
#===============================================================================
# set up default logger
#===============================================================================
class _config:
"""Configuration container"""
FMT = logging.Formatter("%(asctime)s [%(levelname)7s] [%(filename)11s " +
"%(lineno)4d] %(message)s")
FMTX = logging.Formatter("%(levelname)s: %(message)s (check the log for "
"details)")
handler_stdout = logging.StreamHandler()
handler_stdout.setFormatter(FMT)
handler = handler_stdout
logga = logging.getLogger("remuco")
logga.addHandler(handler)
logga.setLevel(INFO)
#===============================================================================
# log functions
#===============================================================================
debug = _config.logga.debug
info = _config.logga.info
warning = _config.logga.warning
error = _config.logga.error
exception = _config.logga.exception
#===============================================================================
# configuration functions
#===============================================================================
def set_file(file):
"""Set log file (pass None to log to stdout)."""
new_handler = None
if file is not None:
try:
new_handler = logging.FileHandler(file, 'w')
except IOError, e:
print("failed to set up log handler (%s)" % e)
return
new_handler.setFormatter(_config.FMT)
print("Log output will be stored in %s" % file)
print("Contribute to Remuco: Please run 'remuco-report' once a client "
"has connected, thanks!")
if _config.handler != _config.handler_stdout:
_config.logga.removeHandler(_config.handler)
if new_handler:
_config.handler_stdout.setLevel(ERROR)
_config.handler_stdout.setFormatter(_config.FMTX)
_config.logga.addHandler(new_handler)
_config.handler = new_handler
else:
_config.handler_stdout.setLevel(_config.logga.level)
_config.handler_stdout.setFormatter(_config.FMT)
_config.handler = _config.handler_stdout
def set_level(level):
""" Set log level (one of log.DEBUG, log.INFO, log.WARNING, log.ERROR)."""
_config.logga.setLevel(level)
if _config.handler is not None:
_config.handler.setLevel(level)
remuco-source-0.9.6/base/module/remuco/manager.py 0000644 0000000 0000000 00000025170 11700415064 021734 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Manage life cycle of stand-alone (not plugin based) player adapters."""
import signal
import gobject
from remuco import log
try:
import dbus
from dbus.exceptions import DBusException
from dbus.mainloop.glib import DBusGMainLoop
except ImportError:
log.warning("dbus not available - dbus using player adapters will crash")
# =============================================================================
# global items and signal handling
# =============================================================================
_ml = None
def _sighandler(signum, frame):
log.info("received signal %i" % signum)
if _ml is not None:
_ml.quit()
# =============================================================================
# start stop functions
# =============================================================================
def _start_pa(pa):
"""Start the given player adapter with error handling."""
log.info("start player adapter")
try:
pa.start()
except StandardError, e:
log.error("failed to start player adapter (%s)" % e)
return False
except Exception, e:
log.exception("** BUG ** %s", e)
return False
else:
log.info("player adapter started")
return True
def _stop_pa(pa):
"""Stop the given player adapter with error handling."""
log.info("stop player adapter")
try:
pa.stop()
except Exception, e:
log.exception("** BUG ** %s", e)
else:
log.info("player adapter stopped")
# =============================================================================
# Polling Observer
# =============================================================================
class _PollingObserver():
"""Polling based observer for a player's run state.
A polling observer uses a custom function to periodically check if a media
player is running and automatically starts and stops the player adapter
accordingly.
"""
def __init__(self, pa, poll_fn):
"""Create a new polling observer.
@param pa:
the PlayerAdapter to automatically start and stop
@param poll_fn:
the function to call periodically to check if the player is running
"""
self.__pa = pa
self.__poll_fn = poll_fn
self.__sid = gobject.timeout_add(5123, self.__poll, False)
gobject.idle_add(self.__poll, True)
def __poll(self, first):
running = self.__poll_fn()
if running and self.__pa.stopped:
_start_pa(self.__pa)
elif not running and not self.__pa.stopped:
_stop_pa(self.__pa)
# else: nothing to do
return first and False or True
def stop(self):
gobject.source_remove(self.__sid)
# =============================================================================
# DBus Observer
# =============================================================================
class _DBusObserver():
"""DBus based observer for a player's run state.
A DBus observer uses DBus name owner change notifications to
automatically start and stop a player adapter if the corresponding
media player starts or stops.
"""
def __init__(self, pa, dbus_name):
"""Create a new DBus observer.
@param pa:
the PlayerAdapter to automatically start and stop
@param dbus_name:
the bus name used by the adapter's media player
"""
DBusGMainLoop(set_as_default=True)
self.__pa = pa
self.__dbus_name = dbus_name
try:
bus = dbus.SessionBus()
except DBusException, e:
log.error("no dbus session bus (%s)" % e)
return
try:
proxy = bus.get_object(dbus.BUS_DAEMON_NAME, dbus.BUS_DAEMON_PATH)
self.__dbus = dbus.Interface(proxy, dbus.BUS_DAEMON_IFACE)
except DBusException, e:
log.error("failed to connect to dbus daemon (%s)" % e)
return
try:
self.__handlers = (
self.__dbus.connect_to_signal("NameOwnerChanged",
self.__on_owner_change,
arg0=self.__dbus_name),
)
self.__dbus.NameHasOwner(self.__dbus_name,
reply_handler=self.__reply_has_owner,
error_handler=self.__dbus_error)
except DBusException, e:
log.error("failed to talk with dbus daemon (%s)" % e)
return
def __on_owner_change(self, name, old, new):
log.debug("dbus name owner changed: '%s' -> '%s'" % (old, new))
_stop_pa(self.__pa)
if new:
_start_pa(self.__pa)
def __reply_has_owner(self, has_owner):
log.debug("dbus name has owner: %s" % has_owner)
if has_owner:
_start_pa(self.__pa)
def __dbus_error(self, error):
log.warning("dbus error: %s" % error)
def stop(self):
for handler in self.__handlers:
handler.remove()
self.__handlers = ()
self.__dbus = None
# =============================================================================
# Manager
# =============================================================================
class Manager(object):
"""Life cycle manager for a stand-alone player adapter.
A manager cares about calling a PlayerAdapter's start and stop methods.
Additionally, because Remuco needs a GLib main loop to run, it sets up and
manages such a loop.
It is intended for player adapters running stand-alone, outside the players
they adapt. A manager is not needed for player adapters realized as a
plugin for a media player. In that case the player's plugin interface
should care about the life cycle of a player adapter (see the Rhythmbox
player adapter as an example).
"""
def __init__(self, pa, dbus_name=None, poll_fn=None):
"""Create a new manager.
@param pa:
the PlayerAdapter to manage
@keyword dbus_name:
if the player adapter uses DBus to communicate with its player set
this to the player's well known bus name (see run() for more
information)
@keyword poll_fn:
if DBus is not used, this function may be set for periodic checks
if the player is running, used to automatically start and stop the
player adapter
When neither `dbus_name` nor `poll_fn` is given, the adapter is started
immediately, assuming the player is running and the adapter is ready to
work.
"""
self.__pa = pa
self.__pa.manager = self
self.__stopped = False
self.__observer = None
global _ml
if _ml is None:
_ml = gobject.MainLoop()
signal.signal(signal.SIGINT, _sighandler)
signal.signal(signal.SIGTERM, _sighandler)
self.__ml = _ml
if dbus_name:
log.info("start dbus observer")
self.__observer = _DBusObserver(pa, dbus_name)
elif poll_fn:
log.info("start polling observer")
self.__observer = _PollingObserver(pa, poll_fn)
else:
# nothing to do
pass
def run(self):
"""Activate the manager.
This method starts the player adapter, runs a main loop (GLib) and
blocks until SIGINT or SIGTERM arrives or until stop() gets called. If
this happens the player adapter gets stopped and this method returns.
If `player_dbus_name` or `poll_fn` has been passed to __init__(), then
the player adapter does not get started until the player is running
(according to checks based on the DBus name or poll function). Also the
adapter gets stopped automatically if the player is not running
anymore. However, the manager keeps running, i.e. the player adapter
may get started and stopped multiple times while this method is
running.
"""
if self.__observer is None: # start pa directly
ready = _start_pa(self.__pa)
else: # observer will start pa
ready = True
if ready and not self.__stopped: # not stopped since creation
log.info("start main loop")
try:
self.__ml.run()
except Exception, e:
log.exception("** BUG ** %s", e)
else:
log.info("main loop stopped")
if self.__observer: # stop observer
self.__observer.stop()
log.info("observer stopped")
# stop pa
_stop_pa(self.__pa)
def stop(self):
"""Shut down the manager.
Stops the manager's main loop and player adapter. As a result a
previous call to run() will return now. This should be used by player
adapters when there is a crucial error and restarting the adapter won't
fix this.
"""
log.info("manager stopped internally")
self.__stopped = True
self.__ml.quit()
class NoManager(object):
"""Dummy manager which can be stopped - does nothing.
Initially this manager is assigned to every PlayerAdapter. That way it is
always safe to call PlayerAdapter.manager.stop() even if an adapter has not
yet or not at all a real Manager.
"""
def stop(self):
"""Stop me, I do nothing."""
remuco-source-0.9.6/base/module/remuco/message.py 0000644 0000000 0000000 00000006443 11700415064 021750 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
IGNORE = 0
# =============================================================================
# connection related messages
# =============================================================================
_CONN = 100
#CONN_PLIST = _CONN
CONN_PINFO = _CONN + 10
CONN_CINFO = _CONN + 20
CONN_SLEEP = _CONN + 30
CONN_WAKEUP = _CONN + 40
CONN_BYE = _CONN + 90
# =============================================================================
# sync messages
# =============================================================================
_SYNC = 200
SYNC_STATE = _SYNC
SYNC_PROGRESS = _SYNC + 1
SYNC_ITEM = _SYNC + 2
# =============================================================================
# control messages
# =============================================================================
_CTRL = 300
CTRL_PLAYPAUSE = _CTRL
CTRL_NEXT = _CTRL + 1
CTRL_PREV = _CTRL + 2
CTRL_SEEK = _CTRL + 3
CTRL_VOLUME = _CTRL + 4
CTRL_REPEAT = _CTRL + 5
CTRL_SHUFFLE = _CTRL + 6
CTRL_FULLSCREEN = _CTRL + 7
CTRL_RATE = _CTRL + 8
CTRL_TAG = _CTRL + 30
CTRL_NAVIGATE = _CTRL + 40 #31 would be ugly
CTRL_SHUTDOWN = _CTRL + 90
# =============================================================================
# action messages
# =============================================================================
_ACT = 400
ACT_PLAYLIST = _ACT
ACT_QUEUE = _ACT + 1
ACT_MLIB = _ACT + 2
ACT_FILES = _ACT + 3
ACT_SEARCH = _ACT + 4
# =============================================================================
# request messages
# =============================================================================
_REQ = 500
REQ_ITEM = _REQ # currently unused
REQ_PLAYLIST = _REQ + 1
REQ_QUEUE = _REQ + 2
REQ_MLIB = _REQ + 3
REQ_FILES = _REQ + 4
REQ_SEARCH = _REQ + 5
# =============================================================================
# internal messages
# =============================================================================
_PRIV = 0x10000000
PRIV_INITIAL_SYNC = _PRIV # used internally in server
# =============================================================================
def _is_in_range(range_start, id):
return id >= range_start and id < range_start + 100
def is_control(id):
return _is_in_range(_CTRL, id)
def is_action(id):
return _is_in_range(_ACT, id)
def is_request(id):
return _is_in_range(_REQ, id)
def is_private(id):
return _is_in_range(_PRIV, id)
remuco-source-0.9.6/base/module/remuco/mpris.py 0000644 0000000 0000000 00000044175 11700415064 021462 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import os.path
import gobject
from remuco.adapter import PlayerAdapter, ItemAction
from remuco.defs import *
from remuco import log
try:
import dbus
from dbus.exceptions import DBusException
except ImportError:
log.warning("dbus not available - MPRIS based player adapters will crash")
# =============================================================================
# MPRIS constants
# =============================================================================
CAN_GO_NEXT = 1 << 0
CAN_GO_PREV = 1 << 1
CAN_PAUSE = 1 << 2
CAN_PLAY = 1 << 3
CAN_SEEK = 1 << 4
CAN_PROVIDE_METADATA = 1 << 5
CAN_HAS_TRACKLIST = 1 << 6
STATUS_PLAYING = 0
STATUS_PAUSED = 1
STATUS_STOPPED = 2
MINFO_KEY_RATING = "rating"
# =============================================================================
# actions
# =============================================================================
IA_APPEND = ItemAction("Append", multiple=True)
IA_APPEND_PLAY = ItemAction("Append and play", multiple=True)
FILE_ACTIONS = (IA_APPEND, IA_APPEND_PLAY)
IA_JUMP = ItemAction("Jump to") # __jump_to() is ambiguous on dynamic playlists
IA_REMOVE = ItemAction("Remove", multiple=True)
PLAYLIST_ACTIONS = [IA_REMOVE]
# =============================================================================
# player adapter
# =============================================================================
class MPRISAdapter(PlayerAdapter):
def __init__(self, name, display_name=None, poll=2.5, mime_types=None,
rating=False, extra_file_actions=None,
extra_playlist_actions=None):
display_name = display_name or name
if rating:
max_rating = 5
else:
max_rating = 0
all_file_actions = FILE_ACTIONS + tuple(extra_file_actions or ())
PlayerAdapter.__init__(self, display_name,
max_rating=max_rating,
playback_known=True,
volume_known=True,
repeat_known=True,
shuffle_known=True,
progress_known=True,
file_actions=all_file_actions,
mime_types=mime_types)
self.__playlist_actions = PLAYLIST_ACTIONS
if self.config.getx("playlist-jump-enabled", "0", int):
self.__playlist_actions.append(IA_JUMP)
if extra_playlist_actions:
self.__playlist_actions.extend(extra_playlist_actions)
self.__name = name
self.__dbus_signal_handler = ()
self._mp_p = None
self._mp_t = None
self._repeat = False
self._shuffle = False
self._playing = PLAYBACK_STOP
self.__volume = 0
self.__progress_now = 0
self.__progress_max = 0
self.__can_pause = False
self.__can_play = False
self.__can_seek = False
self.__can_next = False
self.__can_prev = False
self.__can_tracklist = False
log.debug("init done")
def start(self):
PlayerAdapter.start(self)
try:
bus = dbus.SessionBus()
proxy = bus.get_object("org.mpris.%s" % self.__name, "/Player")
self._mp_p = dbus.Interface(proxy, "org.freedesktop.MediaPlayer")
proxy = bus.get_object("org.mpris.%s" % self.__name, "/TrackList")
self._mp_t = dbus.Interface(proxy, "org.freedesktop.MediaPlayer")
except DBusException, e:
raise StandardError("dbus error: %s" % e)
try:
self.__dbus_signal_handler = (
self._mp_p.connect_to_signal("TrackChange",
self._notify_track),
self._mp_p.connect_to_signal("StatusChange",
self._notify_status),
self._mp_p.connect_to_signal("CapsChange",
self._notify_caps),
self._mp_t.connect_to_signal("TrackListChange",
self._notify_tracklist_change),
)
except DBusException, e:
raise StandardError("dbus error: %s" % e)
try:
self._mp_p.GetStatus(reply_handler=self._notify_status,
error_handler=self._dbus_error)
self._mp_p.GetMetadata(reply_handler=self._notify_track,
error_handler=self._dbus_error)
self._mp_p.GetCaps(reply_handler=self._notify_caps,
error_handler=self._dbus_error)
except DBusException, e:
# this is not necessarily a fatal error
log.warning("dbus error: %s" % e)
def stop(self):
PlayerAdapter.stop(self)
for handler in self.__dbus_signal_handler:
handler.remove()
self.__dbus_signal_handler = ()
self._mp_p = None
self._mp_t = None
def poll(self):
self._poll_volume()
self._poll_progress()
# =========================================================================
# control interface
# =========================================================================
def ctrl_toggle_playing(self):
try:
if self._playing == PLAYBACK_STOP:
self._mp_p.Play(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
else:
self._mp_p.Pause(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_toggle_repeat(self):
try:
self._mp_t.SetLoop(not self._repeat,
reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_toggle_shuffle(self):
try:
self._mp_t.SetRandom(not self._shuffle,
reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_next(self):
if not self.__can_next:
log.debug("go to next item is currently not possible")
return
try:
self._mp_p.Next(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_previous(self):
if not self.__can_prev:
log.debug("go to previous is currently not possible")
return
try:
self._mp_p.Prev(reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def ctrl_volume(self, direction):
if direction == 0:
volume = 0
else:
volume = self.__volume + 5 * direction
volume = min(volume, 100)
volume = max(volume, 0)
try:
self._mp_p.VolumeSet(volume, reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
gobject.idle_add(self._poll_volume)
def ctrl_seek(self, direction):
if not self.__can_seek:
log.debug("seeking is currently not possible")
return
self.__progress_now += 5 * direction
self.__progress_now = min(self.__progress_now, self.__progress_max)
self.__progress_now = max(self.__progress_now, 0)
log.debug("new progress: %d" % self.__progress_now)
try:
self._mp_p.PositionSet(self.__progress_now * 1000,
reply_handler=self._dbus_ignore,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
gobject.idle_add(self._poll_progress)
# =========================================================================
# actions interface
# =========================================================================
def action_files(self, action_id, files, uris):
if action_id == IA_APPEND.id or action_id == IA_APPEND_PLAY.id:
try:
self._mp_t.AddTrack(uris[0], action_id == IA_APPEND_PLAY.id)
for uri in uris[1:]:
self._mp_t.AddTrack(uri, False)
except DBusException, e:
log.warning("dbus error: %s" % e)
return
else:
log.error("** BUG ** unexpected action: %d" % action_id)
def action_playlist_item(self, action_id, positions, ids):
if action_id == IA_REMOVE.id:
positions.sort()
positions.reverse()
try:
for pos in positions:
self._mp_t.DelTrack(pos)
except DBusException, e:
log.warning("dbus error: %s" % e)
return
elif action_id == IA_JUMP.id:
self.__jump_to(positions[0])
else:
log.error("** BUG ** unexpected action: %d" % action_id)
# =========================================================================
# request interface
# =========================================================================
def request_playlist(self, reply):
if not self.__can_tracklist:
reply.send()
return
# TODO: very slow for SongBird, should be async
tracks = self.__get_tracklist()
for track in tracks:
id, info = self.__track2info(track)
artist = info.get(INFO_ARTIST, "???")
title = info.get(INFO_TITLE, "???")
name = "%s - %s" % (artist, title)
reply.ids.append(id)
reply.names.append(name)
reply.item_actions = self.__playlist_actions
reply.send()
# =========================================================================
# internal methods (may be overridden by subclasses to fix MPRIS issues)
# =========================================================================
def _poll_status(self):
"""Poll player status information.
Some MPRIS players do not notify about all status changes, so that
status must be polled. Subclasses may call this method for that purpose.
"""
try:
self._mp_p.GetStatus(reply_handler=self._notify_status,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def _poll_volume(self):
try:
self._mp_p.VolumeGet(reply_handler=self._notify_volume,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def _poll_progress(self):
try:
self._mp_p.PositionGet(reply_handler=self._notify_progress,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def _notify_track(self, track):
log.debug("track: %s" % str(track))
id, info = self.__track2info(track)
self.__progress_max = info.get(INFO_LENGTH, 0) # for update_progress()
img = track.get("arturl")
if not img or not img.startswith("file:"):
img = self.find_image(id)
self.update_item(id, info, img)
try:
self._mp_t.GetCurrentTrack(reply_handler=self._notify_position,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def _notify_status(self, status):
log.debug("status: %s " % str(status))
if status[0] == STATUS_STOPPED:
self._playing = PLAYBACK_STOP
elif status[0] == STATUS_PAUSED:
self._playing = PLAYBACK_PAUSE
elif status[0] == STATUS_PLAYING:
self._playing = PLAYBACK_PLAY
else:
log.warning("unknown play state (%s), assume playing)" % status[0])
self._playing = PLAYBACK_PLAY
self.update_playback(self._playing)
self._shuffle = status[1] # remember for toggle_shuffle()
self.update_shuffle(self._shuffle)
self._repeat = status[2] or status[3] # for toggle_repeat()
self.update_repeat(self._repeat)
def _notify_tracklist_change(self, new_len):
log.debug("tracklist change")
try:
self._mp_t.GetCurrentTrack(reply_handler=self._notify_position,
error_handler=self._dbus_error)
except DBusException, e:
log.warning("dbus error: %s" % e)
def _notify_position(self, position):
log.debug("tracklist pos: %d" % position)
self.update_position(position)
def _notify_volume(self, volume):
self.__volume = volume # remember for ctrl_volume()
self.update_volume(volume)
def _notify_progress(self, progress):
self.__progress_now = progress // 1000 # remember for ctrl_seek()
self.update_progress(self.__progress_now, self.__progress_max)
def _notify_caps(self, caps):
self.__can_play = caps & CAN_PLAY != 0
self.__can_pause = caps & CAN_PAUSE != 0
self.__can_next = caps & CAN_GO_NEXT != 0
self.__can_prev = caps & CAN_GO_PREV != 0
self.__can_seek = caps & CAN_SEEK != 0
self.__can_tracklist = caps & CAN_HAS_TRACKLIST != 0
# =========================================================================
# internal methods (private)
# =========================================================================
def __get_tracklist(self):
"""Get a list of track dicts of all tracks in the tracklist."""
try:
length = self._mp_t.GetLength()
except DBusException, e:
log.warning("dbus error: %s" % e)
length = 0
if length == 0:
return []
tracks = []
for i in range(0, length):
try:
tracks.append(self._mp_t.GetMetadata(i))
except DBusException, e:
log.warning("dbus error: %s" % e)
return []
return tracks
def __track2info(self, track):
"""Convert an MPRIS meta data dict to a Remuco info dict."""
id = track.get("location", "None")
info = {}
title_alt = os.path.basename(id)
title_alt = os.path.splitext(title_alt)[0]
info[INFO_TITLE] = track.get("title", title_alt)
info[INFO_ARTIST] = track.get("artist", "")
info[INFO_ALBUM] = track.get("album", "")
info[INFO_GENRE] = track.get("genre", "")
info[INFO_YEAR] = track.get("year", "")
info[INFO_LENGTH] = track.get("time", track.get("mtime", 0) // 1000)
info[INFO_RATING] = track.get("rating", 0)
return (id, info)
def __jump_to(self, position):
"""Jump to a position in the tracklist.
MPRIS has no such method, this is a workaround. Unfortunately it
behaves not as expected on dynamic playlists.
"""
tracks = self.__get_tracklist()
if position >= len(tracks):
return
uris = []
for track in tracks[position:]:
uris.append(track.get("location", "there must be a location"))
positions = range(position, len(tracks))
self.action_playlist_item(IA_REMOVE.id, positions, uris)
self.action_files(IA_APPEND_PLAY.id, [], uris)
# =========================================================================
# dbus reply handler (may be reused by subclasses)
# =========================================================================
def _dbus_error(self, error):
""" DBus error handler."""
if self._mp_p is None:
return # do not log errors when not stopped already
log.warning("DBus error: %s" % error)
def _dbus_ignore(self):
""" DBus reply handler for methods without reply."""
pass
remuco-source-0.9.6/base/module/remuco/net.py 0000644 0000000 0000000 00000043154 11700415064 021112 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import socket
import struct
import time
import bluetooth
import gobject
from remuco import log
from remuco import message
from remuco import report
from remuco import serial
from remuco.data import ClientInfo
from remuco.remos import zc_publish, zc_unpublish
def build_message(id, serializable):
"""Create a message ready to send on a socket.
@param id:
message id
@param serializable:
message content (object of type Serializable)
@return:
the message as a binary string or None if serialization failed
"""
# This is not included in ClientConnection.send() because if there are
# multiple clients, each client would serialize the data to send again.
# Using this method, a message can be serialized once and send to many
# clients.
if serializable is not None:
ba = serial.pack(serializable)
if ba is None:
log.warning("failed to serialize (msg-id %d)" % id)
return None
else:
ba = ""
header = struct.pack("!hi", id, len(ba))
return "%s%s" % (header, ba)
class ReceiveBuffer(object):
""" A box to pool some receive buffer related data. """
def __init__(self):
self.header = ""
self.data = ""
self.rest = 0
class ClientConnection(object):
IO_HEADER_LEN = 6
IO_MSG_MAX_SIZE = 10240 # prevent DOS
IO_PREFIX = '\xff\xff\xff\xff'
IO_SUFFIX = '\xfe\xfe\xfe\xfe'
IO_PROTO_VERSION = '\x0a'
IO_HELLO = "%s%s%s" % (IO_PREFIX, IO_PROTO_VERSION, IO_SUFFIX) # hello msg
def __init__(self, sock, addr, clients, pinfo_msg, msg_handler_fn, c_type):
self.__sock = sock
self.__addr = addr
self.__clients = clients
self.__pinfo_msg = pinfo_msg
self.__msg_handler_fn = msg_handler_fn
self.__conn_type = c_type
# client info
self.info = ClientInfo()
self.__psave = False
# the following fields are used for iterative receiving on message data
# see io_recv() and io_recv_buff()
self.__rcv_buff_header = ReceiveBuffer()
self.__rcv_buff_data = ReceiveBuffer()
self.__rcv_msg_id = message.IGNORE
self.__rcv_msg_size = 0
self.__snd_buff = "" # buffer for outgoing data
# source IDs for various events
self.__sids = [
gobject.io_add_watch(self.__sock, gobject.IO_IN, self.__io_recv),
gobject.io_add_watch(self.__sock, gobject.IO_ERR, self.__io_error),
gobject.io_add_watch(self.__sock, gobject.IO_HUP, self.__io_hup)
]
self.__sid_out = 0
log.debug("send 'hello' to %s" % self)
self.send(ClientConnection.IO_HELLO)
def __str__(self):
return str(self.__addr)
#==========================================================================
# io
#==========================================================================
def __recv_buff(self, rcv_buff):
""" Receive some data and put it into the given ReceiveBuffer.
@param rcv_buff: the receive buffer to put received data into
@return: true if some data has been received, false if an error occurred
"""
try:
log.debug("try to receive %d bytes" % rcv_buff.rest)
data = self.__sock.recv(rcv_buff.rest)
except socket.timeout, e: # TODO: needed?
log.warning("connection to %s broken (%s)" % (self, e))
self.disconnect()
return False
except socket.error, e:
log.warning("connection to %s broken (%s)" % (self, e))
self.disconnect()
return False
received = len(data)
log.debug("received %d bytes" % received)
if received == 0:
log.warning("connection to %s broken (no data)" % self)
self.disconnect()
return False
rcv_buff.data = "%s%s" % (rcv_buff.data, data)
rcv_buff.rest -= received
return True
def __io_recv(self, fd, cond):
""" GObject callback function (when there is data to receive). """
log.debug("data from client %s available" % self)
# --- init buffers on new message -------------------------------------
if (self.__rcv_buff_header.rest + self.__rcv_buff_data.rest == 0):
self.__rcv_msg_id = message.IGNORE
self.__rcv_msg_size = 0 # will be set later
self.__rcv_buff_header.data = ""
self.__rcv_buff_header.rest = ClientConnection.IO_HEADER_LEN
self.__rcv_buff_data.data = ""
self.__rcv_buff_data.rest = 0 # will be set later
# --- receive header --------------------------------------------------
if self.__rcv_buff_header.rest > 0:
ok = self.__recv_buff(self.__rcv_buff_header)
if not ok:
return False
if self.__rcv_buff_header.rest > 0:
return True # more data to read, come back later
id, size = struct.unpack('!hi', self.__rcv_buff_header.data)
if size > ClientConnection.IO_MSG_MAX_SIZE:
log.warning("msg from %s too big (%d bytes)" % (self, size))
self.disconnect()
return False
log.debug("incoming msg: %d, %dB" % (id, size))
self.__rcv_buff_data.rest = size
self.__rcv_msg_id, self.__rcv_msg_size = id, size
if size > 0:
return True # more data to read, come back later
# --- receive content -------------------------------------------------
if self.__rcv_buff_data.rest > 0:
ok = self.__recv_buff(self.__rcv_buff_data)
if not ok:
return False
if self.__rcv_buff_data.rest > 0:
return True # more data to read, come back later
# --- message complete ------------------------------------------------
msg_id = self.__rcv_msg_id
msg_data = self.__rcv_buff_data.data
log.debug("incoming msg ")
if msg_id == message.IGNORE:
log.debug("received ignore msg (probably a ping)")
elif msg_id == message.CONN_SLEEP:
self.__psave = True
elif msg_id == message.CONN_WAKEUP:
self.__psave = False
self.__msg_handler_fn(self, message.PRIV_INITIAL_SYNC, None)
elif msg_id == message.CONN_CINFO:
log.debug("received client info from %s" % self)
serial.unpack(self.info, msg_data)
if not self in self.__clients: # initial client info
device = self.info.device.copy()
device["conn"] = self.__conn_type
report.log_device(device)
self.__clients.append(self)
log.debug("sending player info to %s" % self)
self.send(self.__pinfo_msg)
self.__msg_handler_fn(self, message.PRIV_INITIAL_SYNC, None)
else:
self.__msg_handler_fn(self, msg_id, msg_data)
return True
def __io_error(self, fd, cond):
""" GObject callback function (when there is an error). """
log.warning("connection to client %s broken" % self)
self.disconnect()
return False
def __io_hup(self, fd, cond):
""" GObject callback function (when other side disconnected). """
log.info("client %s disconnected" % self)
self.disconnect()
return False
def __io_send(self, fd, cond):
""" GObject callback function (when data can be written). """
if not self.__snd_buff:
self.__sid_out = 0
return False
log.debug("try to send %d bytes to %s" % (len(self.__snd_buff), self))
try:
sent = self.__sock.send(self.__snd_buff)
except socket.error, e:
log.warning("failed to send data to %s (%s)" % (self, e))
self.disconnect()
return False
log.debug("sent %d bytes" % sent)
if sent == 0:
log.warning("failed to send data to %s" % self)
self.disconnect()
return False
self.__snd_buff = self.__snd_buff[sent:]
if not self.__snd_buff:
self.__sid_out = 0
return False
else:
return True
def send(self, msg):
"""Send a message to the client.
@param msg:
complete message (incl. ID and length) in binary format
(net.build_message() is your friend here)
@see: net.build_message()
"""
if msg is None:
log.error("** BUG ** msg is None")
return
if self.__sock is None:
log.debug("cannot send message to %s, already disconnected" % self)
return
if self.__psave:
log.debug("%s is in sleep mode, send nothing" % self)
return
self.__snd_buff = "%s%s" % (self.__snd_buff, msg)
# if not already trying to send data ..
if self.__sid_out == 0:
# .. do it when it is possible:
self.__sid_out = gobject.io_add_watch(self.__sock, gobject.IO_OUT,
self.__io_send)
def disconnect(self, remove_from_list=True, send_bye_msg=False):
""" Disconnect the client.
@keyword remove_from_list: whether to remove the client from the client
list or not (default is true)
@keyword send_bye_msg: whether to send a bye message before
disconnecting
"""
# send bye message
if send_bye_msg and self.__sock is not None:
log.info("send 'bye' to %s" % self)
msg = build_message(message.CONN_BYE, None)
sent = 0
retry = 0
while sent < len(msg) and retry < 10:
try:
sent += self.__sock.send(msg)
except socket.error, e:
log.warning("failed to send 'bye' to %s (%s)" % (self, e))
break
time.sleep(0.02)
retry += 1
if sent < len(msg):
log.warning("failed to send 'bye' to %s" % self)
else:
# give client some time to close connection:
time.sleep(0.1)
# disconnect
log.debug("disconnect %s" % self)
if remove_from_list and self in self.__clients:
self.__clients.remove(self)
for sid in self.__sids:
gobject.source_remove(sid)
self.__sids = ()
if (self.__sid_out > 0):
gobject.source_remove(self.__sid_out)
self.__sid_out = 0
if self.__sock is not None:
try:
self.__sock.shutdown(socket.SHUT_RDWR)
except socket.error, e:
pass
self.__sock.close()
self.__sock = None
class _Server(object):
SOCKET_TIMEOUT = 2.5
def __init__(self, clients, pinfo, msg_handler_fn, config):
""" Create a new server.
@param clients:
a list to add connected clients to
@param pinfo:
player info (type data.PlayerInfo)
@param msg_handler_fn:
callback function for passing received messages to
@param config:
adapter configuration
"""
self.__clients = clients
self.__msg_handler_fn = msg_handler_fn
self.__pinfo_msg = build_message(message.CONN_PINFO, pinfo)
self.__sid = None
self._pinfo = pinfo
self._config = config
self._sock = None
# set up socket
try:
self._sock = self._create_socket()
self._sock.settimeout(_Server.SOCKET_TIMEOUT)
except (IOError, socket.error), e:
# TODO: socket.error may be removed when 2.5 support is dropped
log.error("failed to set up %s server (%s)" % (self._get_type(), e))
return
log.info("created %s server" % self._get_type())
# watch socket
self.__sid = gobject.io_add_watch(self._sock,
gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.__handle_io)
#==========================================================================
# io
#==========================================================================
def __handle_io(self, fd, condition):
""" GObject callback function (when there is a socket event). """
if condition == gobject.IO_IN:
try:
log.debug("connection request from %s client" % self._get_type())
client_sock, addr = self._sock.accept()
log.debug("connection request accepted")
client_sock.setblocking(0)
ClientConnection(client_sock, addr, self.__clients,
self.__pinfo_msg, self.__msg_handler_fn,
self._get_type())
except IOError, e:
log.error("accepting %s client failed: %s" %
(self._get_type(), e))
return True
else:
log.error("%s server socket broken" % self._get_type())
self.__sid = None
return False
def down(self):
""" Shut down the server. """
if self.__sid is not None:
gobject.source_remove(self.__sid)
if self._sock is not None:
log.debug("closing %s server socket" % self._get_type())
try:
self._sock.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
self._sock.close()
self._sock = None
def _create_socket(self):
""" Create the server socket.
@return: a socket object
"""
raise NotImplementedError
#==========================================================================
# miscellaneous
#==========================================================================
def _get_type(self):
"""Get server type name."""
raise NotImplementedError
class BluetoothServer(_Server):
UUID = "025fe2ae-0762-4bed-90f2-d8d778f020fe"
def _create_socket(self):
try:
sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock.bind(("", self._config.bluetooth_channel or bluetooth.PORT_ANY))
sock.listen(1)
sock.settimeout(0.33)
bluetooth.advertise_service(sock, self._pinfo.name,
service_id=BluetoothServer.UUID,
service_classes=[BluetoothServer.UUID, bluetooth.SERIAL_PORT_CLASS],
profiles=[bluetooth.SERIAL_PORT_PROFILE])
except Exception, e:
# bluez does not always convert its internal error into a
# IOError-based BluetoothError, so we need to catch here everything
# and convert internal Bluetooth errors to regular IO errors.
if isinstance(e, IOError):
raise e
else:
raise IOError(*e)
return sock
def down(self):
if self._sock is not None:
try:
bluetooth.stop_advertising(self._sock)
except bluetooth.BluetoothError, e:
log.warning("failed to unregister bluetooth service (%s)" % e)
super(BluetoothServer, self).down()
def _get_type(self):
return "bluetooth"
class WifiServer(_Server):
def _create_socket(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', self._config.wifi_port))
sock.listen(1)
zc_publish(self._pinfo.name, sock.getsockname()[1])
return sock
def _get_type(self):
return "wifi"
def down(self):
zc_unpublish()
super(WifiServer, self).down()
remuco-source-0.9.6/base/module/remuco/remos.py 0000644 0000000 0000000 00000016374 11700415064 021455 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Module to abstract platform specific code."""
from __future__ import with_statement
import os
from os.path import join as opj
import re
import sys
from remuco import log
# =============================================================================
# platform detection
# =============================================================================
linux = sys.platform.startswith("linux")
windows = sys.platform.startswith("win")
mac = sys.platform == "darwin"
# =============================================================================
# helpers
# =============================================================================
def _real_path(p):
"""Expand a path to a variable-free absolute path."""
return os.path.abspath(os.path.expanduser(os.path.expandvars(p)))
# =============================================================================
# locations
# =============================================================================
# media_dirs:
# Maps some mimetypes to a list of locations which typically contain files
# of a specific mimetype. For those mimetypes not mapped, `user_home` may
# be used as a fallback.
if linux:
import xdg.BaseDirectory
user_home = os.getenv("HOME")
user_config_dir = xdg.BaseDirectory.xdg_config_home
user_cache_dir = xdg.BaseDirectory.xdg_cache_home
media_dirs = {}
try:
with open(opj(user_config_dir, "user-dirs.dirs")) as fp:
_udc = fp.read()
except IOError, e:
log.warning("failed to load user dirs config (%s)" % e)
media_dirs["audio"] = ["~/Music"]
media_dirs["video"] = ["~/Videos"]
else:
m = re.search(r'XDG_MUSIC_DIR="([^"]+)', _udc)
media_dirs["audio"] = [m and m.groups()[0] or "~/Music"]
m = re.search(r'XDG_VIDEOS_DIR="([^"]+)', _udc)
media_dirs["video"] = [m and m.groups()[0] or "~/Video"]
elif windows:
from win32com.shell import shell, shellcon
user_home = shell.SHGetFolderPath(0, shellcon.CSIDL_PERSONAL, 0, 0)
user_config_dir = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA,0, 0)
user_cache_dir = shell.SHGetFolderPath(0, shellcon.CSIDL_LOCAL_APPDATA, 0, 0)
media_dirs = {}
media_dirs["audio"] = [shell.SHGetFolderPath(0, shellcon.CSIDL_MYMUSIC, 0, 0),
shell.SHGetFolderPath(0, shellcon.CSIDL_COMMON_MUSIC, 0, 0)]
media_dirs["video"] = [shell.SHGetFolderPath(0, shellcon.CSIDL_MYVIDEO, 0, 0),
shell.SHGetFolderPath(0, shellcon.CSIDL_COMMON_VIDEO, 0, 0)]
elif mac:
raise NotImplementedError
else:
assert False
# sanitize locations:
for mime_type, dirs in list(media_dirs.items()): # list prevents iter/edit conflicts
media_dirs[mime_type] = [_real_path(p) for p in dirs]
media_dirs[mime_type] = [p for p in dirs if os.path.exists(p)]
# =============================================================================
# user notifications
# =============================================================================
if linux:
import dbus
from dbus.exceptions import DBusException
def notify(title, text):
"""Notify the user that a new device has been loggend."""
try:
bus = dbus.SessionBus()
except DBusException, e:
log.error("no dbus session bus (%s)" % e)
return
try:
proxy = bus.get_object("org.freedesktop.Notifications",
"/org/freedesktop/Notifications")
notid = dbus.Interface(proxy, "org.freedesktop.Notifications")
except DBusException, e:
log.error("failed to connect to notification daemon (%s)" % e)
return
try:
caps = notid.GetCapabilities()
except DBusException, e:
return
if not caps or "body-markup" not in caps:
text = text.replace("", "")
text = text.replace("", "")
try:
notid.Notify("Remuco", 0, "phone", title, text, [], {}, 15)
except DBusException, e:
log.warning("user notification failed (%s)" % e)
return
else:
def notify(title, text):
log.info("%s: %s" % (title, text))
# TODO: implementations for mac and win
# =============================================================================
# zeroconf
# =============================================================================
_ZC_TYPE = "_remuco._tcp"
if linux:
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from dbus.exceptions import DBusException
import gobject
dbus.set_default_main_loop(DBusGMainLoop())
# Avahi DBus constants (defined here to prevent python-avahi dependency)
_DBA_NAME = "org.freedesktop.Avahi"
_DBA_INTERFACE_SERVER = _DBA_NAME + ".Server"
_DBA_PATH_SERVER = "/"
_DBA_INTERFACE_ENTRY_GROUP = _DBA_NAME + ".EntryGroup"
_zc_group = None
def zc_publish(player, port):
"""Publish a service for the given player at the given port."""
zc_unpublish()
log.debug("publishing zeroconf service")
try:
bus = dbus.SystemBus()
obj = bus.get_object(_DBA_NAME, _DBA_PATH_SERVER)
server = dbus.Interface(obj, _DBA_INTERFACE_SERVER)
obj = bus.get_object(_DBA_NAME, server.EntryGroupNew())
group = dbus.Interface(obj, _DBA_INTERFACE_ENTRY_GROUP)
group.AddService(-1, -1, 0, "Remuco %s" % player, _ZC_TYPE, "local",
"", port, "")
group.Commit()
except dbus.DBusException, e:
log.warning("failed to publish zeroconf service (%s)" % e)
group = None
else:
log.debug("published zeroconf service")
global _zc_group
_zc_group = group
def zc_unpublish():
"""Unpublish the previously published service."""
global _zc_group
if _zc_group:
try:
_zc_group.Reset()
except DBusException, e:
log.warning("failed to unpublish zeroconf service (%s)" % e)
_zc_group = None
else:
def zc_publish(player, port):
log.warning("publishing zeroconf services not implemented on this OS")
def zc_unpublish():
pass
remuco-source-0.9.6/base/module/remuco/report.py 0000644 0000000 0000000 00000007530 11700415064 021635 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
"""Remuco report handler."""
import httplib
import os
import os.path
import urllib
from remuco.config import DEVICE_FILE
from remuco import dictool
from remuco.remos import notify
__HOST = "remuco.sourceforge.net"
__LOC = "/cgi-bin/report"
__DEVICE_FILE_COMMENT = """# Seen Remuco client devices.
#
# The information in this file is sent to remuco.sourceforge.net if you run
# the tool 'remuco-report'. It is used to set up a list of Remuco compatible
# mobile devices.
#
"""
# Fields of a client device info to log.
__FIELDS = ("name", "version", "conn", "utf8", "touch")
def log_device(device):
"""Log a client device."""
device = dictool.dict_to_string(device, keys=__FIELDS)
seen_devices = dictool.read_dicts_from_file(DEVICE_FILE, flat=True,
keys=__FIELDS)
if not device in seen_devices:
notify("New Remuco Client",
"Please run the tool remuco-report !")
seen_devices.append(device)
dictool.write_dicts_to_file(DEVICE_FILE, seen_devices,
comment=__DEVICE_FILE_COMMENT)
def __send_device(device):
"""Send a single device."""
print("sending %s" % device)
params = urllib.urlencode(device)
#print(params)
headers = {"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/plain"}
try:
conn = httplib.HTTPConnection(__HOST)
conn.request("POST", __LOC, params, headers)
response = conn.getresponse()
except IOError, e:
return -1, str(e)
response.read() # needed ?
conn.close()
return response.status, response.reason
def __send_devices():
"""Send all seen devices.
@return: True if sending was successful, False if something failed
"""
device_list = dictool.read_dicts_from_file(DEVICE_FILE, flat=False,
keys=__FIELDS)
ok = True
for device in device_list:
# add a simple watchword marking this report as a real one
device["ww"] = "sun_is_shining"
status, reason = __send_device(device)
if status != httplib.OK:
print("-> failed (%s - %s)" % (status, reason))
if status == httplib.NOT_FOUND:
print(" the submission link I'm using may be outdated")
ok = False
else:
print("-> ok")
return ok
if __name__ == '__main__':
import sys
if len(sys.argv) == 2:
if sys.argv[1] == "send":
ok = __send_devices()
if ok:
sys.exit(os.EX_OK)
else:
sys.exit(os.EX_TEMPFAIL)
elif sys.argv[1] == "dump":
devices = dictool.read_dicts_from_file(DEVICE_FILE, flat=True)
for dev in devices:
print(dev)
sys.exit(os.EX_OK)
sys.exit(os.EX_USAGE)
remuco-source-0.9.6/base/module/remuco/serial.py 0000644 0000000 0000000 00000032463 11700415064 021604 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import inspect
import struct
import array
from remuco import log
TYPE_Y = 1
TYPE_I = 2
TYPE_B = 3
TYPE_S = 4
TYPE_AY = 5
TYPE_AI = 6
TYPE_AS = 7
TYPE_L = 8
TYPE_N = 9
TYPE_AN = 10
TYPE_AB = 11
TYPE_AL = 12
class Bin:
NET_ENCODING = "UTF-8" # codec for data exchanged with clients
NET_ENCODING_ALT = ("UTF-8", "UTF8", "utf-8", "utf8") # synonyms
HOST_ENCODING = NET_ENCODING # will be updated with value from config file
def __init__(self, buff=None):
self.__data = buff or array.array('c')
self.__off = 0
def get_buff(self):
if isinstance(self.__data, basestring):
return self.__data
elif isinstance(self.__data, array.array):
return self.__data.tostring()
else:
log.error("** BUG ** unexpected buffer type")
def read_boolean(self):
b = self.read_byte()
if b == 0:
return False
else:
return True
def read_byte(self):
y = struct.unpack_from('b', self.__data, offset=self.__off)[0]
self.__off += 1
return y
def read_short(self):
n = struct.unpack_from('!h', self.__data, offset=self.__off)[0]
self.__off += 2
return n
def read_int(self):
i = struct.unpack_from('!i', self.__data, offset=self.__off)[0]
self.__off += 4
return i
def read_long(self):
l = struct.unpack_from('!q', self.__data, offset=self.__off)[0]
self.__off += 8
return l
def read_string(self):
""" Read a string.
The read raw string will be converted from Bin.NET_ENCODING to
Bin.HOST_ENCODING.
"""
s = self.__read_string()
if Bin.HOST_ENCODING not in Bin.NET_ENCODING_ALT:
try:
s = unicode(s, Bin.NET_ENCODING).encode(Bin.HOST_ENCODING)
except UnicodeDecodeError, e:
log.warning("could not decode '%s' with codec %s (%s)" %
(s, Bin.NET_ENCODING, e))
except UnicodeEncodeError, e:
log.warning("could not encode '%s' with codec %s (%s)" %
(s, Bin.HOST_ENCODING, e))
return s
def read_type(self, expected):
type = self.read_byte()
if type != expected:
log.warning("bin data malformed (expected type %d, have %d)" %
(expected, type))
return False
else:
return True
def read_array_boolean(self):
return self.__read_array(self.read_boolean)
def read_array_byte(self):
return self.__read_array(self.read_byte)
def read_array_short(self):
return self.__read_array(self.read_short)
def read_array_int(self):
return self.__read_array(self.read_int)
def read_array_long(self):
return self.__read_array(self.read_long)
def read_array_string(self):
return self.__read_array(self.read_string)
def __read_string(self):
""" Read a string as it is, i.e. without any codec conversion. """
l = self.read_short()
s = struct.unpack_from('%ds' % l, self.__data, offset=self.__off)[0]
self.__off += l
return s
def __read_array(self, fn_read_element):
num = self.read_int()
a = []
for i in range(num):
a.append(fn_read_element())
return a
def get_unused_data(self):
return len(self.__data) - self.__off
def write_type(self, type):
self.write_byte(type)
def write_boolean(self, b):
if b:
self.write_byte(1)
else:
self.write_byte(0)
def write_byte(self, y):
if y is None: y = 0
self.__data.extend(' ' * 1)
struct.pack_into('b', self.__data, self.__off, y)
self.__off += 1
def write_short(self, n):
if n is None: n = 0
self.__data.extend(' ' * 2)
struct.pack_into('!h', self.__data, self.__off, n)
self.__off += 2
def write_int(self, i):
if i is None: i = 0
self.__data.extend(' ' * 4)
struct.pack_into('!i', self.__data, self.__off, i)
self.__off += 4
def write_long(self, l):
if l is None: l = 0
self.__data.extend(' ' * 8)
struct.pack_into('!q', self.__data, self.__off, l)
self.__off += 8
def write_string(self, s):
""" Write a string.
If the string is a unicode string, it will be encoded as a normal string
in Bin.NET_ENCODING. If it already is a normal string it will be
converted from Bin.HOST_ENCODING to Bin.NET_ENCODING.
"""
if s is None:
self.__write_string(s)
return
if isinstance(s, unicode):
try:
s = s.encode(Bin.NET_ENCODING)
except UnicodeEncodeError, e:
log.warning("could not encode '%s' with codec %s (%s)" %
(s, Bin.NET_ENCODING, e))
s = str(s)
elif Bin.HOST_ENCODING not in Bin.NET_ENCODING_ALT:
log.debug("convert '%s' from %s to %s" %
(s, Bin.HOST_ENCODING, Bin.NET_ENCODING))
try:
s = unicode(s, Bin.HOST_ENCODING).encode(Bin.NET_ENCODING)
except UnicodeDecodeError, e:
log.warning("could not decode '%s' with codec %s (%s)" %
(s, Bin.HOST_ENCODING, e))
except UnicodeEncodeError, e:
log.warning("could not encode '%s' with codec %s (%s)" %
(s, Bin.NET_ENCODING, e))
self.__write_string(s)
def write_array_boolean(self, ba):
self.__write_array(ba, self.write_boolean)
def write_array_byte(self, ba):
if isinstance(ba, str): # byte sequences often come as strings
self.__write_string(ba, len_as_int=True)
else:
self.__write_array(ba, self.write_byte)
def write_array_short(self, na):
self.__write_array(na, self.write_short)
def write_array_int(self, ia):
self.__write_array(ia, self.write_int)
def write_array_long(self, ia):
self.__write_array(ia, self.write_long)
def write_array_string(self, sa):
self.__write_array(sa, self.write_string)
def __write_string(self, s, len_as_int=False):
""" Write a string.
The string is written as is, i.e. there is no codec conversion.
"""
if s is None:
s = ""
if not isinstance(s, basestring):
s = str(s)
l = len(s)
if len_as_int:
self.write_int(l)
else:
self.write_short(l)
self.__data.extend(' ' * l)
struct.pack_into('%ds' % l, self.__data, self.__off, s)
self.__off += l
def __write_array(self, a, fn_element_write):
if a is None:
l = 0
else:
l = len(a)
self.write_int(l)
for i in range(l):
fn_element_write(a[i])
class Serializable(object):
def get_fmt(self):
raise NotImplementedError
def get_data(self):
raise NotImplementedError
def set_data(self, data):
raise NotImplementedError
def pack(serializable):
fmt = serializable.get_fmt()
data = serializable.get_data()
if len(fmt) != len(data):
log.error("** BUG ** format string and data differ in length")
return None
#log.debug("data to pack: %s" % str(data))
bin = Bin()
try:
for i in range(0,len(fmt)):
type = fmt[i]
bin.write_byte(type)
if type == TYPE_Y:
bin.write_byte(data[i])
elif type == TYPE_B:
bin.write_boolean(data[i])
elif type == TYPE_N:
bin.write_short(data[i])
elif type == TYPE_I:
bin.write_int(data[i])
elif type == TYPE_L:
bin.write_long(data[i])
elif type == TYPE_S:
bin.write_string(data[i])
elif type == TYPE_AB:
bin.write_array_boolean(data[i])
elif type == TYPE_AY:
bin.write_array_byte(data[i])
elif type == TYPE_AN:
bin.write_array_short(data[i])
elif type == TYPE_AI:
bin.write_array_int(data[i])
elif type == TYPE_AL:
bin.write_array_long(data[i])
elif type == TYPE_AS:
bin.write_array_string(data[i])
else:
log.error("** BUG ** unknown type (%d) in format string" % type)
return None
except struct.error, e:
log.exception("** BUG ** %s" % e)
return None
return bin.get_buff()
def unpack(serializable, bytes):
""" Deserialize a Serializable.
@param serializable:
the Serializable to apply the binary data to (may be a class, in which
case a new instance of this class is created)
@param bytes:
binary data (serialized Serializable)
@return: 'serializable' itself if it is an instance of Serializable, a new
instance of 'serializable' if it is a class or None if an error
occurred
"""
if inspect.isclass(serializable):
serializable = serializable()
fmt = serializable.get_fmt()
if fmt and not bytes:
log.warning("there is no data to unpack")
return None
data = []
bin = Bin(buff=bytes)
try:
for type in fmt:
if not bin.read_type(type):
return None
if type == TYPE_Y:
data.append(bin.read_byte())
elif type == TYPE_B:
data.append(bin.read_boolean())
elif type == TYPE_N:
data.append(bin.read_short())
elif type == TYPE_I:
data.append(bin.read_int())
elif type == TYPE_L:
data.append(bin.read_long())
elif type == TYPE_S:
data.append(bin.read_string())
elif type == TYPE_AB:
data.append(bin.read_array_boolean())
elif type == TYPE_AY:
data.append(bin.read_array_byte())
elif type == TYPE_AN:
data.append(bin.read_array_short())
elif type == TYPE_AI:
data.append(bin.read_array_int())
elif type == TYPE_AL:
data.append(bin.read_array_long())
elif type == TYPE_AS:
data.append(bin.read_array_string())
else:
log.warning("bin data malformed (unknown data type: %d)" % type)
return None
except struct.error, e:
log.warning("bin data malformed (%s)" % e)
return None
unused = bin.get_unused_data()
if unused:
log.warning("there are %d unused bytes" % unused)
return None
serializable.set_data(data)
#log.debug("unpacked data : %s" % str(data))
return serializable
remuco-source-0.9.6/base/module/remuco/testshell.py 0000644 0000000 0000000 00000004576 11700415064 022340 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2009 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import signal
import gobject
import inspect
_paref = None
_cmdlist = None
def setup(adapter):
global _paref, _cmdlist
_paref = adapter
_cmdlist = [getattr(adapter, f) for f in dir(adapter)
if f.startswith("ctrl_")]
signal.signal(signal.SIGHUP, handler)
def handler(signum, frame):
"""Ugly handler to call PlayerAdapter's functions and test
functionality. """
signal.signal(signal.SIGHUP, signal.SIG_IGN) #ignore further SIGHUPs
if _paref is not None:
print('Which function should I call?')
for count, f in enumerate(_cmdlist):
parms, _, _, _ = inspect.getargspec(f)
showparms = ''
if parms.__len__() > 1:
showparms = parms[1:] #ignore 'self'
print('[%d] %s (%s)' % (count, f.__name__, showparms))
try:
command = raw_input('Choice: ').split(' ')
idx = int(command[0])
args = command[1:]
#cast what seems to be integer
for i in range(len(args)):
try:
args[i] = int(args[i])
except ValueError:
pass
if idx >= 0 and idx < _cmdlist.__len__():
gobject.idle_add(_cmdlist[idx], *args)
else:
print('Invalid function')
except ValueError:
pass
signal.signal(signal.SIGHUP, handler) #be ready for the next calls
remuco-source-0.9.6/base/module/test/testadapter.py 0000644 0000000 0000000 00000004072 11700415064 022325 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
import gobject
import sys
import remuco.log
from remuco import PlayerAdapter
class AdapterTest(unittest.TestCase):
def setUp(self):
self.__ml = gobject.MainLoop()
ia = remuco.ItemAction("ia_l", multiple=True)
logarg = "--remuco-log-stdout"
if not logarg in sys.argv:
sys.argv.append(logarg)
self.__pa = PlayerAdapter("unittest", playback_known=False,
volume_known=True, repeat_known=True, shuffle_known=False,
progress_known=True, max_rating=3, mime_types=["audio"], poll=1,
file_actions=[ia])
#self.__pa.config.log_level = remuco.log.DEBUG
#self.__pa.config.log_level = remuco.log.INFO
self.__pa.config.log_level = remuco.log.WARNING
def test_adapter(self):
self.__pa.start()
gobject.timeout_add(4000, self.__stop)
self.__ml.run()
def __stop(self):
self.__pa.stop()
self.__ml.quit()
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.test_adapter']
unittest.main() remuco-source-0.9.6/base/module/test/testall.py 0000644 0000000 0000000 00000002510 11700415064 021450 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
import remuco.log
#remuco.log.set_level(remuco.log.DEBUG)
#remuco.log.set_level(remuco.log.INFO)
remuco.log.set_level(remuco.log.WARNING)
from testdictool import DicToolTest
from testserial import SerializationTest
from testnet import ServerTest
from testfiles import FilesTest
from testadapter import AdapterTest
if __name__ == "__main__":
unittest.main() remuco-source-0.9.6/base/module/test/testdictool.py 0000644 0000000 0000000 00000004327 11700415064 022345 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
from remuco import dictool
class DicToolTest(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_it(self):
keys = ("a", "c", "b")
dic1 = { "a": "1:d", "b": "2", "d": "4", "c": "3" }
dic3 = { "a": "1", "b": "2,0", "d": "4", "c": "3" }
flat = dictool.dict_to_string(dic1)
dic2 = dictool.string_to_dict(flat)
assert dic1 == dic2
flat = dictool.dict_to_string(dic1, keys=keys)
assert flat == "a:1:d,c:3,b:2"
dic2 = dictool.string_to_dict(flat)
assert dic1 != dic2
flat = dictool.dict_to_string(dic3, keys=keys)
assert flat == "a:1,c:3,b:2_0"
l1 = [dic1,dic2]
dictool.write_dicts_to_file("/var/tmp/dictest", l1, comment="# hallo")
l2 = dictool.read_dicts_from_file("/var/tmp/dictest")
assert l1 == l2
dictool.write_dicts_to_file("/var/tmp/dictest", [dic1,dic2,dic3,flat],
comment="# hallo", keys=["a"])
l = dictool.read_dicts_from_file("/var/tmp/dictest")
l = dictool.read_dicts_from_file("/var/tmp/non_existent")
if __name__ == "__main__":
unittest.main() remuco-source-0.9.6/base/module/test/testfiles.py 0000644 0000000 0000000 00000004342 11700415064 022007 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
from remuco.files import FileSystemLibrary
class FilesTest(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def __test_path(self, fs, path, depth, limit):
nested, ids, names = fs.get_level(path)
#print("%spath : %s" % (depth, path))
#if nested:
# print("%snested : %s" % (depth, nested))
#if ids:
# print("%sids : %s" % (depth, ids))
#if names:
# print("%snames : %s" % (depth, names))
if len(depth) == limit * 2:
return
if path is None:
path = []
for sub in nested:
self.__test_path(fs, path + [sub], depth + " ", limit)
def test_files(self):
#print("")
fs = FileSystemLibrary(None, ["audio","video"], False, True)
self.__test_path(fs, None, "", limit=0)
self.__test_path(fs, [], "", limit=2)
fs = FileSystemLibrary([ "/home", "/nonexistent", "auto" ],
["audio/mp3", "video/mp4", "application/x-iso9660-image"],
False, True)
self.__test_path(fs, [], "", limit=3)
if __name__ == "__main__":
unittest.main() remuco-source-0.9.6/base/module/test/testnet.py 0000644 0000000 0000000 00000003475 11700415064 021501 0 ustar root root 0000000 0000000 # =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
import gobject
from remuco.data import PlayerInfo
from remuco.net import WifiServer, BluetoothServer
from remuco.config import Config
class ServerTest(unittest.TestCase):
def setUp(self):
self.__ml = gobject.MainLoop()
self.__pi = PlayerInfo("xxx", 0, 0, None, ["1", "2"])
self.__config = Config("unittest")
def test_wifi(self):
s = WifiServer([], self.__pi, None, self.__config)
gobject.timeout_add(2000, self.__stop, s)
self.__ml.run()
def test_bluetooth(self):
s = BluetoothServer([], self.__pi, None, self.__config)
gobject.timeout_add(2000, self.__stop, s)
self.__ml.run()
def __stop(self, s):
s.down()
self.__ml.quit()
if __name__ == "__main__":
unittest.main() remuco-source-0.9.6/base/module/test/testserial.py 0000644 0000000 0000000 00000016227 11700415064 022171 0 ustar root root 0000000 0000000 # -*- coding: UTF-8 -*-
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
import unittest
import remuco
from remuco import data
from remuco import serial
class _SerialzableClass(remuco.serial.Serializable):
def __init__(self):
self.b = None
self.y = None
self.n = None
self.i = None
self.l = None
self.s1 = None
self.s2 = None
self.ba = None
self.ya = None
self.na = None
self.ia = None
self.la = None
self.sa1 = None
self.sa2 = None
def __eq__(self, o):
if self.b != o.b:
print("b differs")
return False
if self.y != o.y:
print("y differs")
return False
if self.n != o.n:
print("n differs")
return False
if self.i != o.i:
print("i differs")
return False
if self.l != o.l:
print("l differs")
return False
if self.s1 != o.s1:
print("s1 differs")
return False
if self.s2 != o.s2:
print("s2 differs")
return False
if self.ba != o.ba:
print("ba differs")
return False
if self.ya != o.ya:
print("ya differs")
return False
if self.na != o.na:
print("na differs")
return False
if self.ia != o.ia:
print("ia differs")
return False
if self.la != o.la:
print("la differs")
return False
if self.sa1 != o.sa1:
print("sa1 differs")
return False
if self.sa2 != o.sa2:
print("sa2 differs")
return False
return True
def init(self):
self.b = True
self.y = 55
self.n = 4 << 10
self.i = 4 << 20
self.l = 3 << 40
self.s1 = "dfödas"
self.s2 = ""
self.ba = [ False, True, False ]
self.ya1 = "bytes as string"
self.ya2 = [ 1, 127, -128 ]
self.na = [ 2 << 10 ]
self.ia1 = [ 1 << 20, 2 << 20 ]
self.ia2 = None
self.la = [ 5 << 40, 6 << 50 ]
self.sa1 = [ "1", "2éü+", None , "" ]
self.sa2 = []
def get_fmt(self):
return (serial.TYPE_B, serial.TYPE_Y, serial.TYPE_N, serial.TYPE_I, serial.TYPE_L,
serial.TYPE_S, serial.TYPE_S,
serial.TYPE_AB, serial.TYPE_AY, serial.TYPE_AY, serial.TYPE_AN, serial.TYPE_AI, serial.TYPE_AI, serial.TYPE_AL,
serial.TYPE_AS, serial.TYPE_AS)
def get_data(self):
return (self.b, self.y, self.n, self.i, self.l,
self.s1, self.s2,
self.ba, self.ya1, self.ya2, self.na, self.ia1, self.ia2, self.la,
self.sa1, self.sa2)
def set_data(self, data):
self.b, self.y, self.n, self.i, self.l, \
self.s1, self.s2, \
self.ba, self.ya1, self.ya2, self.na, self.ia1, self.ia2, self.la, \
self.sa1, self.sa2 = data
class SerializationTest(unittest.TestCase):
def __serialize_and_dump(self, ser):
print("--> serializable:\n%s" % ser)
bindata = serial.pack(ser)
print("--> binary data:")
out = ""
counter = 0
for byte in bindata:
counter += 1
out = "%s %02X" % (out, ord(byte))
if counter % 32 == 0:
out = "%s\n" % out
print(out)
return bindata
def test_serialize_playerinfo(self):
#print("")
ia = remuco.ItemAction("ia_l", multiple=True)
pi = data.PlayerInfo("dings", 123, 4, [ia], ["sm1", "sm2"])
#self.__serialize(pi)
serial.pack(pi)
def test_serialize_itemlist(self):
#print("")
path = [ "path", "to" ]
nested = [ "n1", "n2" ]
ids = ["id1", "id2", "id3" ]
names = [ "na1", "na2", "na3" ]
ia1 = remuco.ItemAction("ia1_l", multiple=True)
ia2 = remuco.ItemAction("ia2_l", multiple=False)
ias = [ ia1, ia2 ]
la1 = remuco.ListAction("la1_l")
las = [ la1 ]
il = data.ItemList(1, path, nested, ids, names, 0, 1, 2, ias, las)
#self.__serialize(il)
serial.pack(il)
# ---------------------------------------------------------------------
path = [ ]
nested = [ ]
ids = None
names = [ "na1", "na2", "na3" ]
ia1 = remuco.ItemAction("ia1_l", multiple=True)
ia2 = remuco.ItemAction("ia2_l", multiple=False)
ias = [ ia1, ia2 ]
#la1 = remuco.ListAction("la1_l")
#las = [ la1 ]
las = None
il = data.ItemList(2, path, nested, ids, names, 0, 1, 2, ias, las)
#self.__serialize(il)
serial.pack(il)
def test_serialize_deserialize(self):
sc1 = _SerialzableClass()
sc1.init()
#bindata = self.__serialize(sc1)
bindata = serial.pack(sc1)
self.assertFalse(bindata is None)
sc2 = _SerialzableClass()
serial.unpack(sc2, bindata)
self.assertFalse(sc2.sa1 is None)
self.assertTrue(len(sc2.sa1) > 2)
self.assertEquals(sc2.sa1[2], "") # None becomes empty string
sc2.sa1[2] = None
self.assertEquals(sc2.ia2, []) # None becomes empty list
sc2.ia2 = None
self.assertEquals(sc1, sc2)
sc3 = serial.unpack(_SerialzableClass, "%strash" % bindata)
self.assertTrue(sc3 is None)
sc3 = serial.unpack(_SerialzableClass, "df")
self.assertTrue(sc3 is None)
sc3 = serial.unpack(_SerialzableClass(), "dfäsadfasd")
self.assertTrue(sc3 is None)
sc3 = serial.unpack(_SerialzableClass, "")
self.assertTrue(sc3 is None)
sc3 = serial.unpack(_SerialzableClass(), None)
self.assertTrue(sc3 is None)
if __name__ == '__main__':
unittest.main()
remuco-source-0.9.6/base/scripts/remuco-report 0000644 0000000 0000000 00000007650 11700415064 021411 0 ustar root root 0000000 0000000 #!/bin/sh
# =============================================================================
#
# Remuco - A remote control system for media players.
# Copyright (C) 2006-2010 by the Remuco team, see AUTHORS.
#
# This file is part of Remuco.
#
# Remuco 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 3 of the License, or
# (at your option) any later version.
#
# Remuco 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 Remuco. If not, see .
#
# =============================================================================
# -----------------------------------------------------------------------------
# functions
# -----------------------------------------------------------------------------
message() {
MPID=""
if [ "$UI" = "ZENI" ] ; then
if [ -z "$2" ] ; then
zenity --info --title="Remuco Report" --text="$1"
else
zenity --info --title="Remuco Report" --text="$1" &
MPID=$!
fi
elif [ "$UI" = "KDIA" ] ; then
if [ -z "$2" ] ; then
kdialog --title="Remuco Report" --msgbox="$1"
else
kdialog --title="Remuco Report" --msgbox="$1" &
MPID=$!
fi
else
echo $1
echo
fi
}
# -----------------------------------------------------------------------------
# constants
# -----------------------------------------------------------------------------
DEVICE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/remuco/devices"
if [ -n "`which zenity`" ] ; then
UI="ZENI"
elif [ -n "`which kdialog`" ] ; then
UI="KDIA"
else
UI="TERM"
fi
#UI="ZENI"
# -----------------------------------------------------------------------------
# here we go
# -----------------------------------------------------------------------------
DATA=`python -m remuco.report dump`
MSG_NO_DATA=\
"Until now no client devices have been logged on this computer. There is "\
"nothing to report.\n\nPlease try again later, once a Remuco client has "\
"connected to this computer. Thanks!"
if [ -z "$DATA" ] ; then
message "$MSG_NO_DATA"
exit
fi
MSG_INTRO=\
"This tool sends the names of seen Remuco client devices to "\
"remuco.sourceforge.net. It is a kind of survey to compile a list of Remuco "\
"compatible devices.\n\nTHE FOLLOWING INFORMATION WILL BE SUBMITTED:"
if [ "$UI" = "ZENI" ] ; then
TEXT="$MSG_INTRO\n\n$DATA\n\nPress OK to submit this report data now."
zenity --question --title="Remuco Report" --text="$TEXT"
[ $? = 0 ] || exit
elif [ "$UI" = "KDIA" ] ; then
TEXT="$MSG_INTRO\n\n$DATA\n\nDo you want to submit this data now?"
kdialog --title "Remuco Report" --yesno "$TEXT"
[ $? = 0 ] || exit
else
echo "===================================================================="
echo "$MSG_INTRO"
echo
echo "$DATA"
echo
echo "Do you want to submit this data now? [Y/n]"
read REPLY
[ -z "$REPLY" -o "$REPLY" = "Y" -o "$REPLY" = "y" -o "$REPLY" = "yes" ] || exit
fi
message "Submitting data ...\n\nPlease stand by." "sub"
# send data
python -m remuco.report send 2>&1
OK=$?
echo
# kill progress dialog
[ -z "$MPID" ] || kill $MPID > /dev/null 2>&1
MSG_SUCCESS=\
"Successfully submitted the device information.\n\nThank you for your "\
"contribution! Please run this tool again, once a new client device has "\
"connected."
MSG_FAILED=\
"Submitting the data failed. Please try again later.\n\nIf you run this tool "\
"in a terminal, you'll see some error output."
MSG_FAILED_T1=\
"Submitting the data failed. Please inspect the error output and/or try "\
"again later."
if [ $OK != 0 ] ; then
if [ -t 1 ] ; then
message "$MSG_FAILED_T1"
else
message "$MSG_FAILED"
fi
exit 1
else
message "$MSG_SUCCESS"
fi
remuco-source-0.9.6/client/android/AndroidManifest.xml 0000644 0000000 0000000 00000007241 11700415064 022745 0 ustar root root 0000000 0000000
remuco-source-0.9.6/client/android/build.properties 0000644 0000000 0000000 00000001506 11700415064 022367 0 ustar root root 0000000 0000000 # This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
# The name of your application package as defined in the manifest.
# Used by the 'uninstall' rule.
application.package=remuco.client.android
remuco-source-0.9.6/client/android/build.xml 0000644 0000000 0000000 00000032024 11700415064 020772 0 ustar root root 0000000 0000000
Creating output directories if needed...
Generating R.java / Manifest.java from the resources...
Compiling aidl files into Java classes...
Converting compiled files and external libraries into ${out-folder}/${dex-file}...
Packaging resources
All generated packages need to be signed with jarsigner before they are published.
Installing ${out-debug-package} onto default emulator...
Installing ${out-debug-package} onto default emulator...
Uninstalling ${application-package} from the default emulator...
Android Ant Build. Available targets:
help: Displays this help.
debug: Builds the application and sign it with a debug key.
release: Builds the application. The generated apk file must be
signed before it is published.
install: Installs the debug package onto a running emulator or
device. This can only be used if the application has
not yet been installed.
reinstall: Installs the debug package on a running emulator or
device that already has the application.
The signatures must match.
uninstall: uninstall the application from a running emulator or
device.
remuco-source-0.9.6/client/android/default.properties 0000644 0000000 0000000 00000000701 11700415064 022710 0 ustar root root 0000000 0000000 # This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Indicates whether an apk should be generated for each density.
split.density=false
# Project target.
target=android-7
remuco-source-0.9.6/client/android/local.properties.example 0000644 0000000 0000000 00000000645 11700415064 024017 0 ustar root root 0000000 0000000 # This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked in Version Control Systems,
# as it contains information specific to your local configuration.
# location of the SDK. This is only used by Ant
# For customization when using a Version Control System, please read the
# header note.
sdk.dir=${user.home}/opt/android-sdk
remuco-source-0.9.6/client/android/res/drawable/button_mute.png 0000644 0000000 0000000 00000004107 11700415064 024577 0 ustar root root 0000000 0000000 ‰PNG
IHDR szzô sRGB ®Îé bKGD • • •›Ï pHYs šœ tIMEÚ
!(i|ëf ÇIDATXÃí—il\WÇϽï¾eæÍî™ñxÇN×¶'Qi)I‰ÒQðÔT‰ÔˆM „Ôˆ$ŠªÒ¨ œ*DE©Æ¥ÒJ³]qb§‰e7±3ñ2cÏööå¾ËšµRiZ>AÿŸïÕù鞣óÿ_€Oôÿ.t7‡3™®««ãöŒþÁ§xм|ùûö‡ºÈ˜ Ù “ÉV¾õÌ‹¯>ÌÍ?$ËòÞíÛ·ÿ”ã8 àÁ×rYæ&W…Kí«mBˆ—J¥Ü¾¾> ØÇz~ÇkƒäÏž{Ú3V‘y|S²vè¹'WgÃ×§BÙrRVc†ëøy‘sž×¢˜ó®l
»âÿÞl__ýH ™L†¼zøÍOÏOü€ézOgSŠDã´R™í\Ò”ªNx‡ï¸,3ƒëy€Ø&pÓ
X:Ã\ßïg#¿=zïë÷êwÀC›7¯oÞ·oèeYÝ·¤Côëe9E(nKƼiäÍ)6Å>Q倫è†ãŠ€=Ÿ€=Š…™ÉØ|4GÎv©—|wóëýýýÎ5ÈòoùA¸)HðQrBný¢f`
QîÆð®Hµ,ì)…êþèyÞ8 X!.Y¼ÑÑPßTYÈŠåe¹à,<:ø¬pâÄÿ´fÍ ñ>€L&ƒ;;;I.—ã (¥xgæ'›º½©¹¹'£!Ÿ])㺸’Qzf’Z
š ©*¶Ÿ™ÉqÜUŽãf!^>Ö4R¨IçÒìôWı+]®1³zÎÇü
LJ‹Ùlö[3A ²Ù,·uëÖí±X,Âó¼Ì#ÇÁ\ź¿%òû#)Ùa
¿.¢¡%Áüy$*Ex²CkÀOœ·u¥ã+>?)tÌ3Æ*Ñh´àº®iYÖñÉ…+¡‘lÉŸ<¸ê>ŸÔôÆmƒµaæ( P àØ±cªªî@õBº(¥÷8ŽsO<š”êjÃ~^’|KÁ+"¼ÇË:ŒY$“¬#.?A˜e&¤¨OÐøØh©T*l۶;xñ¢A)(‘¤[Wï•Sš+™jÇÞßöî½l ` €––cŒãñ8¤Óé@ccc¨±±1–ƒOB>Ã*á:jC ŒgáŠã”˳ñh«1ßÜš_䩘XÃWVHo¹,ˉݻw“þþ~×0Œ9ÆØ˜°´}|z¾è¡R°î#½Ùlß ”"Æ œmÛÄq‚1&žçaÍÑvTHK¢9Cƒ<áÀŠÄÀÕ–¦Ú—ÛZnŠ<ñã©ös‡žTsë-˪ ¢(ºãr±¥vÂv©à÷‡c%wõÌ̹=étÚ<þ<.—Ë>MÓ0Çq Š"9¨‚ßaàd aºR›ó”Ä òlMs]þüIJ%Å„é>Rsv¼^~gxIKÈí·ï_å*
ŠD"%UU!àB¢„xˈ{‡\×Å·¦§§J©\(‚Œ±ïì™ : 7ÒðR1 º•©y€¤AWÓc×§KN9k¹r×¼½ÕÕÇ/÷$’ö7Íæ‡EQÎRJ£DwãÔ4¸2ö˜‘„ïv
…‚ øVq„ „€ ðc€°ÂQH 2Uà)‡0Æ
Bh´Xlþ˵že'•¶:x%1rùLOgåø6 XKiŠŒM×b×ôtdÏ-jEC©Tʽ
` „ª„S‡â
‚à8̉æã%˜×M(X:,N5@—$¦˜” J©§ªªfYÖh.ÔñжvÙ©¦ú˜µœÉØð?–×;W·ˆ¢öÀÔÛãõ
‘ n‰n¥ôåä…wMê_-ظqãÌþýû¿—„B!^×qÿȨ±e‚/Ö'eÛZyr.¯Ãº.?<Ú*C,gB"8ÇaŒ…K—ʦ¦Jº®Ÿš;×íð“ÇÂ+Z/è"6ÿ¾²³ž,8³¤È4Ý¿,xQUƒê-‡ä †††¼={öŒwttœîíí=ÙÓÓsª»»ûøä¥‹¹ù¹™‡\Ç%‰h
¢ª‚êÖ·%¡WÀM,UƦébQ£”Z Q
JÙßàÞß–æ'bq챪Ô¢…®ïKüz´ôÔ;ƒƒƒì?šÑÀÀýÏühà…\ÞüB<Æ}*·"ª¡Ç‡`qX`ÃSi‹µ·Ü¨6&/NÚþ
71vÙW앆goWßhˆë^¯jGBÞXí—bÏNÐïì»Ó>àÚµOIÙg„öŸeG_*•+Ýuu§=Xå»i>·¸†u•P‚AOM>Ÿ·€™ŒRLÛ*¦œ&|ñ§'Iè´»60p¨åÛïsCôaÂÈïv¼òØÙüÕªVjõ‡yÚ‚Ô˜;ˆ¨Ú”"Z7+’h{س(BbU3òf1¾T³,þ›7Ÿ:þÞâw—ˆvìüìMÍú¹^ª6ƒeAc}íÉçŸì—ÉSc±ðÐD2ô0ïØrˆ‚çˆØöêÅI'á?ziÃgNÛv¯þ‘Ñ¿:8¹&wéðת…+BµÑü¯>wk¡¼W–eÑw3!ý¯Eèl6ËíÚµKì\XÓýÒ‘hÓ'ŸŠOô?¡zŸÑLÏ,x IEND®B`‚ remuco-source-0.9.6/client/android/res/drawable/button_next.png 0000644 0000000 0000000 00000003107 11700415064 024602 0 ustar root root 0000000 0000000 ‰PNG
IHDR szzô sRGB ®Îé bKGD • • •›Ï pHYs šœ tIMEÚ
"3È4qI ÇIDATXÃíVOh×þ½ófvv&;»³@¬T츖ü·2 ¦`ÒÖŸ‹‰”S9„-6:œf/ -´ÂXFcrkǧêkÛ`c…
S°ƒÅJ»«•´ÖììÎÎÌ›÷r°äÊÆ–ÝCOñsxð½ßûÞ÷{oÞð¯ñcú/P¯×1 ` õz]îH.‹Y]×O•J¥IÏó´©©)ò,gjjŠpΧMÓüe½^§J©nÂó<²wïÞ·Ç™uçìüü<ƒÈ?GEº®ß™;yòäþí“<Ï#GŽ9BiQJ¥Ré³ãÇÿ¤^¯ÓçÕ¼|ù2¯V«Ÿ€âœ/M{ž÷Ô¦žLlµZ´T*íi6›]Bˆ†á{>œŸýæÆq«Õ¢Œ±·MÓĆaÐ0?¸}ûö‰{÷îÍÍÌÌ\›››‹¶Bà|>kkk}Ã0°išo¶Z-
éo'g³Y[)•=xð ;|ø0KÓtriié—.]š
‚à`ÇÖèè(aŒñ={öd:„MÓív»Ÿ\½zõóÉÉÉò³mBd”R„=†]©Tðs À+Œ1 €–Édøþýû‡Ýn7i4ïÞºuë‘‘‘ÏÓ4}˲,42–ee&&&úNg°¸¸ø«………µÛíßž9sæ/N'4MI) ¥Tš¦!ôn·‹^(`KDE DJ™Éçóœs7Ñëׯª”b½^/SVc !²®ë]×{ëëëƒv»ýñµk×NîÚµë ÜÍf³BHmlldÊåòê³ë=O€”òñmA¡$I¨mÛd||\&I¢ß¹s‡0Ʀi „€ápˆ
ÃÈØ¶ÍcQ6›í///ÿâîÝ»‡¯\¹ò-Bè§!B)…쥄$Žc¤i0Æ C‡È0¢i¸®¾ïCÀ„DQ„0Æ”RJ
…gŒÅý~Ÿ>xðàÃ(Š4˲c,
ÃÐz%B0 ¡Çí¢”BǦ)ÄqqC`Û6 ÄqJ)`Œ¡0©”’¸®Ë9çÐn·±ïûÀ3*•J壟 0šÍ&’RÆ0Æ€‚$I€1½^’$N§Ýn”R ¦é±”Rh4c¬¶ëº.ƒ xãUZ€šÍæSW!J©'Ž(¥`8ÂÖYÙÎùï)¥Ô“:ý~ŸsÎ×
Øl˜¦ ”Rèõz ë:! Š"@B4Mƒƒ˜¦) „€1¦ƒH)º®{Ó0ŒÜâââÏ @m~O¯µ} išÀ+˲”¦i@)…R©¤8çiš¦RÓ´Ä4Í„1¦r¹pÎsŽã(Û¶…B¤i
¦i~ï8ÎïjµÚûÕjõ[)%Ú´EîèÀp8d𦠄Ñ4MqÎUéÚÚÓu}u÷îÝ_W*•ìÍ›7Ýï÷i>Ÿ„ò}_ú¾„^¹\þ[¹\þóÙ³g¿ Öétr›–‡ÙÀ+!±m;¥”&ÍfSnll¨\.÷ïB¡P?wîÜGccc×cq&“‘RJ±²²’ú¾/u]P*•>>}úôo.^¼øZ–ôû}E)B’"1ÆÂqõB„‰”R-//)%pÎ×\×ýúÀs§NúOøþýû2Š"†¡J’„PJƒb±øU¡PørffæïµZ-Ù^SJ)•RX¡Ò4]oµZò¹|ßO=zÔM’„"„R˲¾+‹:þü7ŽãDÓÓÓ©çydeeeióŸ@ǹ•Ë徸páÂ_·8O§Töz½ IX]]]¯T*â…ÉåèÑ£»LÓü2—Ë;vìXñÙ· `||¼ÉdþàºîïOœ81±SÈð<‹Åw5Mû§®ëóûöíËïÉ^5>ý/1«^¯ãJ¥BZVúÒHö¯ñ£Ä÷P½yˆ¬2@ IEND®B`‚ remuco-source-0.9.6/client/android/res/drawable/button_norepeat.png 0000644 0000000 0000000 00000003727 11700415064 025451 0 ustar root root 0000000 0000000 ‰PNG
IHDR szzô sRGB ®Îé bKGD • • •›Ï pHYs šœ tIMEÚ
#”ð} WIDATXÃíW[lÙ>çÌ™ûØž±Ç†Ä‰¨¸ˆUP›Ô*R¡å%¦-" >T¨Z±mµ0<„¥J#Pí˶[µš< U©„ª6R©€.ÚJ Jv£rQˆ8Ž“ñuΜ3Ó‡’*I¹…UÕ—ýžgÎ÷ÍÿýÿwþàüŸ×ò°išhË–-8“Ép 022ÒÔÝÝ=366Æþ§LÓD¾ïK–eíµm»oqqñ+ŽãÄ|ßw4MûÙèèè/S©[-òÁƒÜ•+W¢ÅbQ „ø €ÉÕgãW‘§Óiþܹs‡!Êåò×dY†Ñh”ø¾ï-,,‘H¤9—Ëa €×ßß/]¾|ù{º®¯+•J{\ו«ÕêJ©ÏqœÉ[›€¾¾>åôéÓ?¦”~KE½««Ëå8N‘eY¬T*¤ZÖDQl €eYèøñãoW*•w<ϳ5MC•JEe™lܸñ½cÇŽýöäÉ“àµtvvFÆÇLJ)¥ßˆF£¤££zž'.,,8óóó°R©xŽãÔ!„ÅX,Fs¹‡Ã!ÄÛ¼y³
…¤ÙÙYÉd Çq1Žãx øËyÐóÈ{{{Õééé÷(¥û‰DyÛ¶m¬\.;÷îÝós¹\Îuݺ®¿‡šJ¥~•J¥¥iš†$IòC¡\¯×Åææf¬(
œ˜˜8:44t,NãWV NógΜù>clÿúõë›6mÂOžBˆƒA?™L"Œ±“Ífßþ̲¬?-g… ˲¸óçÏï¡”~·¹¹™Û±cšœœ=eYþõÐÐл†a8«Hÿ}Æc¬XVùB¡P à|>Ï' ÔÖÖ¦Ú¶í‹Åccc ØKV¬° —ËáB¡pBØÖÑÑ!
)›ÍІaüñÂ…ïÖŸG ±XŒ>óûáÖ[̓þˆ2ýðáCO–e1™LzFã7n¤,ËBÏí[·n)•Je7ÆXD‰ÓÓÓ@„'mmmï†á¬n åH¥Rcü!Æø‡ø`ûöí¿×uý#JimnnÎSUUEB»žíJ–eqwïÞÝ ‡B!ßu]T¯×A0¼;00ðI*•ò^•™ZöçÁÁÁšªªåöövB˜™ŸŸ÷c‚®ëÔuÝök×®…–B-/?!äË<χB¡`Û6‡òâñøgcïe_ÿ¢Šôõõ=ÿp]×o4(@×u7
…-K6üG ¥‰¢¨B4ƘhÛ6ò<&“Éì›æ¼ã8LQ”¼ã8!yž'3ÆZ4MK,Ù°zQVfggQ¥R²,UUÏsÛ ‚ÐX:Ó÷}Áó