Fixed item navigation and cleaned up feed and item changes
[straw.git] / src / lib / feeds.py
blob0100690e2f065975a7635242bacbd962c3f9862d
1 import locale, operator
2 import gobject
3 import FeedDataRouter
4 import ItemStore
5 import Config
7 class FeedList(gobject.GObject):
9 __gsignals__ = {
10 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
11 'updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
12 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, gobject.TYPE_INT,)),
13 'imported' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
14 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
15 gobject.TYPE_BOOLEAN,)),
16 'deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
19 def __init__(self, init_seq = []):
20 gobject.GObject.__init__(self)
21 self.feedlist = []
22 self._loading = False
24 def __iter__(self):
25 return iter(self.feedlist)
27 def load_data(self):
28 def _load(feedlist, parent):
29 for df in feedlist:
30 if isinstance(df, list):
31 _load(df[1:], parent)
32 else:
33 f = Feed.create_empty_feed()
34 f.undump(df)
35 self.append(parent, f)
36 self._loading = True
37 feedlist = Config.get_instance().feeds
38 if feedlist:
39 _load(feedlist, None)
40 self._loading = False
41 self.emit('changed')
43 def connect_signals(self, ob):
44 ob.connect('changed', self.feed_detail_changed)
46 # these signals are forwarded so that listeners who just want to
47 # listen for a specific event regardless of what feed it came from can
48 # just connect to this feedlist instead of connecting to the
49 # individual feeds.
50 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
51 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
52 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
53 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
55 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
57 def __setitem__(self, key, value):
58 self.feedlist.__setitem__(key, value)
59 self.connect_signals(value)
60 self.save_feeds_and_notify(True)
62 def extend(self, parent, values, from_sub=False):
63 list.extend(self.feedlist, values)
64 for f in values:
65 f.parent = parent
66 self.connect_signals(f)
67 self.save_feeds()
68 self.emit('imported', values, parent, from_sub)
70 def append(self, parent, value):
71 self.feedlist.append(value)
72 value.parent = parent
73 self.connect_signals(value)
74 self.save_feeds()
75 self.emit('updated', value, parent, -1)
77 def insert(self, index, parent, value):
78 self.feedlist.insert(index, value)
79 value.parent = parent
80 self.connect_signals(value)
81 self.save_feeds()
82 self.emit('updated', value, parent, index)
84 def index(self, feed):
85 return self.feedlist.index(feed)
87 def reorder(self, move, delta):
88 k = self.feedlist[:]
89 move = list(move)
90 move.sort()
91 if delta > 0:
92 move.reverse()
93 if move[0] == 0 and delta < 0 or move[-1] == (len(self.feedlist) - 1) and delta > 0:
94 return
95 for m in move:
96 k[m + delta], k[m] = k[m], k[m + delta]
97 for i in range(len(k)):
98 list.__setitem__(self.feedlist, i, k[i])
99 self.save_feeds()
100 self.emit('changed')
102 def __delitem__(self, value):
103 feed = self.feedlist[value]
104 list.__delitem__(self.feedlist, value)
105 feed.delete_all_items()
106 self.save_feeds()
107 self.emit('deleted', feed)
109 def save_feeds(self):
110 if not self._loading:
111 config = Config.get_instance()
112 config.feeds = [f.dump() for f in self.feedlist]
113 return
115 def feed_detail_changed(self, feed):
116 self.save_feeds()
117 self.emit('changed') # XXXX send the feed as well?
119 def _sort_dsu(self, seq):
120 aux_list = [(x.title, x) for x in seq]
121 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
122 return [x[1] for x in aux_list]
124 def sort(self, indices = None):
125 if not indices or len(indices) == 1:
126 self[:] = self._sort_dsu(self)
127 else:
128 items = self._sort_dsu(indices)
129 for i,x in enumerate(items):
130 list.__setitem__(self, indices[i], items[i])
131 self.save_feeds()
132 self.emit('changed')
134 def __hash__(self):
135 h = 0
136 for item in self.feedlist:
137 h ^= hash(item)
138 return h
140 def get_feed_with_id(self, id):
141 for f in self.flatten_list():
142 if f.id == id:
143 return f
144 return None
146 def flatten_list(self, ob=None):
147 if ob is None:
148 ob = self.feedlist
149 l = []
150 for o in ob:
151 if isinstance(o, list):
152 l = l + self.flatten_list(o)
153 else:
154 l.append(o)
155 return l
157 feedlist_instance = None
159 def get_instance():
160 global feedlist_instance
161 if feedlist_instance is None:
162 feedlist_instance = FeedList()
163 return feedlist_instance
165 class Feed(gobject.GObject):
166 "A Feed object stores information set by user about a RSS feed."
168 DEFAULT = -1
169 STATUS_IDLE = 0
170 STATUS_POLLING = 1
172 __slots__ = ('_title', '_location', '_username', '_password', '_parsed',
173 '__save_fields', '_items', '_slots',
174 '_id', '_channel_description',
175 '_channel_title', '_channel_link', '_channel_copyright',
176 'channel_lbd', 'channel_editor', 'channel_webmaster',
177 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
178 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
180 __save_fields = (('_title', ""), ('_location', ""), ('_username', ""),
181 ('_password', ""), ('_id', ""),
182 ('_channel_description', ""), ('_channel_title', ""),
183 ('_channel_link', ""), ('_channel_copyright', ""),
184 ('channel_creator', ""), ('_error', None),
185 ('_items_stored', DEFAULT),
186 ('_poll_freq', DEFAULT),
187 ('_last_poll', 0),
188 ('_n_items_unread',0))
191 __gsignals__ = {
192 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
193 'poll-done' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
194 'items-added' :(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
195 (gobject.TYPE_PYOBJECT,)),
196 'items-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
197 (gobject.TYPE_PYOBJECT,)),
198 'items-deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
199 (gobject.TYPE_PYOBJECT,))
203 # use one of the factory functions below instead of this directly
204 def __init__(self, title="", location="", username="", password=""):
205 gobject.GObject.__init__(self)
206 self._title = title
207 self._channel_description = ""
208 self._channel_title = ""
209 self._channel_link = ""
210 self._channel_copyright = ""
211 self.channel_lbd = None
212 self.channel_editor = ""
213 self.channel_webmaster = ""
214 self.channel_creator = ""
215 self._location = location
216 self._username = username
217 self._password = password
218 self._parsed = None
219 self._error = None
220 self._n_items_unread = 0
221 self._process_status = self.STATUS_IDLE
222 self.router = FeedDataRouter.FeedDataRouter(self)
223 self._parent = None
224 self._items_stored = Feed.DEFAULT
225 self._poll_freq = Feed.DEFAULT
226 self._last_poll = 0
228 self.config = Config.get_instance()
229 self.config.connect('item-order-changed', self.item_order_changed_cb)
230 self.config.connect('item-stored-changed', self.item_stored_changed_cb)
231 self.item_order_reverse = self.config.item_order
232 self.item_stored_num = self.config.number_of_items_stored
233 self._items = FifoCache(num_entries=Feed.DEFAULT)
234 self._items_loaded = False
235 self._changes_queue = []
236 return
238 def __str__(self):
239 return "Feed '%s' from %s" % (self._title, self._location)
241 @property
242 def id(self):
243 return self._id
245 @apply
246 def parsed():
247 doc = "A ParsedSummary object generated from the summary file"
248 def fget(self):
249 return self._parsed
250 def fset(self, parsed):
251 self._parsed = parsed
252 return property(**locals())
254 @apply
255 def title():
256 doc = "The title of this Feed (as defined by user)"
257 def fget(self):
258 text = ''
259 if self._title:
260 text = self._title
261 return text
262 def fset(self, title):
263 if self._title != title:
264 self._title = title
265 self.emit('changed')
266 return property(**locals())
268 @apply
269 def access_info():
270 doc = "A tuple of location, username, password"
271 def fget(self):
272 return (self._location, self._username, self._password)
273 def fset(self, (location,username,password)):
274 self._location = location
275 self._username = username
276 self._password = password
277 self.emit('changed')
278 return property(**locals())
280 @apply
281 def location():
282 doc = ""
283 def fget(self):
284 return self._location
285 def fset(self, location):
286 if self._location != location:
287 self._location = location
288 self.emit('changed')
289 return property(**locals())
291 @apply
292 def channel_title():
293 doc = ""
294 def fget(self):
295 text = ''
296 if self._channel_title:
297 text = self._channel_title
298 return text
299 def fset(self, t):
300 changed = self._channel_title != t
301 self._channel_title = t
302 if changed:
303 self.emit('changed')
304 return property(**locals())
306 @apply
307 def channel_description():
308 doc = ""
309 def fget(self):
310 text = ''
311 if self._channel_description:
312 text = self._channel_description
313 return text
314 def fset(self, t):
315 changed = self._channel_description != t
316 self._channel_description = t
317 if changed:
318 self.emit('changed')
319 return property(**locals())
321 @apply
322 def channel_link():
323 doc = ""
324 def fget(self):
325 return self._channel_link
326 def fset(self, t):
327 changed = self._channel_link != t
328 self._channel_link = t
329 if changed:
330 self.emit('changed')
331 return property(**locals())
333 @apply
334 def channel_copyright():
335 doc = ""
336 def fget(self):
337 return self._channel_copyright
338 def fset(self, t):
339 changed = self._channel_copyright != t
340 self._channel_copyright = t
341 if changed:
342 self.emit('changed')
343 return property(**locals())
345 @apply
346 def number_of_items_stored():
347 doc = ""
348 def fget(self):
349 return self._items_stored
350 def fset(self, num=None):
351 if self._items_stored != num:
352 self._items_stored = num
353 return property(**locals())
355 @apply
356 def poll_frequency():
357 doc = ""
358 def fget(self):
359 return self._poll_freq
360 def fset(self, freq):
361 if self._poll_freq != freq:
362 self._poll_freq = freq
363 return property(**locals())
365 @apply
366 def last_poll():
367 doc = ""
368 def fget(self):
369 return self._last_poll
370 def fset(self, time):
371 if self._last_poll != time:
372 self._last_poll = time
373 return property(**locals())
375 @apply
376 def number_of_unread():
377 doc = "the number of unread items for this feed"
378 def fget(self):
379 return self._n_items_unread
380 def fset(self, n):
381 if self._n_items_unread:
382 self._n_items_unread = n
383 self.emit('changed')
384 return property(**locals())
386 @apply
387 def error():
388 doc = ""
389 def fget(self):
390 return self._error
391 def fset(self, error):
392 if self._error != error:
393 self._error = error
394 self.emit('changed')
395 return property(**locals())
397 @apply
398 def process_status():
399 doc = ""
400 def fget(self):
401 return self._process_status
402 def fset(self, status):
403 if status != self._process_status:
404 self._process_status = status
405 self.emit('changed')
406 return property(**locals())
408 @apply
409 def parent():
410 doc = ""
411 def fget(self):
412 return self._parent
413 def fset(self, parent):
414 self._parent = parent
415 return property(**locals())
417 @property
418 def next_refresh(self):
419 """ return the feed's next refresh (time)"""
420 nr = None
421 if self._poll_freq == self.DEFAULT:
422 increment = self.config.poll_frequency
423 else:
424 increment = self._poll_freq
425 if increment > 0:
426 nr = self.last_poll + increment
427 return nr
429 def dump(self):
430 fl = {}
431 for f, default in self.__save_fields:
432 fl[f] = self.__getattribute__(f)
433 return fl
435 def undump(self, dict):
436 for f, default in self.__save_fields:
437 self.__setattr__(f, dict.get(f, default))
438 return
440 def poll_done(self):
441 self.emit('poll-done')
443 def item_order_changed_cb(self, config):
444 self.item_order_reverse = config.item_order
446 def item_stored_changed_cb(self, config):
447 self.item_stored_num = config.number_of_items_stored
449 def get_cutpoint(self):
450 cutpoint = self.number_of_items_stored
451 if cutpoint == Feed.DEFAULT:
452 cutpoint = self.item_stored_num
453 return cutpoint
455 def add_items(self, items):
456 if not self._items_loaded: self.load_contents()
457 items = sorted(items, key=operator.attrgetter('pub_date'),
458 reverse=self.item_order_reverse)
459 self._items.set_number_of_entries(self.get_cutpoint())
460 maxid = 0
461 if self._items:
462 maxid = reduce(max, [item.id for item in self._items.itervalues()])
463 newitems = []
464 for item in items:
465 maxid += 1
466 item.id = maxid
467 item.feed = self
468 newitems.append(item)
469 item.connect('changed', self.item_changed_cb)
470 self._items[item.id] = item
471 self.number_of_unread = len([item for item in self._items.itervalues() if not item.seen])
472 self.emit('items-added', newitems)
474 def restore_items(self, items):
475 items = sorted(items, key=operator.attrgetter('pub_date'),
476 reverse=self.item_order_reverse)
477 cutpoint = self.get_cutpoint()
478 self._items.set_number_of_entries(cutpoint)
479 olditems = []
480 for idx, item in enumerate(items):
481 if not item.sticky and idx >= cutpoint:
482 item.clean_up()
483 olditems.append(item)
484 continue
485 item.feed = self
486 item.connect('changed', self.item_changed_cb)
487 self._items[item.id] = item
488 self.number_of_unread = len([item for item in self._items.itervalues() if not item.seen])
489 print "\t items len: %d, olditems len: %d" % (len(items), len(olditems))
490 if olditems:
491 self.emit('items-deleted', olditems)
492 return
494 def item_changed_cb(self, item):
495 self.number_of_unread += item.seen and -1 or 1
496 self._changes_queue.append(item)
497 self.emit('items-changed', self._changes_queue)
499 def delete_all_items(self):
500 self._items.clear()
501 self.emit('items-deleted', self._items.values())
503 @property
504 def items(self):
505 if not self._items_loaded:
506 self.load_contents()
507 return self._items.values()
509 def mark_items_as_read(self, items=None):
510 def mark(item): item.seen = True
511 unread = [item for item in self._items.itervalues() if not item.seen]
512 map(mark, unread)
513 self.emit('items-changed', unread)
515 def load_contents(self):
516 if self._items_loaded:
517 return False
518 itemstore = ItemStore.get_instance()
519 items = itemstore.read_feed_items(self)
520 print "feed.load_contents->items: ", len(items)
521 if items:
522 self.restore_items(items)
523 self._items_loaded = True
524 return self._items_loaded
526 def unload_contents(self):
527 if not self._items_loaded:
528 return
529 self._items.clear()
530 self._items_loaded = False
532 @classmethod
533 def create_new_feed(klass, title, location="", username="", password=""):
534 f = klass()
535 f._title = title
536 f._location = location
537 f._id = Config.get_instance().next_feed_id_seq()
538 f._username = username
539 f._password = password
540 return f
542 @classmethod
543 def create_empty_feed(klass):
544 f = klass()
545 return f
548 import UserDict
549 from collections import deque
551 class FifoCache(object, UserDict.DictMixin):
552 ''' A mapping that remembers the last 'num_entries' items that were set '''
554 def __init__(self, num_entries, dct=()):
555 self.num_entries = num_entries
556 self.dct = dict(dct)
557 self.lst = deque()
559 def __repr__(self):
560 return '%r(%r,%r)' % (
561 self.__class__.__name__, self.num_entries, self.dct)
563 def copy(self):
564 return self.__class__(self.num_entries, self.dct)
566 def keys(self):
567 return list(self.lst)
569 def __getitem__(self, key):
570 return self.dct[key]
572 def __setitem__(self, key, value):
573 dct = self.dct
574 lst = self.lst
575 if key in dct:
576 self.remove_from_deque(lst, key)
577 dct[key] = value
578 lst.append(key)
579 if len(lst) > self.num_entries:
580 del dct[lst.popleft()]
582 def __delitem__(self, key):
583 self.dct.pop(key)
584 self.remove_from_deque(self.lst, key)
586 # a method explicitly defined only as an optimization
587 def __contains__(self, item):
588 return item in self.dct
590 has_key = __contains__
592 def remove_from_deque(self, d, x):
593 for i, v in enumerate(d):
594 if v == x:
595 del d[i]
596 return
597 raise ValueError, '%r not in %r' % (x,d)
599 def set_number_of_entries(self, num):
600 self.num_entries = num
601 while len(self.lst) > num:
602 del self.dct[self.lst.popleft()]