First cut of Media Player D-Bus API support

This makes gPodder depend on mygpoclient >= 1.4.

For players like Panucci, this will save the
current playtime and the total time of the
podcast episode in the local database and also
send exact played events to the web server.
This commit is contained in:
Thomas Perl 2010-04-26 11:38:55 +02:00
parent 5b268229eb
commit 20b9204613
5 changed files with 137 additions and 5 deletions

2
README
View File

@ -32,7 +32,7 @@
* python (>= 2.5) with sqlite3 support
* python-gtk2 (>= 2.12)
* python-feedparser
* python-mygpoclient (>= 1.2; http://thpinfo.com/2010/mygpoclient/)
* python-mygpoclient (>= 1.4; http://thpinfo.com/2010/mygpoclient/)
* python-dbus (optional, but highly recommended)
If your Python installation does not come with "sqlite3" support,

View File

@ -320,6 +320,9 @@ class EpisodeListModel(gtk.ListStore):
if show_padlock:
tooltip.append(_('deletion prevented'))
if episode.total_time > 0:
tooltip.append('%d%%' % (100.*float(episode.current_position)/float(episode.total_time)))
tooltip = ', '.join(tooltip)
if status_icon is not None:

View File

@ -71,6 +71,7 @@ from gpodder import opml
from gpodder import download
from gpodder import my
from gpodder import youtube
from gpodder import player
from gpodder.liblogger import log
_ = gpodder.gettext
@ -230,6 +231,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.config.connect_gtk_paned('paned_position', self.channelPaned)
self.main_window.show()
self.player_receiver = player.MediaPlayerDBusReceiver(self.on_played)
self.gPodder.connect('key-press-event', self.on_key_press)
self.preferences_dialog = None
@ -536,6 +539,40 @@ class gPodder(BuilderWidget, dbus.service.Object):
if not self.channels and not gpodder.ui.fremantle:
util.idle_add(self.on_itemUpdate_activate)
def on_played(self, start, end, total, file_uri):
"""Handle the "played" signal from a media player"""
log('Received play action: %s (%d, %d, %d)', file_uri, start, end, total, sender=self)
filename = file_uri[len('file://'):]
# FIXME: Optimize this by querying the database more directly
for channel in self.channels:
for episode in channel.get_all_episodes():
fn = episode.local_filename(create=False, check_only=True)
if fn == filename:
file_type = episode.file_type()
# Automatically enable D-Bus played status mode
if file_type == 'audio':
self.config.audio_played_dbus = True
elif file_type == 'video':
self.config.video_played_dbus = True
now = time.time()
if total > 0:
episode.total_time = total
if episode.current_position_updated is None or \
now > episode.current_position_updated:
episode.current_position = end
episode.current_position_updated = now
episode.mark(is_played=True)
episode.save()
self.db.commit()
self.update_episode_list_icons([episode.url])
self.update_podcast_list_model([episode.channel.url])
# Submit this action to the webservice
self.mygpo_client.on_playback_full(episode, \
start, end, total)
return
def on_add_remove_podcasts_mygpo(self):
actions = self.mygpo_client.get_received_actions()
if not actions:
@ -1266,6 +1303,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
def _on_config_changed(self, name, old_value, new_value):
if name == 'show_toolbar' and gpodder.ui.desktop:
self.toolbar.set_property('visible', new_value)
elif name == 'videoplayer':
self.config.video_played_dbus = False
elif name == 'player':
self.config.audio_played_dbus = False
elif name == 'episode_list_descriptions':
self.update_episode_list_model()
elif name == 'episode_list_thumbnails':

View File

@ -30,6 +30,7 @@ _ = gpodder.gettext
import atexit
import datetime
import os
import sys
import threading
import time
@ -42,6 +43,19 @@ from gpodder import minidb
import mygpoclient
mygpoclient.user_agent += ' ' + gpodder.user_agent
MYGPOCLIENT_REQUIRED = '1.4'
if not hasattr(mygpoclient, 'require_version') or \
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
print >>sys.stderr, """
Please upgrade your mygpoclient library.
See http://thpinfo.com/2010/mygpoclient/
Required version: %s
Installed version: %s
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__)
sys.exit(1)
from mygpoclient import api
from mygpoclient import util as mygpoutil
@ -112,13 +126,15 @@ class EpisodeAction(object):
'started': int, 'position': int, 'total': int}
def __init__(self, podcast_url, episode_url, device_id, \
action, timestamp, position):
action, timestamp, started, position, total):
self.podcast_url = podcast_url
self.episode_url = episode_url
self.device_id = device_id
self.action = action
self.timestamp = timestamp
self.started = started
self.position = position
self.total = total
# New entity name for "received" actions
class ReceivedEpisodeAction(EpisodeAction): pass
@ -235,10 +251,15 @@ class MygPoClient(object):
else:
raise Exception('Webservice access not enabled')
def _convert_played_episode(self, episode, start, end, total):
return EpisodeAction(episode.channel.url, \
episode.url, self.device_id, 'play', \
int(time.time()), start, end, total)
def _convert_episode(self, episode, action):
return EpisodeAction(episode.channel.url, \
episode.url, self.device_id, action, \
int(time.time()), None)
int(time.time()), None, None, None)
def on_delete(self, episodes):
log('Storing %d episode delete actions', len(episodes), sender=self)
@ -248,6 +269,10 @@ class MygPoClient(object):
log('Storing %d episode download actions', len(episodes), sender=self)
self._store.save(self._convert_episode(e, 'download') for e in episodes)
def on_playback_full(self, episode, start, end, total):
log('Storing full episode playback action', sender=self)
self._store.save(self._convert_played_episode(episode, start, end, total))
def on_playback(self, episodes):
log('Storing %d episode playback actions', len(episodes), sender=self)
self._store.save(self._convert_episode(e, 'play') for e in episodes)
@ -372,14 +397,15 @@ class MygPoClient(object):
return api.EpisodeAction(action.podcast_url, \
action.episode_url, action.action, \
action.device_id, since, \
action.position)
action.started, action.position, action.total)
def convert_from_api(action):
dt = mygpoutil.iso8601_to_datetime(action.timestamp)
since = int(dt.strftime('%s'))
return ReceivedEpisodeAction(action.podcast, \
action.episode, action.device, \
action.action, since, action.position)
action.action, since, \
action.started, action.position, action.total)
try:
save_since = True

62
src/gpodder/player.py Normal file
View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
#
# gPodder 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.
#
# gPodder is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#
# gpodder.player - Podcatcher implementation of the Media Player D-Bus API
# Thomas Perl <thp@gpodder.org>; 2010-04-25
#
# Specification: http://gpodder.org/wiki/Media_Player_D-Bus_API
#
try:
import dbus
except ImportError:
# Import Mock D-Bus interfaces when D-Bus bindings are not installed
from gpodder.gui import dbus
class MediaPlayerDBusReceiver(object):
INTERFACE = 'org.gpodder.player'
SIGNAL_STARTED = 'PlaybackStarted'
SIGNAL_STOPPED = 'PlaybackStopped'
def __init__(self, on_play_event):
self.on_play_event = on_play_event
self.bus = dbus.SessionBus()
self.bus.add_signal_receiver(self.on_playback_started, \
self.SIGNAL_STARTED, \
self.INTERFACE, \
None, \
None)
self.bus.add_signal_receiver(self.on_playback_stopped, \
self.SIGNAL_STOPPED, \
self.INTERFACE, \
None, \
None)
def on_playback_started(self, position, file_uri):
pass
def on_playback_stopped(self, start, end, total, file_uri):
if file_uri.startswith('/'):
file_uri = 'file://' + file_uri
self.on_play_event(start, end, total, file_uri)