Initial import of the work fork.
[straw/fork.git] / straw / feeds.py
blob35fd060839325036931f0c3dcc9b9973fdc9a564
1 # Copyright (c) 2002-2004 Juri Pakaste <juri@iki.fi>
2 # Copyright (c) 2005-2007 Straw Contributors
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 2 of the
7 # License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public
15 # License along with this program; if not, write to the
16 # Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17 # Boston, MA 02111-1307, USA.
19 import locale, operator
20 import types
21 from gettext import gettext as _
22 from StringIO import StringIO
24 import gobject
26 from straw import ItemStore
27 from straw import Config
28 from straw import error
29 from straw import helpers
30 from straw import opml
32 def opml_import(filename, category=None):
33 opmlstr = read(open(filename))
34 listfeeds = [feed.access_info[0] for feed in feedlist]
35 newitems = [feeds.Feed.create_new_feed(b.text, b.url) for b in opmlstr if b.url not in listfeeds]
36 feedlist.extend(category, newitems)
37 return
39 class FeedList(gobject.GObject):
41 __gsignals__ = {
42 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
45 def __init__(self, init_seq = []):
46 gobject.GObject.__init__(self)
47 self.feedlist = []
48 self._loading = False
50 def __iter__(self):
51 return iter(self.feedlist)
53 def load_data(self):
54 def _load(feedlist, parent):
55 for df in feedlist:
56 if isinstance(df, list):
57 _load(df[1:], parent)
58 else:
59 f = Feed.create_empty_feed()
60 f.undump(df)
61 self.append(parent, f)
62 self._loading = True
63 feedlist = Config.get_instance().feeds
64 if feedlist:
65 _load(feedlist, None)
66 self._loading = False
67 self.emit('changed')
69 # these signals are forwarded so that listeners who just want to
70 # listen for a specific event regardless of what feed it came from can
71 # just connect to this feedlist instead of connecting to the
72 # individual feeds.
73 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
74 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
75 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
76 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
77 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
78 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
80 def __setitem__(self, key, value):
81 self.feedlist.__setitem__(key, value)
82 value.connect('changed', self.feed_detail_changed)
83 self.save_feeds_and_notify(True)
85 def extend(self, parent, values, from_sub=False):
86 list.extend(self.feedlist, values)
87 for f in values:
88 f.parent = parent
89 f.connect('changed', self.feed_detail_changed)
90 self.save_feeds()
92 def append(self, parent, value):
93 self.feedlist.append(value)
94 value.parent = parent
95 value.connect('changed', self.feed_detail_changed)
96 self.save_feeds()
98 def insert(self, index, parent, value):
99 self.feedlist.insert(index, value)
100 value.parent = parent
101 value.connect('changed', self.feed_detail_changed)
102 self.save_feeds()
104 def index(self, feed):
105 return self.feedlist.index(feed)
107 def reorder(self, move, delta):
108 k = self.feedlist[:]
109 move = list(move)
110 move.sort()
111 if delta > 0:
112 move.reverse()
113 if move[0] == 0 and delta < 0 or move[-1] == (len(self.feedlist) - 1) and delta > 0:
114 return
115 for m in move:
116 k[m + delta], k[m] = k[m], k[m + delta]
117 for i in range(len(k)):
118 list.__setitem__(self.feedlist, i, k[i])
119 self.save_feeds()
121 def __delitem__(self, value):
122 feed = self.feedlist[value]
123 list.__delitem__(self.feedlist, value)
124 feed.delete_all_items()
125 self.save_feeds()
127 def save_feeds(self):
128 if not self._loading:
129 config = Config.get_instance()
130 config.feeds = [f.dump() for f in self.feedlist]
131 return
133 def feed_detail_changed(self, feed):
134 self.save_feeds()
135 # self.emit('changed') # XXXX send the feed as well?
137 def _sort_dsu(self, seq):
138 aux_list = [(x.title, x) for x in seq]
139 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
140 return [x[1] for x in aux_list]
142 def sort(self, indices = None):
143 if not indices or len(indices) == 1:
144 self[:] = self._sort_dsu(self)
145 else:
146 items = self._sort_dsu(indices)
147 for i,x in enumerate(items):
148 list.__setitem__(self, indices[i], items[i])
149 self.save_feeds()
150 # self.emit('changed')
152 def __hash__(self):
153 h = 0
154 for item in self.feedlist:
155 h ^= hash(item)
156 return h
158 def get_feed_with_id(self, id):
159 for f in self.flatten_list():
160 if f.id == id:
161 return f
162 return None
164 def flatten_list(self, ob=None):
165 if ob is None:
166 ob = self.feedlist
167 l = []
168 for o in ob:
169 if isinstance(o, list):
170 l = l + self.flatten_list(o)
171 else:
172 l.append(o)
173 return l
175 feedlist_instance = None
177 def get_feedlist_instance():
178 global feedlist_instance
179 if feedlist_instance is None:
180 feedlist_instance = FeedList()
181 return feedlist_instance
183 feedlist = get_feedlist_instance()
185 class IdleState(object):
186 ''' state for idle or normal operation '''
187 def __init__(self):
188 filename = os.path.join(straw.STRAW_DATA_DIR, 'feed.png')
189 self.icon = gtk.gdk.pixbuf_new_from_file(filename)
191 @property
192 def icon(self):
193 return self.icon
195 @property
196 def mesg(self):
197 return None
199 class PollingState(object):
200 ''' state when feed is polling '''
201 def __init__(self):
202 self._icon = gtk.Image()
203 self._icon.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU)
205 @property
206 def icon(self):
207 return self._icon.get_pixbuf()
209 @property
210 def mesg(self):
211 return None
213 class ErrorState(object):
214 ''' state when feed has errors '''
216 def __init__(self, mesg):
217 self._icon = gtk.Image()
218 self._icon.set_from_stock(gtk.STOCK_DIALOG_ERROR, gtk.ICON_SIZE_MENU)
219 self._mesg = mesg
221 @property
222 def icon(self):
223 return self._icon.get_pixbuf()
225 @property
226 def mesg(self):
227 return self._mesg
229 class Feed(gobject.GObject):
230 "A Feed object stores information set by user about a RSS feed."
232 DEFAULT = -1
233 STATUS_IDLE = 0
234 STATUS_POLLING = 1
236 __slots__ = ('_title', '_location', '_username', '_password', '_parsed',
237 '__save_fields', '_items', '_slots',
238 '_id', '_channel_description',
239 '_channel_title', '_channel_link', '_channel_copyright',
240 'channel_lbd', 'channel_editor', 'channel_webmaster',
241 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
242 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
244 __save_fields = (('_title', ""), ('_location', ""), ('_username', ""),
245 ('_password', ""), ('_id', ""),
246 ('_channel_description', ""), ('_channel_title', ""),
247 ('_channel_link', ""), ('_channel_copyright', ""),
248 ('channel_creator', ""), ('_error', None),
249 ('_items_stored', DEFAULT),
250 ('_poll_freq', DEFAULT),
251 ('_last_poll', 0),
252 ('_n_items_unread',0))
255 __gsignals__ = {
256 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
257 'poll-done' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
258 'items-added' :(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
259 (gobject.TYPE_PYOBJECT,)),
260 'items-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
261 (gobject.TYPE_PYOBJECT,)),
262 'items-deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
263 (gobject.TYPE_PYOBJECT,))
267 # use one of the factory functions below instead of this directly
268 def __init__(self, title="", location="", username="", password=""):
269 import FeedDataRouter
270 gobject.GObject.__init__(self)
271 self._title = title
272 self._channel_description = ""
273 self._channel_title = ""
274 self._channel_link = ""
275 self._channel_copyright = ""
276 self.channel_lbd = None
277 self.channel_editor = ""
278 self.channel_webmaster = ""
279 self.channel_creator = ""
280 self._location = location
281 self._username = username
282 self._password = password
283 self._parsed = None
284 self._error = None
285 self._n_items_unread = 0
286 self._process_status = self.STATUS_IDLE
287 self.router = FeedDataRouter.FeedDataRouter(self)
288 self._parent = None
289 self._items_stored = Feed.DEFAULT
290 self._poll_freq = Feed.DEFAULT
291 self._last_poll = 0
293 self.config = Config.get_instance()
294 # XXX move this to subscriptions
295 self.config.connect('item-order-changed', self.item_order_changed_cb)
296 self.config.connect('item-stored-changed', self.item_stored_changed_cb)
297 self.item_order_reverse = self.config.item_order
298 self.item_stored_num = self.config.number_of_items_stored
299 self._items = FifoCache(num_entries=Feed.DEFAULT)
300 self._items_loaded = False
301 return
303 def __str__(self):
304 return "Feed '%s' from %s" % (self._title, self._location)
306 @property
307 def id(self):
308 return self._id
310 @apply
311 def parsed():
312 doc = "A ParsedSummary object generated from the summary file"
313 def fget(self):
314 return self._parsed
315 def fset(self, parsed):
316 self._parsed = parsed
317 return property(**locals())
319 @apply
320 def title():
321 doc = "The title of this Feed (as defined by user)"
322 def fget(self):
323 text = ''
324 if self._title:
325 text = self._title
326 return text
327 def fset(self, title):
328 if self._title != title:
329 self._title = title
330 self.emit('changed')
331 return property(**locals())
333 @apply
334 def access_info():
335 doc = "A tuple of location, username, password"
336 def fget(self):
337 return (self._location, self._username, self._password)
338 def fset(self, (location,username,password)):
339 self._location = location
340 self._username = username
341 self._password = password
342 self.emit('changed')
343 return property(**locals())
345 @apply
346 def location():
347 doc = ""
348 def fget(self):
349 return self._location
350 def fset(self, location):
351 if self._location != location:
352 self._location = location
353 self.emit('changed')
354 return property(**locals())
356 @apply
357 def channel_title():
358 doc = ""
359 def fget(self):
360 text = ''
361 if self._channel_title:
362 text = self._channel_title
363 return text
364 def fset(self, t):
365 changed = self._channel_title != t
366 self._channel_title = t
367 if changed:
368 self.emit('changed')
369 return property(**locals())
371 @apply
372 def channel_description():
373 doc = ""
374 def fget(self):
375 text = ''
376 if self._channel_description:
377 text = self._channel_description
378 return text
379 def fset(self, t):
380 changed = self._channel_description != t
381 self._channel_description = t
382 if changed:
383 self.emit('changed')
384 return property(**locals())
386 @apply
387 def channel_link():
388 doc = ""
389 def fget(self):
390 return self._channel_link
391 def fset(self, t):
392 changed = self._channel_link != t
393 self._channel_link = t
394 if changed:
395 self.emit('changed')
396 return property(**locals())
398 @apply
399 def channel_copyright():
400 doc = ""
401 def fget(self):
402 return self._channel_copyright
403 def fset(self, t):
404 changed = self._channel_copyright != t
405 self._channel_copyright = t
406 if changed:
407 self.emit('changed')
408 return property(**locals())
410 @apply
411 def number_of_items_stored():
412 doc = ""
413 def fget(self):
414 return self._items_stored
415 def fset(self, num=None):
416 if self._items_stored != num:
417 self._items_stored = num
418 return property(**locals())
420 @apply
421 def poll_frequency():
422 doc = ""
423 def fget(self):
424 return self._poll_freq
425 def fset(self, freq):
426 if self._poll_freq != freq:
427 self._poll_freq = freq
428 return property(**locals())
430 @apply
431 def last_poll():
432 doc = ""
433 def fget(self):
434 return self._last_poll
435 def fset(self, time):
436 if self._last_poll != time:
437 self._last_poll = time
438 return property(**locals())
440 @apply
441 def number_of_unread():
442 doc = "the number of unread items for this feed"
443 def fget(self):
444 return self._n_items_unread
445 def fset(self, n):
446 self._n_items_unread = n
447 self.emit('changed')
448 return property(**locals())
450 @apply
451 def error():
452 doc = ""
453 def fget(self):
454 return self._error
455 def fset(self, error):
456 if self._error != error:
457 self._error = error
458 self.emit('changed')
459 return property(**locals())
461 @apply
462 def process_status():
463 doc = ""
464 def fget(self):
465 return self._process_status
466 def fset(self, status):
467 if status != self._process_status:
468 self._process_status = status
469 self.emit('changed')
470 return property(**locals())
472 @apply
473 def parent():
474 doc = ""
475 def fget(self):
476 return self._parent
477 def fset(self, parent):
478 self._parent = parent
479 return property(**locals())
481 @property
482 def next_refresh(self):
483 """ return the feed's next refresh (time)"""
484 nr = None
485 if self._poll_freq == self.DEFAULT:
486 increment = self.config.poll_frequency
487 else:
488 increment = self._poll_freq
489 if increment > 0:
490 nr = self.last_poll + increment
491 return nr
493 def dump(self):
494 fl = {}
495 for f, default in self.__save_fields:
496 fl[f] = self.__getattribute__(f)
497 return fl
499 def undump(self, dict):
500 for f, default in self.__save_fields:
501 self.__setattr__(f, dict.get(f, default))
502 return
504 def poll_done(self):
505 self.emit('poll-done')
507 def item_order_changed_cb(self, config):
508 self.item_order_reverse = config.item_order
510 def item_stored_changed_cb(self, config):
511 self.item_stored_num = config.number_of_items_stored
513 def get_cutpoint(self):
514 cutpoint = self.number_of_items_stored
515 if cutpoint == Feed.DEFAULT:
516 cutpoint = self.item_stored_num
517 return cutpoint
520 def add_items(self, items):
521 if not self._items_loaded: self.load_contents()
522 items = sorted(items, key=operator.attrgetter('pub_date'),
523 reverse=self.item_order_reverse)
524 self._items.set_number_of_entries(self.get_cutpoint())
525 maxid = 0
526 if self._items:
527 maxid = reduce(max, [item.id for item in self._items.itervalues()])
528 newitems = filter(lambda x: x not in self._items.itervalues(), items)
529 for item in newitems:
530 maxid += 1
531 item.id = maxid
532 item.feed = self
533 item.connect('changed', self.item_changed_cb)
534 self._items[item.id] = item ## XXX pruned items don't get clean up properly
535 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
536 print self._items.keys()
537 self.emit('items-added', newitems)
539 def restore_items(self, items):
540 items = sorted(items, key=operator.attrgetter('pub_date'),
541 reverse=self.item_order_reverse)
542 cutpoint = self.get_cutpoint()
543 self._items.set_number_of_entries(cutpoint)
544 olditems = []
545 for idx, item in enumerate(items):
546 if not item.sticky and idx >= cutpoint:
547 item.clean_up()
548 olditems.append(item)
549 continue
550 item.feed = self
551 item.connect('changed', self.item_changed_cb)
552 self._items[item.id] = item
553 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
554 if olditems: self.emit('items-deleted', olditems)
555 print self._items.keys()
556 return
558 def item_changed_cb(self, item):
559 self.number_of_unread = len(filter(lambda x: not x.seen, self._items.itervalues()))
560 self.emit('items-changed', [item])
562 def delete_all_items(self):
563 self._items.clear()
564 self.emit('items-deleted', self._items.values())
566 @property
567 def items(self):
568 if not self._items_loaded:
569 self.load_contents()
570 return self._items.values()
572 def mark_items_as_read(self, items=None):
573 def mark(item): item.seen = True
574 unread = [item for item in self._items.itervalues() if not item.seen]
575 map(mark, unread)
576 self.emit('items-changed', unread)
578 def load_contents(self):
579 if self._items_loaded:
580 return False
581 itemstore = ItemStore.get_instance()
582 items = itemstore.read_feed_items(self)
583 print "feed.load_contents->items: ", len(items)
584 if items:
585 self.restore_items(items)
586 self._items_loaded = True
587 return self._items_loaded
589 def unload_contents(self):
590 if not self._items_loaded:
591 return
592 self._items.clear()
593 self._items_loaded = False
595 @classmethod
596 def create_new_feed(klass, title, location="", username="", password=""):
597 f = klass()
598 f._title = title
599 f._location = location
600 f._id = Config.get_instance().next_feed_id_seq()
601 f._username = username
602 f._password = password
603 return f
605 @classmethod
606 def create_empty_feed(klass):
607 f = klass()
608 return f
611 import UserDict
612 from collections import deque
614 class FifoCache(object, UserDict.DictMixin):
615 ''' A mapping that remembers the last 'num_entries' items that were set '''
617 def __init__(self, num_entries, dct=()):
618 self.num_entries = num_entries
619 self.dct = dict(dct)
620 self.lst = deque()
622 def __repr__(self):
623 return '%r(%r,%r)' % (
624 self.__class__.__name__, self.num_entries, self.dct)
626 def copy(self):
627 return self.__class__(self.num_entries, self.dct)
629 def keys(self):
630 return list(self.lst)
632 def __getitem__(self, key):
633 return self.dct[key]
635 def __setitem__(self, key, value):
636 dct = self.dct
637 lst = self.lst
638 if key in dct:
639 self.remove_from_deque(lst, key)
640 dct[key] = value
641 lst.append(key)
642 if len(lst) > self.num_entries:
643 del dct[lst.popleft()]
645 def __delitem__(self, key):
646 self.dct.pop(key)
647 self.remove_from_deque(self.lst, key)
649 # a method explicitly defined only as an optimization
650 def __contains__(self, item):
651 return item in self.dct
653 has_key = __contains__
655 def remove_from_deque(self, d, x):
656 for i, v in enumerate(d):
657 if v == x:
658 del d[i]
659 return
660 raise ValueError, '%r not in %r' % (x,d)
662 def set_number_of_entries(self, num):
663 self.num_entries = num
664 while len(self.lst) > num:
665 self.dct.pop(self.lst.popleft())
667 PSEUDO_ALL_KEY = 'ALL'
668 PSEUDO_UNCATEGORIZED_KEY = 'UNCATEGORIZED'
670 class FeedCategoryList(gobject.GObject):
672 __gsignals__ = {
673 'added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
674 'deleted' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
675 'category-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
676 'pseudo-changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
679 def __init__(self):
680 gobject.GObject.__init__(self)
681 # have to define this here so the titles can be translated
682 PSEUDO_TITLES = {PSEUDO_ALL_KEY: _('All'),
683 PSEUDO_UNCATEGORIZED_KEY: _('Feeds')}
684 self._all_category = PseudoCategory(PSEUDO_TITLES[PSEUDO_ALL_KEY],
685 PSEUDO_ALL_KEY)
686 self._un_category = PseudoCategory(
687 PSEUDO_TITLES[PSEUDO_UNCATEGORIZED_KEY], PSEUDO_UNCATEGORIZED_KEY)
688 self._user_categories = []
689 self._pseudo_categories = (self._all_category, self._un_category)
690 self._loading = False
691 self._feedlist = get_feedlist_instance()
693 def load_data(self):
694 """Loads categories from config.
695 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
696 {'id', feed_id1, 'from_subscription': bool} ...], ...]
698 cats = Config.get_instance().categories or []
699 categorized = {}
700 for c in cats:
701 head = c[0]
702 tail = c[1:]
703 pseudo_key = head.get('pseudo', None)
704 if pseudo_key == PSEUDO_ALL_KEY:
705 fc = self.all_category
706 elif pseudo_key == PSEUDO_UNCATEGORIZED_KEY:
707 fc = self.un_category
708 else:
709 fc = FeedCategory(head['title'])
710 sub = head.get('subscription', None)
711 if sub is not None:
712 fc.subscription = undump_subscription(sub)
713 for f in tail:
714 # backwards compatibility for stuff saved with versions <= 0.23
715 if type(f) is int:
716 fid = f
717 from_sub = False
718 else:
719 fid = f['id']
720 from_sub = f['from_subscription']
721 feed = self._feedlist.get_feed_with_id(fid)
722 if feed and not pseudo_key: # we deal with pseudos later
723 if feed in fc.feeds:
724 error.log("%s (%d) was already in %s, skipping" % (str(feed), fid, str(fc)))
725 continue
726 fc.append_feed(feed, from_sub)
727 categorized[feed] = True
728 # User categories: connect pseudos later
729 if not pseudo_key:
730 fc.connect('changed', self.category_changed)
731 self._user_categories.append(fc)
732 # just in case we've missed any feeds, go through the list
733 # and add to the pseudocategories. cache the feed list of all_category
734 # so we don't get a function call (and a list comprehension loop
735 # inside it) on each feed. it should be ok here, there are no
736 # duplicates in feedlist. right?
737 pseudos_changed = False
738 all_feeds = self.all_category.feeds
739 for f in self._feedlist:
740 if f not in all_feeds:
741 self.all_category.append_feed(f, False)
742 pseudos_changed = True
743 uf = categorized.get(f, None)
744 if not uf:
745 self.un_category.append_feed(f, False)
746 pseudos_changed = True
747 if pseudos_changed:
748 self.save_data()
749 for cat in self.pseudo_categories:
750 cat.connect('changed', self.pseudo_category_changed)
752 def save_data(self):
753 Config.get_instance().categories = [
754 cat.dump() for cat in self]
756 def pseudo_category_changed(self, category, *args):
757 self.save_data()
758 self.emit('pseudo-changed')
760 def category_changed(self, signal):
761 if signal.feed is not None:
762 uncategorized = True
763 for cat in self.user_categories:
764 if signal.feed in cat.feeds:
765 uncategorized = False
766 break
767 if uncategorized:
768 self.un_category.append_feed(signal.feed, False)
769 else:
770 try:
771 self.un_category.remove_feed(signal.feed)
772 except ValueError:
773 pass
774 self.save_data()
775 self.emit('category-changed')
777 def remove_feed(self, feed):
778 for c in self:
779 try:
780 c.remove_feed(feed)
781 del self._feedlist[feed]
782 except ValueError:
783 pass
785 def append_feed(self, feed, category=None, index=None):
786 self._feedlist.append(category, feed)
787 if category and category not in self.pseudo_categories:
788 if index:
789 category.insert_feed(index, feed, False)
790 else:
791 category.append_feed(feed, False)
792 else:
793 print "uncategory append"
794 self.un_category.append_feed(feed, False)
795 print "all category append"
796 self.all_category.append_feed(feed, False)
798 def import_feeds(self, feeds, category=None, from_sub=False):
799 self._feedlist.extend(category, feeds)
800 if category and category not in self.pseudo_categories:
801 category.extend_feed(feeds, from_sub)
802 else:
803 print 'feed imported ', feeds
804 self.un_category.extend_feed(feeds, from_sub)
805 self.all_category.extend_feed(feeds, from_sub)
807 @property
808 def user_categories(self):
809 return self._user_categories
811 @property
812 def pseudo_categories(self):
813 return self._pseudo_categories
815 @property
816 def all_categories(self):
817 return self.pseudo_categories + tuple(self.user_categories)
819 @property
820 def all_category(self):
821 return self._all_category
823 @property
824 def un_category(self):
825 return self._un_category
827 class CategoryIterator:
828 def __init__(self, fclist):
829 self._fclist = fclist
830 self._index = -1
832 def __iter__(self):
833 return self
835 def _next(self):
836 self._index += 1
837 i = self._index
838 uclen = len(self._fclist.user_categories)
839 if i < uclen:
840 return self._fclist.user_categories[i]
841 elif i < uclen + len(self._fclist.pseudo_categories):
842 return self._fclist.pseudo_categories[i - uclen]
843 else:
844 raise StopIteration
846 def next(self):
847 v = self._next()
848 return v
850 def __iter__(self):
851 return self.CategoryIterator(self)
853 def add_category(self, category):
854 category.connect('changed', self.category_changed)
855 self._user_categories.append(category)
856 auxlist = [(x.title.lower(),x) for x in self._user_categories]
857 auxlist.sort()
858 self._user_categories = [x[1] for x in auxlist]
859 self.save_data()
860 self.emit('added', category)
862 def remove_category(self, category):
863 for feed in category.feeds:
864 category.remove_feed(feed)
865 self._user_categories.remove(category)
866 self.save_data()
867 self.emit('deleted', category)
869 # It might be good to have a superclass FeedCategorySubscription or something
870 # so we could support different formats. However, I don't know of any other
871 # relevant format used for this purpose, so that can be done later if needed.
872 # Of course, they could also just implement the same interface.
873 class OPMLCategorySubscription(gobject.GObject):
874 REFRESH_DEFAULT = -1
876 __gsignals__ = {
877 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
878 'updated' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
881 def __init__(self, location=None):
882 gobject.GObject.__init__(self)
883 self._location = location
884 self._username = None
885 self._password = None
886 self._contents = None
887 self._frequency = OPMLCategorySubscription.REFRESH_DEFAULT
888 self._last_poll = 0
889 self._error = None
891 @apply
892 def location():
893 doc = ""
894 def fget(self):
895 return self._location
896 def fset(self, location):
897 self._location = location
898 self.emit('changed')
899 return property(**locals())
901 @apply
902 def username():
903 doc = ""
904 def fget(self):
905 return self._username
906 def fset(self, username):
907 self._username = username
908 self.emit('changed')
909 return property(**locals())
911 @apply
912 def password():
913 doc = ""
914 def fget(self):
915 return self._password
916 def fset(self, password):
917 self._password = password
918 self.emit('changed')
919 return property(**locals())
921 @apply
922 def frequency():
923 doc = ""
924 def fget(self):
925 return self._frequency
926 def fset(self, freq):
927 self._frequency = freq
928 self.emit('changed')
929 return property(**locals())
931 @apply
932 def last_poll():
933 doc = ""
934 def fget(self):
935 return self._last_poll
936 def fset(self, last_poll):
937 self._last_poll = last_poll
938 self.emit('changed')
939 return property(**locals())
941 @apply
942 def error():
943 doc = ""
944 def fget(self):
945 return self._error
946 def fset(self, error):
947 self._error = error
948 self.emit('changed')
949 return property(**locals())
951 def parse(self, data):
952 datastream = StringIO(data)
953 entries = opml.read(datastream)
954 contents = [(e.url, e.text) for e in entries]
955 updated = contents == self._contents
956 self._contents = contents
957 if updated:
958 self.emit('updated')
959 return
961 @property
962 def contents(self):
963 return self._contents
965 @classmethod
966 def undump(klass, dictionary):
967 sub = klass()
968 sub.location = dictionary.get('location')
969 sub.username = dictionary.get('username')
970 sub.password = dictionary.get('password')
971 sub.frequency = dictionary.get(
972 'frequency', OPMLCategorySubscription.REFRESH_DEFAULT)
973 sub.last_poll = dictionary.get('last_poll', 0)
974 sub.error = dictionary.get('error')
975 return sub
977 def dump(self):
978 return {'type': 'opml',
979 'location': self.location,
980 'username': self.username,
981 'password': self.password,
982 'frequency': self.frequency,
983 'last_poll': self.last_poll,
984 'error': self.error}
986 def undump_subscription(dictionary):
987 try:
988 if dictionary.get('type') == 'opml':
989 return OPMLCategorySubscription.undump(dictionary)
990 except Exception, e:
991 error.log("exception while undumping subscription: " + str(e))
992 raise
994 class CategoryMember(object):
995 def __init__(self, feed=None, from_sub=False):
996 self._feed = feed
997 self._from_subscription = from_sub
999 @apply
1000 def feed():
1001 doc = ""
1002 def fget(self):
1003 return self._feed
1004 def fset(self, feed):
1005 self._feed = feed
1006 return property(**locals())
1008 @apply
1009 def from_subscription():
1010 doc = ""
1011 def fget(self):
1012 return self._from_subscription
1013 def fset(self, p):
1014 self._from_subscription = p
1015 return property(**locals())
1017 class FeedCategory(gobject.GObject):
1019 __gsignals__ = {
1020 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,()),
1021 'feed-added':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
1022 (gobject.TYPE_PYOBJECT,)),
1023 'feed-removed':(gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
1024 (gobject.TYPE_PYOBJECT,))
1027 def __init__(self, title=""):
1028 gobject.GObject.__init__(self)
1029 self.feedlist = []
1030 self._title = title
1031 self._subscription = None
1033 @apply
1034 def title():
1035 doc = ""
1036 def fget(self):
1037 return self._title
1038 def fset(self, title):
1039 self._title = title
1040 self.emit('changed')
1041 return property(**locals())
1043 @apply
1044 def subscription():
1045 doc = ""
1046 def fget(self):
1047 return self._subscription
1048 def fset(self, sub):
1049 self._subscription = sub
1050 self._subscription.connect('changed', self._subscription_changed)
1051 self._subscription.connect('updated', self._subscription_contents_updated)
1052 self.emit('changed')
1053 return property(**locals())
1055 def read_contents_from_subscription(self):
1056 if self.subscription is None:
1057 return
1058 subfeeds = self.subscription.contents
1059 sfdict = dict(subfeeds)
1060 feedlist = get_feedlist_instance()
1061 current = dict([(feed.location, feed) for feed in self.feeds])
1062 allfeeds = dict([(feed.location, feed) for feed in feedlist])
1063 common, toadd, toremove = helpers.listdiff(sfdict.keys(), current.keys())
1064 existing, nonexisting, ignore = helpers.listdiff(
1065 toadd, allfeeds.keys())
1067 newfeeds = [Feed.create_new_feed(sfdict[f], f) for f in nonexisting]
1068 feedlist.extend(self.feedlist, newfeeds, from_sub=True) # will call extend_feed
1069 self.extend_feed([allfeeds[f] for f in existing], True)
1071 for f in toremove:
1072 index = self.index_feed(allfeeds[f])
1073 member = self.feedlist[index]
1074 if member.from_subscription:
1075 self.remove_feed(allfeeds[f])
1076 return
1078 def _subscription_changed(self, *args):
1079 self.emit('changed')
1081 def _subscription_contents_updated(self, *args):
1082 self.read_contents_from_subscription()
1084 def __str__(self):
1085 return "FeedCategory %s" % self.title
1087 def __hash__(self):
1088 return hash(id(self))
1090 def append_feed(self, value, from_sub):
1091 self.feedlist.append(CategoryMember(value, from_sub))
1092 self.emit('feed-added', value)
1094 def extend_feed(self, values, from_sub):
1095 self.feedlist.extend([CategoryMember(v, from_sub) for v in values])
1096 self.emit('changed')
1098 def insert_feed(self, index, value, from_sub):
1099 self.feedlist.insert(index, CategoryMember(value, from_sub))
1100 self.emit('feed-added', value)
1102 def remove(self, value):
1103 self.feedlist.remove(value)
1104 self.emit('feed-removed', value.feed)
1106 def remove_feed(self, value):
1107 for index, member in enumerate(self.feedlist):
1108 if member.feed is value:
1109 del self.feedlist[index]
1110 break
1111 else:
1112 raise ValueError(value)
1113 self.emit('feed-removed', value)
1115 def reverse(self):
1116 self.feedlist.reverse()
1117 self.emit('changed') # reverse=True))
1119 def index_feed(self, value):
1120 for index, f in enumerate(self.feedlist):
1121 if self.feedlist[index].feed is value:
1122 return index
1123 raise ValueError(value)
1125 def _sort_dsu(self, seq):
1126 aux_list = [(x.feed.title.lower(), x) for x in seq]
1127 aux_list.sort(lambda a,b:locale.strcoll(a[0],b[0]))
1128 return [x[1] for x in aux_list]
1130 def sort(self, indices=None):
1131 if not indices or len(indices) == 1:
1132 self.feedlist[:] = self._sort_dsu(self.feedlist)
1133 else:
1134 items = self._sort_dsu(indices)
1135 for i,x in enumerate(items):
1136 list.__setitem__(self.feedlist, indices[i], items[i])
1137 self.emit('changed')
1139 def move_feed(self, source, target):
1140 if target > source:
1141 target -= 1
1142 if target == source:
1143 return
1144 t = self[source]
1145 del self[source]
1146 self.feedlist.insert(target, t)
1147 self.emit('changed')
1149 def dump(self):
1150 head = {'title': self.title}
1151 if self.subscription is not None:
1152 head['subscription'] = self.subscription.dump()
1153 return [head] + [
1154 {'id': f.feed.id, 'from_subscription': f.from_subscription}
1155 for f in self.feedlist]
1157 @property
1158 def feeds(self):
1159 return [f.feed for f in self.feedlist]
1161 def __eq__(self, ob):
1162 if isinstance(ob, types.NoneType):
1163 return 0
1164 elif isinstance(ob, FeedCategory):
1165 return self.title == ob.title and list.__eq__(self.feedlist, ob)
1166 else:
1167 raise NotImplementedError
1169 def __contains__(self, item):
1170 error.log("warning, should probably be querying the feeds property instead?")
1171 return list.__contains__(self.feedlist, item)
1173 class PseudoCategory(FeedCategory):
1174 def __init__(self, title="", key=None):
1175 if key not in (PSEUDO_ALL_KEY, PSEUDO_UNCATEGORIZED_KEY):
1176 raise ValueError, "Invalid key"
1177 FeedCategory.__init__(self, title)
1178 self._pseudo_key = key
1180 def __str__(self):
1181 return "PseudoCategory %s" % self.title
1183 def dump(self):
1184 return [{'pseudo': self._pseudo_key, 'title': ''}] + [
1185 {'id': f.feed.id, 'from_subscription': False} for f in self.feedlist]
1187 def append_feed(self, feed, from_sub):
1188 assert not from_sub
1189 FeedCategory.append_feed(self, feed, False)
1191 def insert_feed(self, index, feed, from_sub):
1192 assert not from_sub
1193 FeedCategory.insert_feed(self, index, feed, False)
1195 fclist = None
1197 def get_category_list():
1198 global fclist
1199 if fclist is None:
1200 fclist = FeedCategoryList()
1201 return fclist
1203 category_list = get_category_list()