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/>.
23 # my.py -- mygpo Client Abstraction for gPodder
24 # Thomas Perl <thp@gpodder.org>; 2010-01-19
35 from gpodder
.liblogger
import log
37 from gpodder
import util
39 # Append gPodder's user agent to mygpoclient's user agent
41 mygpoclient
.user_agent
+= ' ' + gpodder
.user_agent
43 from mygpoclient
import api
46 import simplejson
as json
52 ADD
, REMOVE
= range(2)
54 def __init__(self
, url
, change
, podcast
=None):
57 self
.podcast
= podcast
60 def description(self
):
61 if self
.change
== self
.ADD
:
62 return _('Add %s') % self
.url
64 return _('Remove %s') % self
.podcast
.title
67 class Actions(object):
72 UPDATE_DEVICE
= (1<<x
for x
in range(3))
74 class MygPoClient(object):
75 CACHE_FILE
= 'mygpo.queue.json'
79 def __init__(self
, config
,
80 on_rewrite_url
=lambda old_url
, new_url
: None,
81 on_add_remove_podcasts
=lambda add_urls
, remove_urls
: None,
82 on_send_full_subscriptions
=lambda: None):
83 self
._cache
= {'actions': Actions
.NONE
,
85 'remove_podcasts': [],
91 # Callback for actions that need to be handled by the UI frontend
92 self
._on
_rewrite
_url
= on_rewrite_url
93 self
._on
_add
_remove
_podcasts
= on_add_remove_podcasts
94 self
._on
_send
_full
_subscriptions
= on_send_full_subscriptions
96 # Initialize the _client attribute and register with config
97 self
.on_config_changed('mygpo_username')
98 assert self
._client
is not None
99 self
._config
.add_observer(self
.on_config_changed
)
101 # Initialize and load the local queue
102 self
._cache
_file
= os
.path
.join(gpodder
.home
, self
.CACHE_FILE
)
104 self
._cache
= json
.loads(open(self
._cache
_file
).read())
106 log('Cannot read cache file: %s', str(e
), sender
=self
)
108 self
._worker
_thread
= None
109 atexit
.register(self
._at
_exit
)
111 # Do the initial flush (in case any actions are queued)
114 def can_access_webservice(self
):
115 return self
._config
.mygpo_enabled
and self
._config
.mygpo_device_uid
117 def schedule_podcast_sync(self
):
118 log('Scheduling podcast list sync', sender
=self
)
119 self
.schedule(Actions
.SYNC_PODCASTS
)
121 def request_podcast_lists_in_cache(self
):
122 if 'add_podcasts' not in self
._cache
:
123 self
._cache
['add_podcasts'] = []
124 if 'remove_podcasts' not in self
._cache
:
125 self
._cache
['remove_podcasts'] = []
127 def force_fresh_upload(self
):
128 self
._on
_send
_full
_subscriptions
()
130 def set_subscriptions(self
, urls
):
131 log('Uploading (overwriting) subscriptions...')
132 self
._client
.put_subscriptions(self
._config
.mygpo_device_uid
, urls
)
133 log('Subscription upload done.')
135 def on_subscribe(self
, urls
):
136 self
.request_podcast_lists_in_cache()
137 self
._cache
['add_podcasts'].extend(urls
)
139 if url
in self
._cache
['remove_podcasts']:
140 self
._cache
['remove_podcasts'].remove(url
)
141 self
.schedule(Actions
.SYNC_PODCASTS
)
144 def on_unsubscribe(self
, urls
):
145 self
.request_podcast_lists_in_cache()
146 self
._cache
['remove_podcasts'].extend(urls
)
148 if url
in self
._cache
['add_podcasts']:
149 self
._cache
['add_podcasts'].remove(url
)
150 self
.schedule(Actions
.SYNC_PODCASTS
)
155 return self
._cache
.get('actions', Actions
.NONE
)
158 self
._worker
_proc
(forced
=True)
160 def _worker_proc(self
, forced
=False):
162 log('Worker thread waiting for timeout', sender
=self
)
163 time
.sleep(self
.FLUSH_TIMEOUT
)
165 # Only work when enabled, UID set and allowed to work
166 if self
.can_access_webservice() and \
167 (self
._worker
_thread
is not None or forced
):
168 self
._worker
_thread
= None
169 log('Worker thread starting to work...', sender
=self
)
170 for retry
in range(self
.FLUSH_RETRIES
):
172 log('Retrying flush queue...', sender
=self
)
174 # Update the device first, so it can be created if new
175 if self
.actions
& Actions
.UPDATE_DEVICE
:
178 if self
.actions
& Actions
.SYNC_PODCASTS
:
179 self
.synchronize_subscriptions()
181 if self
.actions
& Actions
.UPLOAD_EPISODES
:
182 # TODO: Upload episode actions
186 # No more pending actions. Ready to quit.
189 log('Flush completed (result: %d)', self
.actions
, sender
=self
)
190 self
._dump
_cache
_to
_file
()
192 def _dump_cache_to_file(self
):
194 fp
= open(self
._cache
_file
, 'w')
195 fp
.write(json
.dumps(self
._cache
))
197 # FIXME: Atomic file write would be nice ;)
199 log('Cannot dump cache to file: %s', str(e
), sender
=self
)
205 if self
._worker
_thread
is None:
206 self
._worker
_thread
= threading
.Thread(target
=self
._worker
_proc
)
207 self
._worker
_thread
.setDaemon(True)
208 self
._worker
_thread
.start()
210 log('Flush already queued', sender
=self
)
212 def schedule(self
, action
):
213 if 'actions' not in self
._cache
:
214 self
._cache
['actions'] = 0
216 self
._cache
['actions'] |
= action
219 def done(self
, action
):
220 if 'actions' not in self
._cache
:
221 self
._cache
['actions'] = 0
223 if action
== Actions
.SYNC_PODCASTS
:
224 self
._cache
['add_podcasts'] = []
225 self
._cache
['remove_podcasts'] = []
227 self
._cache
['actions'] &= ~action
229 def on_config_changed(self
, name
=None, old_value
=None, new_value
=None):
230 if name
in ('mygpo_username', 'mygpo_password', 'mygpo_server'):
231 self
._client
= api
.MygPodderClient(self
._config
.mygpo_username
,
232 self
._config
.mygpo_password
, self
._config
.mygpo_server
)
233 log('Reloading settings.', sender
=self
)
234 elif name
.startswith('mygpo_device_'):
235 self
.schedule(Actions
.UPDATE_DEVICE
)
236 if name
== 'mygpo_device_uid':
237 # Reset everything because we have a new device ID
238 threading
.Thread(target
=self
.force_fresh_upload
).start()
239 self
._cache
['podcasts_since'] = 0
241 def synchronize_subscriptions(self
):
243 device_id
= self
._config
.mygpo_device_uid
244 since
= self
._cache
.get('podcasts_since', 0)
246 # Step 1: Pull updates from the server and notify the frontend
247 result
= self
._client
.pull_subscriptions(device_id
, since
)
248 self
._cache
['podcasts_since'] = result
.since
249 if result
.add
or result
.remove
:
250 log('Changes from server: add %d, remove %d', \
252 len(result
.remove
), \
254 self
._on
_add
_remove
_podcasts
(result
.add
, result
.remove
)
256 # Step 2: Push updates to the server and rewrite URLs (if any)
257 add
= list(set(self
._cache
.get('add_podcasts', [])))
258 remove
= list(set(self
._cache
.get('remove_podcasts', [])))
260 # Only do a push request if something has changed
261 result
= self
._client
.update_subscriptions(device_id
, add
, remove
)
262 self
._cache
['podcasts_since'] = result
.since
264 for old_url
, new_url
in result
.update_urls
:
266 log('URL %s rewritten: %s', old_url
, new_url
, sender
=self
)
267 self
._on
_rewrite
_url
(old_url
, new_url
)
269 self
.done(Actions
.SYNC_PODCASTS
)
272 log('Cannot upload subscriptions: %s', str(e
), sender
=self
, traceback
=True)
275 def update_device(self
):
277 log('Uploading device settings...', sender
=self
)
278 uid
= self
._config
.mygpo_device_uid
279 caption
= self
._config
.mygpo_device_caption
280 device_type
= self
._config
.mygpo_device_type
281 self
._client
.update_device_settings(uid
, caption
, device_type
)
282 log('Device settings uploaded.', sender
=self
)
283 self
.done(Actions
.UPDATE_DEVICE
)
286 log('Cannot update device %s: %s', uid
, str(e
), sender
=self
, traceback
=True)
289 def get_devices(self
):
291 for d
in self
._client
.get_devices():
292 result
.append((d
.device_id
, d
.caption
, d
.type))
295 def open_website(self
):
296 util
.open_website('http://' + self
._config
.mygpo_server
)