1 import locale
, operator
7 class FeedList(gobject
.GObject
):
10 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
11 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
12 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, gobject
.TYPE_INT
,)),
13 'imported' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
14 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
15 gobject
.TYPE_BOOLEAN
,)),
16 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,))
19 def __init__(self
, init_seq
= []):
20 gobject
.GObject
.__init
__(self
)
25 return iter(self
.feedlist
)
28 def _load(feedlist
, parent
):
30 if isinstance(df
, list):
33 f
= Feed
.create_empty_feed()
35 self
.append(parent
, f
)
37 feedlist
= Config
.get_instance().feeds
43 def connect_signals(self
, ob
):
44 ob
.connect('changed', self
.feed_detail_changed
)
46 # these signals are forwarded so that listeners who just want to
47 # listen for a specific event regardless of what feed it came from can
48 # just connect to this feedlist instead of connecting to the
50 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
51 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
52 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
53 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
55 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
57 def __setitem__(self
, key
, value
):
58 self
.feedlist
.__setitem
__(key
, value
)
59 self
.connect_signals(value
)
60 self
.save_feeds_and_notify(True)
62 def extend(self
, parent
, values
, from_sub
=False):
63 list.extend(self
.feedlist
, values
)
66 self
.connect_signals(f
)
68 self
.emit('imported', values
, parent
, from_sub
)
70 def append(self
, parent
, value
):
71 self
.feedlist
.append(value
)
73 self
.connect_signals(value
)
75 self
.emit('updated', value
, parent
, -1)
77 def insert(self
, index
, parent
, value
):
78 self
.feedlist
.insert(index
, value
)
80 self
.connect_signals(value
)
82 self
.emit('updated', value
, parent
, index
)
84 def index(self
, feed
):
85 return self
.feedlist
.index(feed
)
87 def reorder(self
, move
, delta
):
93 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
96 k
[m
+ delta
], k
[m
] = k
[m
], k
[m
+ delta
]
97 for i
in range(len(k
)):
98 list.__setitem
__(self
.feedlist
, i
, k
[i
])
102 def __delitem__(self
, value
):
103 feed
= self
.feedlist
[value
]
104 list.__delitem
__(self
.feedlist
, value
)
105 feed
.delete_all_items()
107 self
.emit('deleted', feed
)
109 def save_feeds(self
):
110 if not self
._loading
:
111 config
= Config
.get_instance()
112 config
.feeds
= [f
.dump() for f
in self
.feedlist
]
115 def feed_detail_changed(self
, feed
):
117 self
.emit('changed') # XXXX send the feed as well?
119 def _sort_dsu(self
, seq
):
120 aux_list
= [(x
.title
, x
) for x
in seq
]
121 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
122 return [x
[1] for x
in aux_list
]
124 def sort(self
, indices
= None):
125 if not indices
or len(indices
) == 1:
126 self
[:] = self
._sort
_dsu
(self
)
128 items
= self
._sort
_dsu
(indices
)
129 for i
,x
in enumerate(items
):
130 list.__setitem
__(self
, indices
[i
], items
[i
])
136 for item
in self
.feedlist
:
140 def get_feed_with_id(self
, id):
141 for f
in self
.flatten_list():
146 def flatten_list(self
, ob
=None):
151 if isinstance(o
, list):
152 l
= l
+ self
.flatten_list(o
)
157 feedlist_instance
= None
160 global feedlist_instance
161 if feedlist_instance
is None:
162 feedlist_instance
= FeedList()
163 return feedlist_instance
165 class Feed(gobject
.GObject
):
166 "A Feed object stores information set by user about a RSS feed."
172 __slots__
= ('_title', '_location', '_username', '_password', '_parsed',
173 '__save_fields', '_items', '_slots',
174 '_id', '_channel_description',
175 '_channel_title', '_channel_link', '_channel_copyright',
176 'channel_lbd', 'channel_editor', 'channel_webmaster',
177 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
178 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
180 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
181 ('_password', ""), ('_id', ""),
182 ('_channel_description', ""), ('_channel_title', ""),
183 ('_channel_link', ""), ('_channel_copyright', ""),
184 ('channel_creator', ""), ('_error', None),
185 ('_items_stored', DEFAULT
),
186 ('_poll_freq', DEFAULT
),
188 ('_n_items_unread',0))
192 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
193 'poll-done' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
194 'items-added' :(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
195 (gobject
.TYPE_PYOBJECT
,)),
196 'items-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
197 (gobject
.TYPE_PYOBJECT
,)),
198 'items-deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
199 (gobject
.TYPE_PYOBJECT
,))
203 # use one of the factory functions below instead of this directly
204 def __init__(self
, title
="", location
="", username
="", password
=""):
205 gobject
.GObject
.__init
__(self
)
207 self
._channel
_description
= ""
208 self
._channel
_title
= ""
209 self
._channel
_link
= ""
210 self
._channel
_copyright
= ""
211 self
.channel_lbd
= None
212 self
.channel_editor
= ""
213 self
.channel_webmaster
= ""
214 self
.channel_creator
= ""
215 self
._location
= location
216 self
._username
= username
217 self
._password
= password
220 self
._n
_items
_unread
= 0
221 self
._process
_status
= self
.STATUS_IDLE
222 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
224 self
._items
_stored
= Feed
.DEFAULT
225 self
._poll
_freq
= Feed
.DEFAULT
228 self
.config
= Config
.get_instance()
229 self
.config
.connect('item-order-changed', self
.item_order_changed_cb
)
230 self
.config
.connect('item-stored-changed', self
.item_stored_changed_cb
)
231 self
.item_order_reverse
= self
.config
.item_order
232 self
.item_stored_num
= self
.config
.number_of_items_stored
233 self
._items
= FifoCache(num_entries
=Feed
.DEFAULT
)
234 self
._items
_loaded
= False
235 self
._changes
_queue
= []
239 return "Feed '%s' from %s" % (self
._title
, self
._location
)
247 doc
= "A ParsedSummary object generated from the summary file"
250 def fset(self
, parsed
):
251 self
._parsed
= parsed
252 return property(**locals())
256 doc
= "The title of this Feed (as defined by user)"
262 def fset(self
, title
):
263 if self
._title
!= title
:
266 return property(**locals())
270 doc
= "A tuple of location, username, password"
272 return (self
._location
, self
._username
, self
._password
)
273 def fset(self
, (location
,username
,password
)):
274 self
._location
= location
275 self
._username
= username
276 self
._password
= password
278 return property(**locals())
284 return self
._location
285 def fset(self
, location
):
286 if self
._location
!= location
:
287 self
._location
= location
289 return property(**locals())
296 if self
._channel
_title
:
297 text
= self
._channel
_title
300 changed
= self
._channel
_title
!= t
301 self
._channel
_title
= t
304 return property(**locals())
307 def channel_description():
311 if self
._channel
_description
:
312 text
= self
._channel
_description
315 changed
= self
._channel
_description
!= t
316 self
._channel
_description
= t
319 return property(**locals())
325 return self
._channel
_link
327 changed
= self
._channel
_link
!= t
328 self
._channel
_link
= t
331 return property(**locals())
334 def channel_copyright():
337 return self
._channel
_copyright
339 changed
= self
._channel
_copyright
!= t
340 self
._channel
_copyright
= t
343 return property(**locals())
346 def number_of_items_stored():
349 return self
._items
_stored
350 def fset(self
, num
=None):
351 if self
._items
_stored
!= num
:
352 self
._items
_stored
= num
353 return property(**locals())
356 def poll_frequency():
359 return self
._poll
_freq
360 def fset(self
, freq
):
361 if self
._poll
_freq
!= freq
:
362 self
._poll
_freq
= freq
363 return property(**locals())
369 return self
._last
_poll
370 def fset(self
, time
):
371 if self
._last
_poll
!= time
:
372 self
._last
_poll
= time
373 return property(**locals())
376 def number_of_unread():
377 doc
= "the number of unread items for this feed"
379 return self
._n
_items
_unread
381 if self
._n
_items
_unread
:
382 self
._n
_items
_unread
= n
384 return property(**locals())
391 def fset(self
, error
):
392 if self
._error
!= error
:
395 return property(**locals())
398 def process_status():
401 return self
._process
_status
402 def fset(self
, status
):
403 if status
!= self
._process
_status
:
404 self
._process
_status
= status
406 return property(**locals())
413 def fset(self
, parent
):
414 self
._parent
= parent
415 return property(**locals())
418 def next_refresh(self
):
419 """ return the feed's next refresh (time)"""
421 if self
._poll
_freq
== self
.DEFAULT
:
422 increment
= self
.config
.poll_frequency
424 increment
= self
._poll
_freq
426 nr
= self
.last_poll
+ increment
431 for f
, default
in self
.__save
_fields
:
432 fl
[f
] = self
.__getattribute
__(f
)
435 def undump(self
, dict):
436 for f
, default
in self
.__save
_fields
:
437 self
.__setattr
__(f
, dict.get(f
, default
))
441 self
.emit('poll-done')
443 def item_order_changed_cb(self
, config
):
444 self
.item_order_reverse
= config
.item_order
446 def item_stored_changed_cb(self
, config
):
447 self
.item_stored_num
= config
.number_of_items_stored
449 def get_cutpoint(self
):
450 cutpoint
= self
.number_of_items_stored
451 if cutpoint
== Feed
.DEFAULT
:
452 cutpoint
= self
.item_stored_num
455 def add_items(self
, items
):
456 if not self
._items
_loaded
: self
.load_contents()
457 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
458 reverse
=self
.item_order_reverse
)
459 self
._items
.set_number_of_entries(self
.get_cutpoint())
462 maxid
= reduce(max, [item
.id for item
in self
._items
.itervalues()])
468 newitems
.append(item
)
469 item
.connect('changed', self
.item_changed_cb
)
470 self
._items
[item
.id] = item
471 self
.number_of_unread
= len([item
for item
in self
._items
.itervalues() if not item
.seen
])
472 self
.emit('items-added', newitems
)
474 def restore_items(self
, items
):
475 items
= sorted(items
, key
=operator
.attrgetter('pub_date'),
476 reverse
=self
.item_order_reverse
)
477 cutpoint
= self
.get_cutpoint()
478 self
._items
.set_number_of_entries(cutpoint
)
480 for idx
, item
in enumerate(items
):
481 if not item
.sticky
and idx
>= cutpoint
:
483 olditems
.append(item
)
486 item
.connect('changed', self
.item_changed_cb
)
487 self
._items
[item
.id] = item
488 self
.number_of_unread
= len([item
for item
in self
._items
.itervalues() if not item
.seen
])
489 print "\t items len: %d, olditems len: %d" % (len(items
), len(olditems
))
491 self
.emit('items-deleted', olditems
)
494 def item_changed_cb(self
, item
):
495 self
.number_of_unread
+= item
.seen
and -1 or 1
496 self
._changes
_queue
.append(item
)
497 self
.emit('items-changed', self
._changes
_queue
)
499 def delete_all_items(self
):
501 self
.emit('items-deleted', self
._items
.values())
505 if not self
._items
_loaded
:
507 return self
._items
.values()
509 def mark_items_as_read(self
, items
=None):
510 def mark(item
): item
.seen
= True
511 unread
= [item
for item
in self
._items
.itervalues() if not item
.seen
]
513 self
.emit('items-changed', unread
)
515 def load_contents(self
):
516 if self
._items
_loaded
:
518 itemstore
= ItemStore
.get_instance()
519 items
= itemstore
.read_feed_items(self
)
520 print "feed.load_contents->items: ", len(items
)
522 self
.restore_items(items
)
523 self
._items
_loaded
= True
524 return self
._items
_loaded
526 def unload_contents(self
):
527 if not self
._items
_loaded
:
530 self
._items
_loaded
= False
533 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
536 f
._location
= location
537 f
._id
= Config
.get_instance().next_feed_id_seq()
538 f
._username
= username
539 f
._password
= password
543 def create_empty_feed(klass
):
549 from collections
import deque
551 class FifoCache(object, UserDict
.DictMixin
):
552 ''' A mapping that remembers the last 'num_entries' items that were set '''
554 def __init__(self
, num_entries
, dct
=()):
555 self
.num_entries
= num_entries
560 return '%r(%r,%r)' % (
561 self
.__class
__.__name
__, self
.num_entries
, self
.dct
)
564 return self
.__class
__(self
.num_entries
, self
.dct
)
567 return list(self
.lst
)
569 def __getitem__(self
, key
):
572 def __setitem__(self
, key
, value
):
576 self
.remove_from_deque(lst
, key
)
579 if len(lst
) > self
.num_entries
:
580 del dct
[lst
.popleft()]
582 def __delitem__(self
, key
):
584 self
.remove_from_deque(self
.lst
, key
)
586 # a method explicitly defined only as an optimization
587 def __contains__(self
, item
):
588 return item
in self
.dct
590 has_key
= __contains__
592 def remove_from_deque(self
, d
, x
):
593 for i
, v
in enumerate(d
):
597 raise ValueError, '%r not in %r' % (x
,d
)
599 def set_number_of_entries(self
, num
):
600 self
.num_entries
= num
601 while len(self
.lst
) > num
:
602 del self
.dct
[self
.lst
.popleft()]