use a FifoCache with feed items, fix typo in ImageCache
[straw.git] / src / lib / feeds.py
blob71c008abd93b56ac283ee603124fccde70666d7d
1 import locale, operator
2 import gobject
3 import FeedDataRouter
4 import ItemStore
5 import Config
6 from error import log
8 class FeedList(gobject.GObject):
10 __gsignals__ = {
11 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
12 'updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
13 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT, gobject.TYPE_INT,)),
14 'imported' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
15 (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT,
16 gobject.TYPE_BOOLEAN,)),
17 'deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
20 def __init__(self, init_seq = []):
21 gobject.GObject.__init__(self)
22 self.feedlist = []
23 self._loading = False
25 def __iter__(self):
26 return iter(self.feedlist)
28 def load_data(self):
29 def _load(feedlist, parent):
30 for df in feedlist:
31 if isinstance(df, list):
32 _load(df[1:], parent)
33 else:
34 f = Feed.create_empty_feed()
35 f.undump(df)
36 self.append(parent, f)
37 self._loading = True
38 feedlist = Config.get_instance().feeds
39 if feedlist:
40 _load(feedlist, None)
41 self._loading = False
42 self.emit('changed')
44 def connect_signals(self, ob):
45 ob.connect('changed', self.feed_detail_changed)
47 # these signals are forwarded so that listeners who just want to
48 # listen for a specific event regardless of what feed it came from can
49 # just connect to this feedlist instead of connecting to the
50 # individual feeds.
51 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
52 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
53 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
55 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
56 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
58 def __setitem__(self, key, value):
59 self.feedlist.__setitem__(key, value)
60 self.connect_signals(value)
61 self.save_feeds_and_notify(True)
63 def extend(self, parent, values, from_sub=False):
64 list.extend(self.feedlist, values)
65 for f in values:
66 f.parent = parent
67 self.connect_signals(f)
68 self.save_feeds()
69 self.emit('imported', values, parent, from_sub)
71 def append(self, parent, value):
72 self.feedlist.append(value)
73 value.parent = parent
74 self.connect_signals(value)
75 self.save_feeds()
76 self.emit('updated', value, parent, -1)
78 def insert(self, index, parent, value):
79 self.feedlist.insert(index, value)
80 value.parent = parent
81 self.connect_signals(value)
82 self.save_feeds()
83 self.emit('updated', value, parent, index)
85 def index(self, feed):
86 return self.feedlist.index(feed)
88 def reorder(self, move, delta):
89 k = self.feedlist[:]
90 move = list(move)
91 move.sort()
92 if delta > 0:
93 move.reverse()
94 if move[0] == 0 and delta < 0 or move[-1] == (len(self.feedlist) - 1) and delta > 0:
95 return
96 for m in move:
97 k[m + delta], k[m] = k[m], k[m + delta]
98 for i in range(len(k)):
99 list.__setitem__(self.feedlist, i, k[i])
100 self.save_feeds()
101 self.emit('changed')
103 def __delitem__(self, value):
104 feed = self.feedlist[value]
105 list.__delitem__(self.feedlist, value)
106 feed.delete_all_items()
107 self.save_feeds()
108 self.emit('deleted', feed)
110 def save_feeds(self):
111 if not self._loading:
112 config = Config.get_instance()
113 config.feeds = [f.dump() for f in self.feedlist]
114 return
116 def feed_detail_changed(self, feed):
117 self.save_feeds()
118 self.emit('changed') # XXXX send the feed as well?
120 def _sort_dsu(self, seq):
121 aux_list = [(x.title, x) for x in seq]
122 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
123 return [x[1] for x in aux_list]
125 def sort(self, indices = None):
126 if not indices or len(indices) == 1:
127 self[:] = self._sort_dsu(self)
128 else:
129 items = self._sort_dsu(indices)
130 for i,x in enumerate(items):
131 list.__setitem__(self, indices[i], items[i])
132 self.save_feeds()
133 self.emit('changed')
135 def __hash__(self):
136 h = 0
137 for item in self.feedlist:
138 h ^= hash(item)
139 return h
141 def get_feed_with_id(self, id):
142 for f in self.flatten_list():
143 if f.id == id:
144 return f
145 return None
147 def flatten_list(self, ob=None):
148 if ob is None:
149 ob = self.feedlist
150 l = []
151 for o in ob:
152 if isinstance(o, list):
153 l = l + self.flatten_list(o)
154 else:
155 l.append(o)
156 return l
158 feedlist_instance = None
160 def get_instance():
161 global feedlist_instance
162 if feedlist_instance is None:
163 feedlist_instance = FeedList()
164 return feedlist_instance
166 class Feed(gobject.GObject):
167 "A Feed object stores information set by user about a RSS feed."
169 DEFAULT = -1
170 STATUS_IDLE = 0
171 STATUS_POLLING = 1
173 __slots__ = ('_title', '_location', '_username', '_password', '_parsed',
174 '__save_fields', '_items', '_slots',
175 '_id', '_channel_description',
176 '_channel_title', '_channel_link', '_channel_copyright',
177 'channel_lbd', 'channel_editor', 'channel_webmaster',
178 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
179 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
181 __save_fields = (('_title', ""), ('_location', ""), ('_username', ""),
182 ('_password', ""), ('_id', ""),
183 ('_channel_description', ""), ('_channel_title', ""),
184 ('_channel_link', ""), ('_channel_copyright', ""),
185 ('channel_creator', ""), ('_error', None),
186 ('_items_stored', DEFAULT),
187 ('_poll_freq', DEFAULT),
188 ('_last_poll', 0),
189 ('_n_items_unread',0))
192 __gsignals__ = {
193 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
194 'poll-done' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
195 'items-updated' :(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
196 (gobject.TYPE_PYOBJECT,)),
197 'items-read' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
198 (gobject.TYPE_PYOBJECT,)),
199 'items-deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
200 (gobject.TYPE_PYOBJECT,)),
201 'item-order-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,())
205 # use one of the factory functions below instead of this directly
206 def __init__(self, title="", location="", username="", password=""):
207 gobject.GObject.__init__(self)
208 self._title = title
209 self._channel_description = ""
210 self._channel_title = ""
211 self._channel_link = ""
212 self._channel_copyright = ""
213 self.channel_lbd = None
214 self.channel_editor = ""
215 self.channel_webmaster = ""
216 self.channel_creator = ""
217 self._location = location
218 self._username = username
219 self._password = password
220 self._parsed = None
221 self._error = None
222 self._n_items_unread = 0
223 self._process_status = self.STATUS_IDLE
224 self.router = FeedDataRouter.FeedDataRouter(self)
225 self._parent = None
226 self._items_stored = Feed.DEFAULT
227 self._poll_freq = Feed.DEFAULT
228 self._last_poll = 0
229 self._items = FifoCache(num_entries=Feed.DEFAULT)
230 self._items_loaded = False
231 return
233 def __str__(self):
234 return "Feed '%s' from %s" % (self._title, self._location)
236 @property
237 def id(self):
238 return self._id
240 @apply
241 def parsed():
242 doc = "A ParsedSummary object generated from the summary file"
243 def fget(self):
244 return self._parsed
245 def fset(self, parsed):
246 self._parsed = parsed
247 return property(**locals())
249 @apply
250 def title():
251 doc = "The title of this Feed (as defined by user)"
252 def fget(self):
253 text = ''
254 if self._title:
255 text = self._title
256 return text
257 def fset(self, title):
258 if self._title != title:
259 self._title = title
260 self.emit('changed')
261 return property(**locals())
263 @apply
264 def access_info():
265 doc = "A tuple of location, username, password"
266 def fget(self):
267 return (self._location, self._username, self._password)
268 def fset(self, (location,username,password)):
269 self._location = location
270 self._username = username
271 self._password = password
272 self.emit('changed')
273 return property(**locals())
275 @apply
276 def location():
277 doc = ""
278 def fget(self):
279 return self._location
280 def fset(self, location):
281 if self._location != location:
282 self._location = location
283 self.emit('changed')
284 return property(**locals())
286 @apply
287 def channel_title():
288 doc = ""
289 def fget(self):
290 text = ''
291 if self._channel_title:
292 text = self._channel_title
293 return text
294 def fset(self, t):
295 changed = self._channel_title != t
296 self._channel_title = t
297 if changed:
298 self.emit('changed')
299 return property(**locals())
301 @apply
302 def channel_description():
303 doc = ""
304 def fget(self):
305 text = ''
306 if self._channel_description:
307 text = self._channel_description
308 return text
309 def fset(self, t):
310 changed = self._channel_description != t
311 self._channel_description = t
312 if changed:
313 self.emit('changed')
314 return property(**locals())
316 @apply
317 def channel_link():
318 doc = ""
319 def fget(self):
320 return self._channel_link
321 def fset(self, t):
322 changed = self._channel_link != t
323 self._channel_link = t
324 if changed:
325 self.emit('changed')
326 return property(**locals())
328 @apply
329 def channel_copyright():
330 doc = ""
331 def fget(self):
332 return self._channel_copyright
333 def fset(self, t):
334 changed = self._channel_copyright != t
335 self._channel_copyright = t
336 if changed:
337 self.emit('changed')
338 return property(**locals())
340 @apply
341 def number_of_items_stored():
342 doc = ""
343 def fget(self):
344 return self._items_stored
345 def fset(self, num=None):
346 if self._items_stored != num:
347 self._items_stored = num
348 return property(**locals())
350 @apply
351 def poll_frequency():
352 doc = ""
353 def fget(self):
354 return self._poll_freq
355 def fset(self, freq):
356 if self._poll_freq != freq:
357 self._poll_freq = freq
358 return property(**locals())
360 @apply
361 def last_poll():
362 doc = ""
363 def fget(self):
364 return self._last_poll
365 def fset(self, time):
366 if self._last_poll != time:
367 self._last_poll = time
368 return property(**locals())
370 @apply
371 def number_of_unread():
372 doc = "the number of unread items for this feed"
373 def fget(self):
374 return self._n_items_unread
375 def fset(self, n):
376 self._n_items_unread = n
377 self.emit('changed')
378 return property(**locals())
380 @apply
381 def error():
382 doc = ""
383 def fget(self):
384 return self._error
385 def fset(self, error):
386 if self._error != error:
387 self._error = error
388 self.emit('changed')
389 return property(**locals())
391 @apply
392 def process_status():
393 doc = ""
394 def fget(self):
395 return self._process_status
396 def fset(self, status):
397 if status != self._process_status:
398 self._process_status = status
399 self.emit('changed')
400 return property(**locals())
402 @apply
403 def parent():
404 doc = ""
405 def fget(self):
406 return self._parent
407 def fset(self, parent):
408 self._parent = parent
409 return property(**locals())
411 @property
412 def next_refresh(self):
413 """ return the feed's next refresh (time)"""
414 nr = None
415 if self._poll_freq == self.DEFAULT:
416 increment = Config.get_instance().poll_frequency
417 else:
418 increment = self._poll_freq
419 if increment > 0:
420 nr = self.last_poll + increment
421 return nr
423 def dump(self):
424 fl = {}
425 for f, default in self.__save_fields:
426 fl[f] = self.__getattribute__(f)
427 return fl
429 def undump(self, dict):
430 for f, default in self.__save_fields:
431 self.__setattr__(f, dict.get(f, default))
432 return
434 def poll_done(self):
435 self.emit('poll-done')
437 def add_items(self, items):
438 items = sorted(items, key=operator.attrgetter('pub_date'))
439 config = Config.get_instance()
440 cutpoint = self.number_of_items_stored
441 if cutpoint == Feed.DEFAULT:
442 cutpoint = config.number_of_items_stored
443 self._items.set_number_of_entries(cutpoint)
444 maxid = 0
445 if self._items:
446 maxid = reduce(max, [item.id for item in self._items])
447 print "MAX ID IS ", maxid
448 newitems = []
449 for item in items:
450 maxid += 1
451 item.id = maxid
452 item.feed = self
453 newitems.append(item)
454 self._items[item.id] = item
455 self.emit('items-updated', newitems)
457 def restore_items(self, items):
458 items = sorted(items, key=operator.attrgetter('pub_date'))
459 cutpoint = self.number_of_items_stored
460 if cutpoint == Feed.DEFAULT:
461 config = Config.get_instance()
462 cutpoint = config.number_of_items_stored
463 self._items.set_number_of_entries(cutpoint)
464 olditems = []
465 for idx, item in enumerate(items):
466 if not item.sticky and idx > cutpoint:
467 item.clean_up()
468 olditems.append(item)
469 continue
470 item.feed = self
471 self._items[item.id] = item
472 print "\told: %s, new:%s" % (len(olditems),len(self._items))
473 if olditems:
474 self.emit('items-deleted', olditems)
475 return
477 def item_read_cb(self, item):
478 print "ITEM READ! -> ", item
480 def delete_all_items(self):
481 self.emit('items-deleted', self._items)
483 @property
484 def items(self):
485 if not self._items_loaded:
486 self.load_contents()
487 return self._items.itervalues()
489 def mark_all_items_as_read(self):
490 def mark(item): item.seen = True
491 unread = [item for item in self._items if not item.seen]
492 map(mark, unread)
493 self.emit('items-read', unread)
495 def load_contents(self):
496 if self._items_loaded:
497 return False
498 itemstore = ItemStore.get_instance()
499 items = itemstore.read_feed_items(self)
500 print "feed.load_contents->items: ", len(items)
501 if items:
502 self.restore_items(items)
503 self._items_loaded = True
504 return self._items_loaded
506 def unload_contents(self):
508 Unloads the items by disconnecting the signals and reinitialising the
509 instance variables
511 TODO: unload seems to lose some circular references. garbage collector
512 will find them, though, so maybe it's not a problem.
514 if not self._items_loaded:
515 return
516 self._items.clear()
517 self._items_loaded = False
519 @classmethod
520 def create_new_feed(klass, title, location="", username="", password=""):
521 f = klass()
522 f._title = title
523 f._location = location
524 f._id = Config.get_instance().next_feed_id_seq()
525 f._username = username
526 f._password = password
527 return f
529 @classmethod
530 def create_empty_feed(klass):
531 f = klass()
532 return f
535 import UserDict
536 from collections import deque
538 class FifoCache(object, UserDict.DictMixin):
539 ''' A mapping that remembers the last 'num_entries' items that were set '''
541 def __init__(self, num_entries, dct=()):
542 self.num_entries = num_entries
543 self.dct = dict(dct)
544 self.lst = deque()
546 def __repr__(self):
547 return '%r(%r,%r)' % (
548 self.__class__.__name__, self.num_entries, self.dct)
550 def copy(self):
551 return self.__class__(self.num_entries, self.dct)
553 def keys(self):
554 return list(self.lst)
556 def __getitem__(self, key):
557 return self.dct[key]
559 def __setitem__(self, key, value):
560 dct = self.dct
561 lst = self.lst
562 if key in dct:
563 self.remove_from_deque(lst, key)
564 dct[key] = value
565 lst.append(key)
566 if len(lst) > self.num_entries:
567 del dct[lst.popleft()]
569 def __delitem__(self, key):
570 self.dct.pop(key)
571 self.remove_from_deque(self.lst, key)
573 # a method explicitly defined only as an optimization
574 def __contains__(self, item):
575 return item in self.dct
577 has_key = __contains__
579 def remove_from_deque(self, d, x):
580 for i, v in enumerate(d):
581 if v == x:
582 del d[i]
583 return
584 raise ValueError, '%r not in %r' % (x,d)
586 def set_number_of_entries(self, num):
587 self.num_entries = num
588 while len(self.lst) > num:
589 del self.dct[self.lst.popleft()]