Fix a MAFW filename encoding issue on Maemo 5
[gpodder.git] / src / gpodder / gtkui / frmntl / mafw.py
blob5fca75d86fc26ce66d3cae72cccf89f3fe62512a
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # gPodder - A media aggregator and podcast client
5 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
7 # gPodder is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # gPodder is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
22 # Maemo 5 Media Player / MAFW Playback Monitor
23 # Send playback status information to gPodder using D-Bus
24 # Thomas Perl <thp@gpodder.org>; 2010-08-16 / 2010-08-17
27 # The code below is based on experimentation with MAFW and real files,
28 # so it might not work in the general case. It worked fine for me with
29 # local and streaming files (audio/video), though. Blame missing docs!
31 import gtk
32 import gobject
33 import dbus
34 import dbus.mainloop
35 import dbus.service
36 import dbus.glib
37 import urllib
38 import time
40 import gpodder
42 class gPodderPlayer(dbus.service.Object):
43 # Empty class with method definitions to send D-Bus signals
45 def __init__(self, path, name):
46 dbus.service.Object.__init__(self, object_path=path, bus_name=name)
48 # Signals for gPodder's media player integration
49 @dbus.service.signal(dbus_interface='org.gpodder.player', signature='us')
50 def PlaybackStarted(self, position, file_uri):
51 pass
53 @dbus.service.signal(dbus_interface='org.gpodder.player', signature='uuus')
54 def PlaybackStopped(self, start_position, end_position, total_time, \
55 file_uri):
56 pass
58 class MafwResumeHandler(object):
59 # Simple state machine for handling resume with MAFW
61 # NoResume ... No desire to resume / error state ("do nothing")
62 # Init ....... Want to resume, filename and position set
63 # Loaded ..... The correct filename has been loaded
64 # Seekable ... The media is seekable (metadata "is-seekable" is True)
65 # Playing .... The media is being played back (state_changed with 1)
66 # Done ....... The "resume" action has been carried out
67 NoResume, Init, Loaded, Seekable, Playing, Done = range(6)
69 def __init__(self, playback_monitor):
70 self.playback_monitor = playback_monitor
71 self.state = MafwResumeHandler.NoResume
72 self.filename = None
73 self.position = None
75 def set_resume_point(self, filename, position):
76 self.filename = filename
77 self.position = position
78 if self.position:
79 self.state = MafwResumeHandler.Init
80 else:
81 self.state = MafwResumeHandler.NoResume
83 def on_media_changed(self, filename):
84 if self.state == MafwResumeHandler.Init:
85 if filename.startswith('file://'):
86 filename = urllib.unquote(filename[len('file://'):])
87 if self.filename == filename:
88 self.state = MafwResumeHandler.Loaded
90 def on_metadata_changed(self, key, value):
91 if self.state == MafwResumeHandler.Loaded:
92 if key == 'is-seekable' and value == True:
93 self.state = MafwResumeHandler.Seekable
95 def on_state_changed(self, new_state):
96 if self.state == MafwResumeHandler.Seekable:
97 if new_state == 1:
98 self.state = MafwResumeHandler.Playing
99 self.playback_monitor.set_position(self.position)
100 self.state = MafwResumeHandler.Done
103 class MafwPlaybackMonitor(object):
104 MAFW_RENDERER_OBJECT = 'com.nokia.mafw.renderer.Mafw-Gst-Renderer-Plugin.gstrenderer'
105 MAFW_RENDERER_PATH = '/com/nokia/mafw/renderer/gstrenderer'
106 MAFW_RENDERER_INTERFACE = 'com.nokia.mafw.renderer'
107 MAFW_RENDERER_SIGNAL_MEDIA = 'media_changed'
108 MAFW_RENDERER_SIGNAL_STATE = 'state_changed'
109 MAFW_RENDERER_SIGNAL_METADATA = 'metadata_changed'
111 MAFW_SENDER_PATH = '/org/gpodder/maemo/mafw'
113 class MafwPlayState(object):
114 Stopped = 0
115 Playing = 1
116 Paused = 2
117 Transitioning = 3
119 def __init__(self, bus):
120 self.bus = bus
121 self._filename = None
122 self._is_playing = False
123 self._start_time = time.time()
124 self._start_position = 0
125 self._duration = 0
127 self._resume_handler = MafwResumeHandler(self)
129 self._player = gPodderPlayer(self.MAFW_SENDER_PATH, \
130 dbus.service.BusName(gpodder.dbus_bus_name, self.bus))
132 state, object_id = self.get_status()
134 self.on_media_changed(0, object_id)
135 self.on_state_changed(state)
137 self.bus.add_signal_receiver(self.on_media_changed, \
138 self.MAFW_RENDERER_SIGNAL_MEDIA, \
139 self.MAFW_RENDERER_INTERFACE, \
140 None, \
141 self.MAFW_RENDERER_PATH)
143 self.bus.add_signal_receiver(self.on_state_changed, \
144 self.MAFW_RENDERER_SIGNAL_STATE, \
145 self.MAFW_RENDERER_INTERFACE, \
146 None, \
147 self.MAFW_RENDERER_PATH)
149 self.bus.add_signal_receiver(self.on_metadata_changed, \
150 self.MAFW_RENDERER_SIGNAL_METADATA, \
151 self.MAFW_RENDERER_INTERFACE, \
152 None, \
153 self.MAFW_RENDERER_PATH)
155 # Capture requests to the renderer where the position is to be set to
156 # something else because we don't get normal signals in these cases
157 bus.add_match_string("type='method_call',destination='com.nokia.mafw.renderer.Mafw-Gst-Renderer-Plugin.gstrenderer',path='/com/nokia/mafw/renderer/gstrenderer',interface='com.nokia.mafw.renderer',member='set_position'")
158 bus.add_message_filter(self._message_filter)
160 def set_resume_point(self, filename, position):
161 self._resume_handler.set_resume_point(filename, position)
163 def _message_filter(self, bus, message):
164 try:
165 # message type 1 = dbus.lowlevel.MESSAGE_TYPE_METHOD_CALL
166 if message.get_path() == self.MAFW_RENDERER_PATH and \
167 message.get_interface() == self.MAFW_RENDERER_INTERFACE and \
168 message.get_destination() == self.MAFW_RENDERER_OBJECT and \
169 message.get_type() == 1 and \
170 message.get_member() == 'set_position' and \
171 self._is_playing:
172 # Fake stop-of-old / start-of-new
173 self.on_state_changed(-1)
174 self.on_state_changed(self.MafwPlayState.Playing)
175 finally:
176 return 1 # = dbus.lowlevel.HANDLER_RESULT_NOT_YET_HANDLED
178 def object_id_to_filename(self, object_id):
179 # Naive, but works for now...
180 if object_id.startswith('localtagfs::'):
181 if isinstance(object_id, unicode):
182 object_id = object_id.encode('utf-8')
183 return 'file://'+urllib.quote(urllib.unquote(object_id[object_id.index('%2F'):]))
184 elif object_id.startswith('urisource::'):
185 return object_id[len('urisource::'):]
186 else:
187 # This is pretty bad, actually (can happen with other
188 # sources, but should not happen for gPodder episodes)
189 return object_id
191 @property
192 def renderer(self):
193 o = self.bus.get_object(self.MAFW_RENDERER_OBJECT, \
194 self.MAFW_RENDERER_PATH)
195 return dbus.Interface(o, self.MAFW_RENDERER_INTERFACE)
197 def get_position(self):
198 return self.renderer.get_position()
200 def set_position(self, position):
201 self.renderer.set_position(0, position)
202 self._start_position = position
203 self._start_time = time.time()
204 return False
206 def get_status(self):
207 """Returns playing status and updates filename"""
208 playlist, index, state, object_id = self.renderer.get_status()
209 return (state, object_id)
211 def on_media_changed(self, position, object_id):
212 # We don't know the duration for newly-loaded files at first
213 self._duration = 0
215 if self._is_playing:
216 # Fake stop-of-old / start-of-new
217 self.on_state_changed(-1) # (see below where we catch the "-1")
218 self._filename = self.object_id_to_filename(object_id)
219 self.on_state_changed(self.MafwPlayState.Playing)
220 else:
221 self._filename = self.object_id_to_filename(object_id)
223 self._resume_handler.on_media_changed(self._filename)
225 def on_state_changed(self, state):
226 if state == self.MafwPlayState.Playing:
227 self._is_playing = True
228 try:
229 self._start_position = self.get_position()
230 except:
231 # XXX: WTF?
232 pass
233 self._start_time = time.time()
234 self._player.PlaybackStarted(self._start_position, self._filename)
235 else:
236 if self._is_playing:
237 try:
238 # Lame: if state is -1 (a faked "stop" event), don't try to
239 # get the "current" position, but use the wall time method
240 # from below to calculate the stop time
241 assert state != -1
243 position = self.get_position()
244 except:
245 # Happens if the assertion fails or if the position cannot
246 # be determined for whatever reason. Use wall time and
247 # assume that the media file has advanced the same amount.
248 position = self._start_position + (time.time()-self._start_time)
249 if self._start_position != position:
250 self._player.PlaybackStopped(self._start_position, \
251 position, self._duration, self._filename)
252 self._is_playing = False
254 self._resume_handler.on_state_changed(state)
256 def on_metadata_changed(self, key, count, value):
257 if key == 'duration':
258 # Remember the duration of the media - right now, we don't care
259 # if this is for the right file, as we re-set the internally-saved
260 # duration when the media is changed (see on_media_changed above)
261 self._duration = int(value)
262 elif key == 'is-seekable':
263 self._resume_handler.on_metadata_changed(key, value)