1 import locale
, operator
5 from StringIO
import StringIO
12 from utils
import calltrace
15 class FeedList(gobject
.GObject
):
18 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
21 def __init__(self
, init_seq
= []):
22 gobject
.GObject
.__init
__(self
)
27 return iter(self
.feedlist
)
30 def _load(feedlist
, parent
):
32 if isinstance(df
, list):
35 f
= Feed
.create_empty_feed()
37 self
.append(parent
, f
)
39 feedlist
= Config
.get_instance().feeds
45 # these signals are forwarded so that listeners who just want to
46 # listen for a specific event regardless of what feed it came from can
47 # just connect to this feedlist instead of connecting to the
49 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
50 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
51 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
52 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
53 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
56 def __setitem__(self
, key
, value
):
57 self
.feedlist
.__setitem
__(key
, value
)
58 value
.connect('changed', self
.feed_detail_changed
)
59 self
.save_feeds_and_notify(True)
61 def extend(self
, parent
, values
, from_sub
=False):
62 list.extend(self
.feedlist
, values
)
65 f
.connect('changed', self
.feed_detail_changed
)
68 def append(self
, parent
, value
):
69 self
.feedlist
.append(value
)
71 value
.connect('changed', self
.feed_detail_changed
)
74 def insert(self
, index
, parent
, value
):
75 self
.feedlist
.insert(index
, value
)
77 value
.connect('changed', self
.feed_detail_changed
)
80 def index(self
, feed
):
81 return self
.feedlist
.index(feed
)
83 def reorder(self
, move
, delta
):
89 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
92 k
[m
+ delta
], k
[m
] = k
[m
], k
[m
+ delta
]
93 for i
in range(len(k
)):
94 list.__setitem
__(self
.feedlist
, i
, k
[i
])
97 def __delitem__(self
, value
):
98 feed
= self
.feedlist
[value
]
99 list.__delitem
__(self
.feedlist
, value
)
100 feed
.delete_all_items()
103 def save_feeds(self
):
104 if not self
._loading
:
105 config
= Config
.get_instance()
106 config
.feeds
= [f
.dump() for f
in self
.feedlist
]
109 def feed_detail_changed(self
, feed
):
111 # self.emit('changed') # XXXX send the feed as well?
113 def _sort_dsu(self
, seq
):
114 aux_list
= [(x
.title
, x
) for x
in seq
]
115 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
116 return [x
[1] for x
in aux_list
]
118 def sort(self
, indices
= None):
119 if not indices
or len(indices
) == 1:
120 self
[:] = self
._sort
_dsu
(self
)
122 items
= self
._sort
_dsu
(indices
)
123 for i
,x
in enumerate(items
):
124 list.__setitem
__(self
, indices
[i
], items
[i
])
126 # self.emit('changed')
130 for item
in self
.feedlist
:
134 def get_feed_with_id(self
, id):
135 for f
in self
.flatten_list():
140 def flatten_list(self
, ob
=None):
145 if isinstance(o
, list):
146 l
= l
+ self
.flatten_list(o
)
151 feedlist_instance
= None
153 def get_feedlist_instance():
154 global feedlist_instance
155 if feedlist_instance
is None:
156 feedlist_instance
= FeedList()
157 return feedlist_instance
159 feedlist
= get_feedlist_instance()
161 class IdleState(object):
162 ''' state for idle or normal operation '''
164 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
165 self
.icon
= gtk
.gdk
.pixbuf_new_from_file(filename
)
175 class PollingState(object):
176 ''' state when feed is polling '''
178 self
._icon
= gtk
.Image()
179 self
._icon
.set_from_stock(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
183 return self
._icon
.get_pixbuf()
189 class ErrorState(object):
190 ''' state when feed has errors '''
192 def __init__(self
, mesg
):
193 self
._icon
= gtk
.Image()
194 self
._icon
.set_from_stock(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
199 return self
._icon
.get_pixbuf()
205 class Feed(gobject
.GObject
):
206 "A Feed object stores information set by user about a RSS feed."
212 __slots__
= ('_title', '_location', '_username', '_password', '_parsed',
213 '__save_fields', '_items', '_slots',
214 '_id', '_channel_description',
215 '_channel_title', '_channel_link', '_channel_copyright',
216 'channel_lbd', 'channel_editor', 'channel_webmaster',
217 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
218 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
220 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
221 ('_password', ""), ('_id', ""),
222 ('_channel_description', ""), ('_channel_title', ""),
223 ('_channel_link', ""), ('_channel_copyright', ""),
224 ('channel_creator', ""), ('_error', None),
225 ('_items_stored', DEFAULT
),
226 ('_poll_freq', DEFAULT
),
228 ('_n_items_unread',0))
232 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
233 'poll-done' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
234 'items-added' :(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
235 (gobject
.TYPE_PYOBJECT
,)),
236 'items-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
237 (gobject
.TYPE_PYOBJECT
,)),
238 'items-deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
239 (gobject
.TYPE_PYOBJECT
,))
243 # use one of the factory functions below instead of this directly
244 def __init__(self
, title
="", location
="", username
="", password
=""):
245 import FeedDataRouter
246 gobject
.GObject
.__init
__(self
)
248 self
._channel
_description
= ""
249 self
._channel
_title
= ""
250 self
._channel
_link
= ""
251 self
._channel
_copyright
= ""
252 self
.channel_lbd
= None
253 self
.channel_editor
= ""
254 self
.channel_webmaster
= ""
255 self
.channel_creator
= ""
256 self
._location
= location
257 self
._username
= username
258 self
._password
= password
261 self
._n
_items
_unread
= 0
262 self
._process
_status
= self
.STATUS_IDLE
263 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
265 self
._items
_stored
= Feed
.DEFAULT
266 self
._poll
_freq
= Feed
.DEFAULT
269 self
.config
= Config
.get_instance()
270 # XXX move this to subscriptions
271 self
.config
.connect('item-order-changed', self
.item_order_changed_cb
)
272 self
.config
.connect('item-stored-changed', self
.item_stored_changed_cb
)
273 self
.item_order_reverse
= self
.config
.item_order
274 self
.item_stored_num
= self
.config
.number_of_items_stored
275 self
._items
= FifoCache(num_entries
=Feed
.DEFAULT
)
276 self
._items
_loaded
= False
280 return "Feed '%s' from %s" % (self
._title
, self
._location
)
288 doc
= "A ParsedSummary object generated from the summary file"
291 def fset(self
, parsed
):
292 self
._parsed
= parsed
293 return property(**locals())
297 doc
= "The title of this Feed (as defined by user)"
303 def fset(self
, title
):
304 if self
._title
!= title
:
307 return property(**locals())
311 doc
= "A tuple of location, username, password"
313 return (self
._location
, self
._username
, self
._password
)
314 def fset(self
, (location
,username
,password
)):
315 self
._location
= location
316 self
._username
= username
317 self
._password
= password
319 return property(**locals())
325 return self
._location
326 def fset(self
, location
):
327 if self
._location
!= location
:
328 self
._location
= location
330 return property(**locals())
337 if self
._channel
_title
:
338 text
= self
._channel
_title
341 changed
= self
._channel
_title
!= t
342 self
._channel
_title
= t
345 return property(**locals())
348 def channel_description():
352 if self
._channel
_description
:
353 text
= self
._channel
_description
356 changed
= self
._channel
_description
!= t
357 self
._channel
_description
= t
360 return property(**locals())
366 return self
._channel
_link
368 changed
= self
._channel
_link
!= t
369 self
._channel
_link
= t
372 return property(**locals())
375 def channel_copyright():
378 return self
._channel
_copyright
380 changed
= self
._channel
_copyright
!= t
381 self
._channel
_copyright
= t
384 return property(**locals())
387 def number_of_items_stored():
390 return self
._items
_stored
391 def fset(self
, num
=None):
392 if self
._items
_stored
!= num
:
393 self
._items
_stored
= num
394 return property(**locals())
397 def poll_frequency():
400 return self
._poll
_freq
401 def fset(self
, freq
):
402 if self
._poll
_freq
!= freq
:
403 self
._poll
_freq
= freq
404 return property(**locals())
410 return self
._last
_poll
411 def fset(self
, time
):
412 if self
._last
_poll
!= time
:
413 self
._last
_poll
= time
414 return property(**locals())
417 def number_of_unread():
418 doc
= "the number of unread items for this feed"
420 return self
._n
_items
_unread
422 self
._n
_items
_unread
= n
424 return property(**locals())
431 def fset(self
, error
):
432 if self
._error
!= error
:
435 return property(**locals())
438 def process_status():
441 return self
._process
_status
442 def fset(self
, status
):
443 if status
!= self
._process
_status
:
444 self
._process
_status
= status
446 return property(**locals())
453 def fset(self
, parent
):
454 self
._parent
= parent
455 return property(**locals())
458 def next_refresh(self
):
459 """ return the feed's next refresh (time)"""
461 if self
._poll
_freq
== self
.DEFAULT
:
462 increment
= self
.config
.poll_frequency
464 increment
= self
._poll
_freq
466 nr
= self
.last_poll
+ increment
471 for f
, default
in self
.__save
_fields
:
472 fl
[f
] = self
.__getattribute
__(f
)
475 def undump(self
, dict):
476 for f
, default
in self
.__save
_fields
:
477 self
.__setattr
__(f
, dict.get(f
, default
))
481 self
.emit('poll-done')
483 def item_order_changed_cb(self
, config
):
484 self
.item_order_reverse
= config
.item_order
486 def item_stored_changed_cb(self
, config
):
487 self
.item_stored_num
= config
.number_of_items_stored
489 def get_cutpoint(self
):
490 cutpoint
= self
.number_of_items_stored
491 if cutpoint
== Feed
.DEFAULT
:
492 cutpoint
= self
.item_stored_num
496 def add_items(self
, items
):
497 if not self
._items
_loaded
: self
.load_contents()
498 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
499 reverse
=self
.item_order_reverse
)
500 self
._items
.set_number_of_entries(self
.get_cutpoint())
503 maxid
= reduce(max, [item
.id for item
in self
._items
.itervalues()])
504 newitems
= filter(lambda x
: x
not in self
._items
.itervalues(), items
)
505 for item
in newitems
:
509 item
.connect('changed', self
.item_changed_cb
)
510 self
._items
[item
.id] = item
## XXX pruned items don't get clean up properly
511 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
512 self
.emit('items-added', newitems
)
514 def restore_items(self
, items
):
515 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
516 reverse
=self
.item_order_reverse
)
517 cutpoint
= self
.get_cutpoint()
518 self
._items
.set_number_of_entries(cutpoint
)
520 for idx
, item
in enumerate(items
):
521 if not item
.sticky
and idx
>= cutpoint
:
523 olditems
.append(item
)
526 item
.connect('changed', self
.item_changed_cb
)
527 self
._items
[item
.id] = item
528 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
529 if olditems
: self
.emit('items-deleted', olditems
)
532 def item_changed_cb(self
, item
):
533 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
534 self
.emit('items-changed', [item
])
536 def delete_all_items(self
):
538 self
.emit('items-deleted', self
._items
.values())
542 if not self
._items
_loaded
:
544 return self
._items
.values()
546 def mark_items_as_read(self
, items
=None):
547 def mark(item
): item
.seen
= True
548 unread
= [item
for item
in self
._items
.itervalues() if not item
.seen
]
550 self
.emit('items-changed', unread
)
552 def load_contents(self
):
553 if self
._items
_loaded
:
555 itemstore
= ItemStore
.get_instance()
556 items
= itemstore
.read_feed_items(self
)
557 print "feed.load_contents->items: ", len(items
)
559 self
.restore_items(items
)
560 self
._items
_loaded
= True
561 return self
._items
_loaded
563 def unload_contents(self
):
564 if not self
._items
_loaded
:
567 self
._items
_loaded
= False
570 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
573 f
._location
= location
574 f
._id
= Config
.get_instance().next_feed_id_seq()
575 f
._username
= username
576 f
._password
= password
580 def create_empty_feed(klass
):
586 from collections
import deque
588 class FifoCache(object, UserDict
.DictMixin
):
589 ''' A mapping that remembers the last 'num_entries' items that were set '''
591 def __init__(self
, num_entries
, dct
=()):
592 self
.num_entries
= num_entries
597 return '%r(%r,%r)' % (
598 self
.__class
__.__name
__, self
.num_entries
, self
.dct
)
601 return self
.__class
__(self
.num_entries
, self
.dct
)
604 return list(self
.lst
)
606 def __getitem__(self
, key
):
609 def __setitem__(self
, key
, value
):
613 self
.remove_from_deque(lst
, key
)
616 if len(lst
) > self
.num_entries
:
617 del dct
[lst
.popleft()]
619 def __delitem__(self
, key
):
621 self
.remove_from_deque(self
.lst
, key
)
623 # a method explicitly defined only as an optimization
624 def __contains__(self
, item
):
625 return item
in self
.dct
627 has_key
= __contains__
629 def remove_from_deque(self
, d
, x
):
630 for i
, v
in enumerate(d
):
634 raise ValueError, '%r not in %r' % (x
,d
)
636 def set_number_of_entries(self
, num
):
637 self
.num_entries
= num
638 while len(self
.lst
) > num
:
639 del self
.dct
[self
.lst
.popleft()]
641 PSEUDO_ALL_KEY
= 'ALL'
642 PSEUDO_UNCATEGORIZED_KEY
= 'UNCATEGORIZED'
644 class FeedCategoryList(gobject
.GObject
):
647 'added' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,)),
648 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,)),
649 'category-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
650 'pseudo-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
654 gobject
.GObject
.__init
__(self
)
655 # have to define this here so the titles can be translated
656 PSEUDO_TITLES
= {PSEUDO_ALL_KEY
: _('All'),
657 PSEUDO_UNCATEGORIZED_KEY
: _('Feeds')}
658 self
._all
_category
= PseudoCategory(PSEUDO_TITLES
[PSEUDO_ALL_KEY
],
660 self
._un
_category
= PseudoCategory(
661 PSEUDO_TITLES
[PSEUDO_UNCATEGORIZED_KEY
], PSEUDO_UNCATEGORIZED_KEY
)
662 self
._user
_categories
= []
663 self
._pseudo
_categories
= (self
._all
_category
, self
._un
_category
)
664 self
._loading
= False
665 self
._feedlist
= get_feedlist_instance()
668 """Loads categories from config.
669 Data format: [[{'title': '', 'subscription': {}, 'pseudo': pseudo key},
670 {'id', feed_id1, 'from_subscription': bool} ...], ...]
672 cats
= Config
.get_instance().categories
or []
677 pseudo_key
= head
.get('pseudo', None)
678 if pseudo_key
== PSEUDO_ALL_KEY
:
679 fc
= self
.all_category
680 elif pseudo_key
== PSEUDO_UNCATEGORIZED_KEY
:
681 fc
= self
.un_category
683 fc
= FeedCategory(head
['title'])
684 sub
= head
.get('subscription', None)
686 fc
.subscription
= undump_subscription(sub
)
688 # backwards compatibility for stuff saved with versions <= 0.23
694 from_sub
= f
['from_subscription']
695 feed
= self
._feedlist
.get_feed_with_id(fid
)
696 if feed
and not pseudo_key
: # we deal with pseudos later
698 error
.log("%s (%d) was already in %s, skipping" % (str(feed
), fid
, str(fc
)))
700 fc
.append_feed(feed
, from_sub
)
701 categorized
[feed
] = True
702 # User categories: connect pseudos later
704 fc
.connect('changed', self
.category_changed
)
705 self
._user
_categories
.append(fc
)
706 # just in case we've missed any feeds, go through the list
707 # and add to the pseudocategories. cache the feed list of all_category
708 # so we don't get a function call (and a list comprehension loop
709 # inside it) on each feed. it should be ok here, there are no
710 # duplicates in feedlist. right?
711 pseudos_changed
= False
712 all_feeds
= self
.all_category
.feeds
713 for f
in self
._feedlist
:
714 if f
not in all_feeds
:
715 self
.all_category
.append_feed(f
, False)
716 pseudos_changed
= True
717 uf
= categorized
.get(f
, None)
719 self
.un_category
.append_feed(f
, False)
720 pseudos_changed
= True
723 for cat
in self
.pseudo_categories
:
724 cat
.connect('changed', self
.pseudo_category_changed
)
727 Config
.get_instance().categories
= [
728 cat
.dump() for cat
in self
]
730 def pseudo_category_changed(self
, category
, *args
):
732 self
.emit('pseudo-changed')
734 def category_changed(self
, signal
):
735 if signal
.feed
is not None:
737 for cat
in self
.user_categories
:
738 if signal
.feed
in cat
.feeds
:
739 uncategorized
= False
742 self
.un_category
.append_feed(signal
.feed
, False)
745 self
.un_category
.remove_feed(signal
.feed
)
749 self
.emit('category-changed')
751 def remove_feed(self
, feed
):
755 del self
._feedlist
[feed
]
760 def append_feed(self
, feed
, category
=None, index
=None):
761 self
._feedlist
.append(category
, feed
)
762 if category
and category
not in self
.pseudo_categories
:
764 category
.insert_feed(index
, feed
, False)
766 category
.append_feed(feed
, False)
768 print "uncategory append"
769 self
.un_category
.append_feed(feed
, False)
770 print "all category append"
771 self
.all_category
.append_feed(feed
, False)
773 def import_feeds(self
, feeds
, category
=None, from_sub
=False):
774 self
._feedlist
.extend(category
, feeds
)
775 if category
and category
not in self
.pseudo_categories
:
776 category
.extend_feed(feeds
, from_sub
)
778 print 'feed imported ', feeds
779 self
.un_category
.extend_feed(feeds
, from_sub
)
780 self
.all_category
.extend_feed(feeds
, from_sub
)
783 def user_categories(self
):
784 return self
._user
_categories
787 def pseudo_categories(self
):
788 return self
._pseudo
_categories
791 def all_categories(self
):
792 return self
.pseudo_categories
+ tuple(self
.user_categories
)
795 def all_category(self
):
796 return self
._all
_category
799 def un_category(self
):
800 return self
._un
_category
802 class CategoryIterator
:
803 def __init__(self
, fclist
):
804 self
._fclist
= fclist
813 uclen
= len(self
._fclist
.user_categories
)
815 return self
._fclist
.user_categories
[i
]
816 elif i
< uclen
+ len(self
._fclist
.pseudo_categories
):
817 return self
._fclist
.pseudo_categories
[i
- uclen
]
826 return self
.CategoryIterator(self
)
828 def add_category(self
, category
):
829 category
.connect('changed', self
.category_changed
)
830 self
._user
_categories
.append(category
)
831 auxlist
= [(x
.title
.lower(),x
) for x
in self
._user
_categories
]
833 self
._user
_categories
= [x
[1] for x
in auxlist
]
835 self
.emit('added', category
)
837 def remove_category(self
, category
):
838 for feed
in category
.feeds
:
839 category
.remove_feed(feed
)
840 self
._user
_categories
.remove(category
)
842 self
.emit('deleted', category
)
844 # It might be good to have a superclass FeedCategorySubscription or something
845 # so we could support different formats. However, I don't know of any other
846 # relevant format used for this purpose, so that can be done later if needed.
847 # Of course, they could also just implement the same interface.
848 class OPMLCategorySubscription(gobject
.GObject
):
852 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
853 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
856 def __init__(self
, location
=None):
857 gobject
.GObject
.__init
__(self
)
858 self
._location
= location
859 self
._username
= None
860 self
._password
= None
861 self
._contents
= None
862 self
._frequency
= OPMLCategorySubscription
.REFRESH_DEFAULT
870 return self
._location
871 def fset(self
, location
):
872 self
._location
= location
874 return property(**locals())
880 return self
._username
881 def fset(self
, username
):
882 self
._username
= username
884 return property(**locals())
890 return self
._password
892 self
._password
= password
894 return property(**locals())
900 return self
._frequency
901 def fset(self
, freq
):
902 self
._frequency
= freq
904 return property(**locals())
910 return self
._last
_poll
911 def fset(self
, last_poll
):
912 self
._last
_poll
= last_poll
914 return property(**locals())
921 def fset(self
, error
):
924 return property(**locals())
926 def parse(self
, data
):
927 datastream
= StringIO(data
)
928 entries
= OPMLImport
.read(datastream
)
929 contents
= [(e
.url
, e
.text
) for e
in entries
]
930 updated
= contents
== self
._contents
931 self
._contents
= contents
938 return self
._contents
941 def undump(klass
, dictionary
):
943 sub
.location
= dictionary
.get('location')
944 sub
.username
= dictionary
.get('username')
945 sub
.password
= dictionary
.get('password')
946 sub
.frequency
= dictionary
.get(
947 'frequency', OPMLCategorySubscription
.REFRESH_DEFAULT
)
948 sub
.last_poll
= dictionary
.get('last_poll', 0)
949 sub
.error
= dictionary
.get('error')
953 return {'type': 'opml',
954 'location': self
.location
,
955 'username': self
.username
,
956 'password': self
.password
,
957 'frequency': self
.frequency
,
958 'last_poll': self
.last_poll
,
961 def undump_subscription(dictionary
):
963 if dictionary
.get('type') == 'opml':
964 return OPMLCategorySubscription
.undump(dictionary
)
966 error
.log("exception while undumping subscription: " + str(e
))
969 class CategoryMember(object):
970 def __init__(self
, feed
=None, from_sub
=False):
972 self
._from
_subscription
= from_sub
979 def fset(self
, feed
):
981 return property(**locals())
984 def from_subscription():
987 return self
._from
_subscription
989 self
._from
_subscription
= p
990 return property(**locals())
992 class FeedCategory(gobject
.GObject
):
995 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,()),
996 'feed-added':(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
997 (gobject
.TYPE_PYOBJECT
,)),
998 'feed-removed':(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
999 (gobject
.TYPE_PYOBJECT
,))
1002 def __init__(self
, title
=""):
1003 gobject
.GObject
.__init
__(self
)
1006 self
._subscription
= None
1013 def fset(self
, title
):
1015 self
.emit('changed')
1016 return property(**locals())
1022 return self
._subscription
1023 def fset(self
, sub
):
1024 self
._subscription
= sub
1025 self
._subscription
.connect('changed', self
._subscription
_changed
)
1026 self
._subscription
.connect('updated', self
._subscription
_contents
_updated
)
1027 self
.emit('changed')
1028 return property(**locals())
1030 def read_contents_from_subscription(self
):
1031 if self
.subscription
is None:
1033 subfeeds
= self
.subscription
.contents
1034 sfdict
= dict(subfeeds
)
1035 feedlist
= get_feedlist_instance()
1036 current
= dict([(feed
.location
, feed
) for feed
in self
.feeds
])
1037 allfeeds
= dict([(feed
.location
, feed
) for feed
in feedlist
])
1038 common
, toadd
, toremove
= utils
.listdiff(sfdict
.keys(), current
.keys())
1039 existing
, nonexisting
, ignore
= utils
.listdiff(
1040 toadd
, allfeeds
.keys())
1042 newfeeds
= [Feed
.create_new_feed(sfdict
[f
], f
) for f
in nonexisting
]
1043 feedlist
.extend(self
.feedlist
, newfeeds
, from_sub
=True) # will call extend_feed
1044 self
.extend_feed([allfeeds
[f
] for f
in existing
], True)
1047 index
= self
.index_feed(allfeeds
[f
])
1048 member
= self
.feedlist
[index
]
1049 if member
.from_subscription
:
1050 self
.remove_feed(allfeeds
[f
])
1053 def _subscription_changed(self
, *args
):
1054 self
.emit('changed')
1056 def _subscription_contents_updated(self
, *args
):
1057 self
.read_contents_from_subscription()
1060 return "FeedCategory %s" % self
.title
1063 return hash(id(self
))
1065 def append_feed(self
, value
, from_sub
):
1066 self
.feedlist
.append(CategoryMember(value
, from_sub
))
1067 self
.emit('feed-added', value
)
1069 def extend_feed(self
, values
, from_sub
):
1070 self
.feedlist
.extend([CategoryMember(v
, from_sub
) for v
in values
])
1071 self
.emit('changed')
1073 def insert_feed(self
, index
, value
, from_sub
):
1074 self
.feedlist
.insert(index
, CategoryMember(value
, from_sub
))
1075 self
.emit('feed-added', value
)
1077 def remove(self
, value
):
1078 self
.feedlist
.remove(value
)
1079 self
.emit('feed-removed', value
.feed
)
1081 def remove_feed(self
, value
):
1082 for index
, member
in enumerate(self
.feedlist
):
1083 if member
.feed
is value
:
1084 del self
.feedlist
[index
]
1087 raise ValueError(value
)
1088 self
.emit('feed-removed', value
)
1091 self
.feedlist
.reverse()
1092 self
.emit('changed') # reverse=True))
1094 def index_feed(self
, value
):
1095 for index
, f
in enumerate(self
.feedlist
):
1096 if self
.feedlist
[index
].feed
is value
:
1098 raise ValueError(value
)
1100 def _sort_dsu(self
, seq
):
1101 aux_list
= [(x
.feed
.title
.lower(), x
) for x
in seq
]
1102 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
1103 return [x
[1] for x
in aux_list
]
1105 def sort(self
, indices
=None):
1106 if not indices
or len(indices
) == 1:
1107 self
.feedlist
[:] = self
._sort
_dsu
(self
.feedlist
)
1109 items
= self
._sort
_dsu
(indices
)
1110 for i
,x
in enumerate(items
):
1111 list.__setitem
__(self
.feedlist
, indices
[i
], items
[i
])
1112 self
.emit('changed')
1114 def move_feed(self
, source
, target
):
1117 if target
== source
:
1121 self
.feedlist
.insert(target
, t
)
1122 self
.emit('changed')
1125 head
= {'title': self
.title
}
1126 if self
.subscription
is not None:
1127 head
['subscription'] = self
.subscription
.dump()
1129 {'id': f
.feed
.id, 'from_subscription': f
.from_subscription
}
1130 for f
in self
.feedlist
]
1134 return [f
.feed
for f
in self
.feedlist
]
1136 def __eq__(self
, ob
):
1137 if isinstance(ob
, types
.NoneType
):
1139 elif isinstance(ob
, FeedCategory
):
1140 return self
.title
== ob
.title
and list.__eq
__(self
.feedlist
, ob
)
1142 raise NotImplementedError
1144 def __contains__(self
, item
):
1145 error
.log("warning, should probably be querying the feeds property instead?")
1146 return list.__contains
__(self
.feedlist
, item
)
1148 class PseudoCategory(FeedCategory
):
1149 def __init__(self
, title
="", key
=None):
1150 if key
not in (PSEUDO_ALL_KEY
, PSEUDO_UNCATEGORIZED_KEY
):
1151 raise ValueError, "Invalid key"
1152 FeedCategory
.__init
__(self
, title
)
1153 self
._pseudo
_key
= key
1156 return "PseudoCategory %s" % self
.title
1159 return [{'pseudo': self
._pseudo
_key
, 'title': ''}] + [
1160 {'id': f
.feed
.id, 'from_subscription': False} for f
in self
.feedlist
]
1162 def append_feed(self
, feed
, from_sub
):
1164 FeedCategory
.append_feed(self
, feed
, False)
1166 def insert_feed(self
, index
, feed
, from_sub
):
1168 FeedCategory
.insert_feed(self
, index
, feed
, False)
1172 def get_category_list():
1175 fclist
= FeedCategoryList()
1178 category_list
= get_category_list()