1 import locale
, operator
6 class FeedList(gobject
.GObject
):
9 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
10 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
11 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, gobject
.TYPE_INT
,)),
12 'imported' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
13 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
14 gobject
.TYPE_BOOLEAN
,)),
15 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,))
18 def __init__(self
, init_seq
= []):
19 gobject
.GObject
.__init
__(self
)
24 return iter(self
.feedlist
)
27 def _load(feedlist
, parent
):
29 if isinstance(df
, list):
32 f
= Feed
.create_empty_feed()
34 self
.append(parent
, f
)
36 feedlist
= Config
.get_instance().feeds
42 def connect_signals(self
, ob
):
43 ob
.connect('changed', self
.feed_detail_changed
)
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 self
.connect_signals(value
)
59 self
.save_feeds_and_notify(True)
61 def extend(self
, parent
, values
, from_sub
=False):
62 list.extend(self
.feedlist
, values
)
65 self
.connect_signals(f
)
67 self
.emit('imported', values
, parent
, from_sub
)
69 def append(self
, parent
, value
):
70 self
.feedlist
.append(value
)
72 self
.connect_signals(value
)
74 self
.emit('updated', value
, parent
, -1)
76 def insert(self
, index
, parent
, value
):
77 self
.feedlist
.insert(index
, value
)
79 self
.connect_signals(value
)
81 self
.emit('updated', value
, parent
, index
)
83 def index(self
, feed
):
84 return self
.feedlist
.index(feed
)
86 def reorder(self
, move
, delta
):
92 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
95 k
[m
+ delta
], k
[m
] = k
[m
], k
[m
+ delta
]
96 for i
in range(len(k
)):
97 list.__setitem
__(self
.feedlist
, i
, k
[i
])
101 def __delitem__(self
, value
):
102 feed
= self
.feedlist
[value
]
103 list.__delitem
__(self
.feedlist
, value
)
104 feed
.delete_all_items()
106 self
.emit('deleted', feed
)
108 def save_feeds(self
):
109 if not self
._loading
:
110 config
= Config
.get_instance()
111 config
.feeds
= [f
.dump() for f
in self
.feedlist
]
114 def feed_detail_changed(self
, feed
):
116 self
.emit('changed') # XXXX send the feed as well?
118 def _sort_dsu(self
, seq
):
119 aux_list
= [(x
.title
, x
) for x
in seq
]
120 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
121 return [x
[1] for x
in aux_list
]
123 def sort(self
, indices
= None):
124 if not indices
or len(indices
) == 1:
125 self
[:] = self
._sort
_dsu
(self
)
127 items
= self
._sort
_dsu
(indices
)
128 for i
,x
in enumerate(items
):
129 list.__setitem
__(self
, indices
[i
], items
[i
])
135 for item
in self
.feedlist
:
139 def get_feed_with_id(self
, id):
140 for f
in self
.flatten_list():
145 def flatten_list(self
, ob
=None):
150 if isinstance(o
, list):
151 l
= l
+ self
.flatten_list(o
)
156 feedlist_instance
= None
158 global feedlist_instance
159 if feedlist_instance
is None:
160 feedlist_instance
= FeedList()
161 return feedlist_instance
164 class IdleState(object):
165 ''' state for idle or normal operation '''
167 filename
= os
.path
.join(utils
.find_image_dir(), 'feed.png')
168 self
.icon
= gtk
.gdk
.pixbuf_new_from_file(filename
)
178 class PollingState(object):
179 ''' state when feed is polling '''
181 self
._icon
= gtk
.Image()
182 self
._icon
.set_from_stock(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
186 return self
._icon
.get_pixbuf()
192 class ErrorState(object):
193 ''' state when feed has errors '''
195 def __init__(self
, mesg
):
196 self
._icon
= gtk
.Image()
197 self
._icon
.set_from_stock(gtk
.STOCK_DIALOG_ERROR
, gtk
.ICON_SIZE_MENU
)
202 return self
._icon
.get_pixbuf()
208 class FeedAdapter(gobject
.GObject
):
209 ''' Adapter to a feed entity
211 FeedAdapter wraps a feedparser.FeedParserDict object and delegates the
212 getter to get values from the wrapped object. For attributes that can be
213 set, it's best to create concrete attributes for that instead of mutating
214 the wrapped object. This mutable attributes can then be saved into the
215 on-disk representation.
217 The main point of this exercise is so that we can access feedparser data
218 structure's attributes instead of remapping it and readding stuff
219 everytime feedparser updates its API, or when we suddenly decided we need
220 some or all of feedparser's api.
222 FeedAdapter's responsibilities:
224 - core data comes from the wrapped object, FeedParserDict
225 - ability to dump and load itself at least using config persistence
226 - ability to manage its own items
229 - ability to update the wrapped object
230 - ability to retain the number of items
231 - ability to set states (merge FeedDataRouter here):
234 - updating images, etc...
239 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
240 ('_password', ""),('_id', ""), ('_error', None),
241 ('_items_stored', DEFAULT
), ('_poll_freq', DEFAULT
),
242 ('_last_poll', 0), #('_n_items_unread',0),
243 ('name',''),('description',''),
244 ('author',''),('copyright',''),
245 ('channel_creator', ""), # deprecate these soon
246 ('_channel_description', ""), ('_channel_title', ""),
247 ('_channel_link', ""), ('_channel_copyright', "")
251 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
254 def __init__(self
, title
, **kwargs
):
255 gobject
.GObject
.__init
__(self
)
256 self
._wrapped
_data
= kwargs
.get('data', None)
258 self
._location
= kwargs
.get('location',None)
259 self
._username
= kwargs
.get('username',None)
260 self
._password
= kwargs
.get('password',None)
261 self
._state
= kwargs
.get('state',None)
262 self
._id
= FeedAdapter
.DEFAULT
263 self
._items
_stored
= FeedAdapter
.DEFAULT
264 self
._poll
_freq
= FeedAdapter
.DEFAULT
265 self
._last
_poll
= FeedAdapter
.DEFAULT
266 self
._items
= FifoCache(num_entries
=FeedAdapter
.DEFAULT
)
271 return "Feed '%s' from %s" % (self
._title
, self
._location
)
273 def __getattr__(self
, key
):
275 return getattr(self
, key
)
276 except AttributeError:
277 return getattr(self
._wrapped
, key
)
279 def update(self
, parserDict
):
280 ''' updated the wrapped object '''
281 self
._wrapped
_data
= parserDict
283 def setState(self
, state
):
284 ''' sets the state of self '''
289 ''' gets the state of self '''
294 doc
= "The title of this Feed (as defined by user)"
300 def fset(self
, title
):
301 if self
._title
!= title
:
303 return property(**locals())
306 def number_of_items_stored():
307 doc
= 'the number of items to be stored for this feed'
309 return self
._items
_stored
310 def fset(self
, num
=None):
311 if self
._items
_stored
!= num
:
312 self
._items
_stored
= num
313 return property(**locals())
316 def poll_frequency():
317 doc
= 'the refresh frequency of this feed'
319 return self
._poll
_freq
320 def fset(self
, freq
):
321 if self
._poll
_freq
!= freq
:
322 self
._poll
_freq
= freq
323 return property(**locals())
327 doc
= 'the time this feed was last updated'
329 return self
._last
_poll
330 def fset(self
, time
):
331 if self
._last
_poll
!= time
:
332 self
._last
_poll
= time
333 return property(**locals())
337 doc
= 'error condition'
339 print 'DEPRECATED: use feed.state.mesg instead'
341 def fset(self
, error
):
342 print 'DEPRECATED: use feed.state.mesg instead'
343 if self
._error
!= error
:
345 return property(**locals())
349 doc
= 'the parent of this feed'
352 def fset(self
, parent
):
353 self
._parent
= parent
354 return property(**locals())
357 ''' dumps the attributes of this feed for storing '''
359 for f
, default
in self
.__save
_fields
:
360 fl
[f
] = self
.__getattribute
__(f
)
363 def undump(self
, dict):
364 ''' sets the values of dict for this feed'''
365 for f
, default
in self
.__save
_fields
:
366 self
.__setattr
__(f
, dict.get(f
, default
))
370 class Feed(gobject
.GObject
):
371 "A Feed object stores information set by user about a RSS feed."
377 __slots__
= ('_title', '_location', '_username', '_password', '_parsed',
378 '__save_fields', '_items', '_slots',
379 '_id', '_channel_description',
380 '_channel_title', '_channel_link', '_channel_copyright',
381 'channel_lbd', 'channel_editor', 'channel_webmaster',
382 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
383 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
385 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
386 ('_password', ""), ('_id', ""),
387 ('_channel_description', ""), ('_channel_title', ""),
388 ('_channel_link', ""), ('_channel_copyright', ""),
389 ('channel_creator', ""), ('_error', None),
390 ('_items_stored', DEFAULT
),
391 ('_poll_freq', DEFAULT
),
393 ('_n_items_unread',0))
397 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
398 'poll-done' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
399 'items-added' :(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
400 (gobject
.TYPE_PYOBJECT
,)),
401 'items-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
402 (gobject
.TYPE_PYOBJECT
,)),
403 'items-deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
404 (gobject
.TYPE_PYOBJECT
,))
408 # use one of the factory functions below instead of this directly
409 def __init__(self
, title
="", location
="", username
="", password
=""):
410 import FeedDataRouter
411 gobject
.GObject
.__init
__(self
)
413 self
._channel
_description
= ""
414 self
._channel
_title
= ""
415 self
._channel
_link
= ""
416 self
._channel
_copyright
= ""
417 self
.channel_lbd
= None
418 self
.channel_editor
= ""
419 self
.channel_webmaster
= ""
420 self
.channel_creator
= ""
421 self
._location
= location
422 self
._username
= username
423 self
._password
= password
426 self
._n
_items
_unread
= 0
427 self
._process
_status
= self
.STATUS_IDLE
428 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
430 self
._items
_stored
= Feed
.DEFAULT
431 self
._poll
_freq
= Feed
.DEFAULT
434 self
.config
= Config
.get_instance()
435 # XXX move this to subscriptions
436 self
.config
.connect('item-order-changed', self
.item_order_changed_cb
)
437 self
.config
.connect('item-stored-changed', self
.item_stored_changed_cb
)
438 self
.item_order_reverse
= self
.config
.item_order
439 self
.item_stored_num
= self
.config
.number_of_items_stored
440 self
._items
= FifoCache(num_entries
=Feed
.DEFAULT
)
441 self
._items
_loaded
= False
445 return "Feed '%s' from %s" % (self
._title
, self
._location
)
453 doc
= "A ParsedSummary object generated from the summary file"
456 def fset(self
, parsed
):
457 self
._parsed
= parsed
458 return property(**locals())
462 doc
= "The title of this Feed (as defined by user)"
468 def fset(self
, title
):
469 if self
._title
!= title
:
472 return property(**locals())
476 doc
= "A tuple of location, username, password"
478 return (self
._location
, self
._username
, self
._password
)
479 def fset(self
, (location
,username
,password
)):
480 self
._location
= location
481 self
._username
= username
482 self
._password
= password
484 return property(**locals())
490 return self
._location
491 def fset(self
, location
):
492 if self
._location
!= location
:
493 self
._location
= location
495 return property(**locals())
502 if self
._channel
_title
:
503 text
= self
._channel
_title
506 changed
= self
._channel
_title
!= t
507 self
._channel
_title
= t
510 return property(**locals())
513 def channel_description():
517 if self
._channel
_description
:
518 text
= self
._channel
_description
521 changed
= self
._channel
_description
!= t
522 self
._channel
_description
= t
525 return property(**locals())
531 return self
._channel
_link
533 changed
= self
._channel
_link
!= t
534 self
._channel
_link
= t
537 return property(**locals())
540 def channel_copyright():
543 return self
._channel
_copyright
545 changed
= self
._channel
_copyright
!= t
546 self
._channel
_copyright
= t
549 return property(**locals())
552 def number_of_items_stored():
555 return self
._items
_stored
556 def fset(self
, num
=None):
557 if self
._items
_stored
!= num
:
558 self
._items
_stored
= num
559 return property(**locals())
562 def poll_frequency():
565 return self
._poll
_freq
566 def fset(self
, freq
):
567 if self
._poll
_freq
!= freq
:
568 self
._poll
_freq
= freq
569 return property(**locals())
575 return self
._last
_poll
576 def fset(self
, time
):
577 if self
._last
_poll
!= time
:
578 self
._last
_poll
= time
579 return property(**locals())
582 def number_of_unread():
583 doc
= "the number of unread items for this feed"
585 return self
._n
_items
_unread
587 self
._n
_items
_unread
= n
589 return property(**locals())
596 def fset(self
, error
):
597 if self
._error
!= error
:
600 return property(**locals())
603 def process_status():
606 return self
._process
_status
607 def fset(self
, status
):
608 if status
!= self
._process
_status
:
609 self
._process
_status
= status
611 return property(**locals())
618 def fset(self
, parent
):
619 self
._parent
= parent
620 return property(**locals())
623 def next_refresh(self
):
624 """ return the feed's next refresh (time)"""
626 if self
._poll
_freq
== self
.DEFAULT
:
627 increment
= self
.config
.poll_frequency
629 increment
= self
._poll
_freq
631 nr
= self
.last_poll
+ increment
636 for f
, default
in self
.__save
_fields
:
637 fl
[f
] = self
.__getattribute
__(f
)
640 def undump(self
, dict):
641 for f
, default
in self
.__save
_fields
:
642 self
.__setattr
__(f
, dict.get(f
, default
))
646 self
.emit('poll-done')
648 def item_order_changed_cb(self
, config
):
649 self
.item_order_reverse
= config
.item_order
651 def item_stored_changed_cb(self
, config
):
652 self
.item_stored_num
= config
.number_of_items_stored
654 def get_cutpoint(self
):
655 cutpoint
= self
.number_of_items_stored
656 if cutpoint
== Feed
.DEFAULT
:
657 cutpoint
= self
.item_stored_num
661 def add_items(self
, items
):
662 if not self
._items
_loaded
: self
.load_contents()
663 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
664 reverse
=self
.item_order_reverse
)
665 self
._items
.set_number_of_entries(self
.get_cutpoint())
668 maxid
= reduce(max, [item
.id for item
in self
._items
.itervalues()])
669 newitems
= filter(lambda x
: x
not in self
._items
.itervalues(), items
)
670 for item
in newitems
:
674 item
.connect('changed', self
.item_changed_cb
)
675 self
._items
[item
.id] = item
## XXX pruned items don't get clean up properly
676 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
677 self
.emit('items-added', newitems
)
679 def restore_items(self
, items
):
680 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
681 reverse
=self
.item_order_reverse
)
682 cutpoint
= self
.get_cutpoint()
683 self
._items
.set_number_of_entries(cutpoint
)
685 for idx
, item
in enumerate(items
):
686 if not item
.sticky
and idx
>= cutpoint
:
688 olditems
.append(item
)
691 item
.connect('changed', self
.item_changed_cb
)
692 self
._items
[item
.id] = item
693 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
694 if olditems
: self
.emit('items-deleted', olditems
)
697 def item_changed_cb(self
, item
):
698 self
.number_of_unread
= len(filter(lambda x
: not x
.seen
, self
._items
.itervalues()))
699 self
.emit('items-changed', [item
])
701 def delete_all_items(self
):
703 self
.emit('items-deleted', self
._items
.values())
707 if not self
._items
_loaded
:
709 return self
._items
.values()
711 def mark_items_as_read(self
, items
=None):
712 def mark(item
): item
.seen
= True
713 unread
= [item
for item
in self
._items
.itervalues() if not item
.seen
]
715 self
.emit('items-changed', unread
)
717 def load_contents(self
):
718 if self
._items
_loaded
:
720 itemstore
= ItemStore
.get_instance()
721 items
= itemstore
.read_feed_items(self
)
722 print "feed.load_contents->items: ", len(items
)
724 self
.restore_items(items
)
725 self
._items
_loaded
= True
726 return self
._items
_loaded
728 def unload_contents(self
):
729 if not self
._items
_loaded
:
732 self
._items
_loaded
= False
735 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
738 f
._location
= location
739 f
._id
= Config
.get_instance().next_feed_id_seq()
740 f
._username
= username
741 f
._password
= password
745 def create_empty_feed(klass
):
751 from collections
import deque
753 class FifoCache(object, UserDict
.DictMixin
):
754 ''' A mapping that remembers the last 'num_entries' items that were set '''
756 def __init__(self
, num_entries
, dct
=()):
757 self
.num_entries
= num_entries
762 return '%r(%r,%r)' % (
763 self
.__class
__.__name
__, self
.num_entries
, self
.dct
)
766 return self
.__class
__(self
.num_entries
, self
.dct
)
769 return list(self
.lst
)
771 def __getitem__(self
, key
):
774 def __setitem__(self
, key
, value
):
778 self
.remove_from_deque(lst
, key
)
781 if len(lst
) > self
.num_entries
:
782 del dct
[lst
.popleft()]
784 def __delitem__(self
, key
):
786 self
.remove_from_deque(self
.lst
, key
)
788 # a method explicitly defined only as an optimization
789 def __contains__(self
, item
):
790 return item
in self
.dct
792 has_key
= __contains__
794 def remove_from_deque(self
, d
, x
):
795 for i
, v
in enumerate(d
):
799 raise ValueError, '%r not in %r' % (x
,d
)
801 def set_number_of_entries(self
, num
):
802 self
.num_entries
= num
803 while len(self
.lst
) > num
:
804 del self
.dct
[self
.lst
.popleft()]