1 import locale
, operator
8 class FeedList(gobject
.GObject
):
11 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
12 'updated' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
13 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
, gobject
.TYPE_INT
,)),
14 'imported' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
15 (gobject
.TYPE_PYOBJECT
, gobject
.TYPE_PYOBJECT
,
16 gobject
.TYPE_BOOLEAN
,)),
17 'deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, (gobject
.TYPE_PYOBJECT
,))
20 def __init__(self
, init_seq
= []):
21 gobject
.GObject
.__init
__(self
)
26 return iter(self
.feedlist
)
29 def _load(feedlist
, parent
):
31 if isinstance(df
, list):
34 f
= Feed
.create_empty_feed()
36 self
.append(parent
, f
)
38 feedlist
= Config
.get_instance().feeds
44 def connect_signals(self
, ob
):
45 ob
.connect('changed', self
.feed_detail_changed
)
47 # these signals are forwarded so that listeners who just want to
48 # listen for a specific event regardless of what feed it came from can
49 # just connect to this feedlist instead of connecting to the
51 #ob.signal_connect(Event.AllItemsReadSignal, self._forward_signal)
52 #ob.signal_connect(Event.ItemReadSignal, self._forward_signal)
53 #ob.signal_connect(Event.ItemsAddedSignal, self._forward_signal)
54 #ob.signal_connect(Event.FeedPolledSignal, self._forward_signal)
55 #ob.signal_connect(Event.FeedStatusChangedSignal, self._forward_signal)
56 #ob.signal_connect(Event.FeedErrorStatusChangedSignal, self._forward_signal)
58 def __setitem__(self
, key
, value
):
59 self
.feedlist
.__setitem
__(key
, value
)
60 self
.connect_signals(value
)
61 self
.save_feeds_and_notify(True)
63 def extend(self
, parent
, values
, from_sub
=False):
64 list.extend(self
.feedlist
, values
)
67 self
.connect_signals(f
)
69 self
.emit('imported', values
, parent
, from_sub
)
71 def append(self
, parent
, value
):
72 self
.feedlist
.append(value
)
74 self
.connect_signals(value
)
76 self
.emit('updated', value
, parent
, -1)
78 def insert(self
, index
, parent
, value
):
79 self
.feedlist
.insert(index
, value
)
81 self
.connect_signals(value
)
83 self
.emit('updated', value
, parent
, index
)
85 def index(self
, feed
):
86 return self
.feedlist
.index(feed
)
88 def reorder(self
, move
, delta
):
94 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
97 k
[m
+ delta
], k
[m
] = k
[m
], k
[m
+ delta
]
98 for i
in range(len(k
)):
99 list.__setitem
__(self
.feedlist
, i
, k
[i
])
103 def __delitem__(self
, value
):
104 feed
= self
.feedlist
[value
]
105 list.__delitem
__(self
.feedlist
, value
)
106 feed
.delete_all_items()
108 self
.emit('deleted', feed
)
110 def save_feeds(self
):
111 if not self
._loading
:
112 config
= Config
.get_instance()
113 config
.feeds
= [f
.dump() for f
in self
.feedlist
]
116 def feed_detail_changed(self
, feed
):
118 self
.emit('changed') # XXXX send the feed as well?
120 def _sort_dsu(self
, seq
):
121 aux_list
= [(x
.title
, x
) for x
in seq
]
122 aux_list
.sort(lambda a
,b
:locale
.strcoll(a
[0],b
[0]))
123 return [x
[1] for x
in aux_list
]
125 def sort(self
, indices
= None):
126 if not indices
or len(indices
) == 1:
127 self
[:] = self
._sort
_dsu
(self
)
129 items
= self
._sort
_dsu
(indices
)
130 for i
,x
in enumerate(items
):
131 list.__setitem
__(self
, indices
[i
], items
[i
])
137 for item
in self
.feedlist
:
141 def get_feed_with_id(self
, id):
142 for f
in self
.flatten_list():
147 def flatten_list(self
, ob
=None):
152 if isinstance(o
, list):
153 l
= l
+ self
.flatten_list(o
)
158 feedlist_instance
= None
161 global feedlist_instance
162 if feedlist_instance
is None:
163 feedlist_instance
= FeedList()
164 return feedlist_instance
166 class Feed(gobject
.GObject
):
167 "A Feed object stores information set by user about a RSS feed."
173 __slots__
= ('_title', '_location', '_username', '_password', '_parsed',
174 '__save_fields', '_items', '_slots',
175 '_id', '_channel_description',
176 '_channel_title', '_channel_link', '_channel_copyright',
177 'channel_lbd', 'channel_editor', 'channel_webmaster',
178 'channel_creator','_error', '_process_status', 'router', 'sticky', '_parent',
179 '_items_stored', '_poll_freq', '_last_poll','_n_items_unread')
181 __save_fields
= (('_title', ""), ('_location', ""), ('_username', ""),
182 ('_password', ""), ('_id', ""),
183 ('_channel_description', ""), ('_channel_title', ""),
184 ('_channel_link', ""), ('_channel_copyright', ""),
185 ('channel_creator', ""), ('_error', None),
186 ('_items_stored', DEFAULT
),
187 ('_poll_freq', DEFAULT
),
189 ('_n_items_unread',0))
193 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
194 'poll-done' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ()),
195 'items-updated' :(gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
196 (gobject
.TYPE_PYOBJECT
,)),
197 'items-read' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
198 (gobject
.TYPE_PYOBJECT
,)),
199 'items-deleted' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,
200 (gobject
.TYPE_PYOBJECT
,)),
201 'item-order-changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
,())
205 # use one of the factory functions below instead of this directly
206 def __init__(self
, title
="", location
="", username
="", password
=""):
207 gobject
.GObject
.__init
__(self
)
209 self
._channel
_description
= ""
210 self
._channel
_title
= ""
211 self
._channel
_link
= ""
212 self
._channel
_copyright
= ""
213 self
.channel_lbd
= None
214 self
.channel_editor
= ""
215 self
.channel_webmaster
= ""
216 self
.channel_creator
= ""
217 self
._location
= location
218 self
._username
= username
219 self
._password
= password
222 self
._n
_items
_unread
= 0
223 self
._process
_status
= self
.STATUS_IDLE
224 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
226 self
._items
_stored
= Feed
.DEFAULT
227 self
._poll
_freq
= Feed
.DEFAULT
229 self
._items
= FifoCache(num_entries
=Feed
.DEFAULT
)
230 self
._items
_loaded
= False
234 return "Feed '%s' from %s" % (self
._title
, self
._location
)
242 doc
= "A ParsedSummary object generated from the summary file"
245 def fset(self
, parsed
):
246 self
._parsed
= parsed
247 return property(**locals())
251 doc
= "The title of this Feed (as defined by user)"
257 def fset(self
, title
):
258 if self
._title
!= title
:
261 return property(**locals())
265 doc
= "A tuple of location, username, password"
267 return (self
._location
, self
._username
, self
._password
)
268 def fset(self
, (location
,username
,password
)):
269 self
._location
= location
270 self
._username
= username
271 self
._password
= password
273 return property(**locals())
279 return self
._location
280 def fset(self
, location
):
281 if self
._location
!= location
:
282 self
._location
= location
284 return property(**locals())
291 if self
._channel
_title
:
292 text
= self
._channel
_title
295 changed
= self
._channel
_title
!= t
296 self
._channel
_title
= t
299 return property(**locals())
302 def channel_description():
306 if self
._channel
_description
:
307 text
= self
._channel
_description
310 changed
= self
._channel
_description
!= t
311 self
._channel
_description
= t
314 return property(**locals())
320 return self
._channel
_link
322 changed
= self
._channel
_link
!= t
323 self
._channel
_link
= t
326 return property(**locals())
329 def channel_copyright():
332 return self
._channel
_copyright
334 changed
= self
._channel
_copyright
!= t
335 self
._channel
_copyright
= t
338 return property(**locals())
341 def number_of_items_stored():
344 return self
._items
_stored
345 def fset(self
, num
=None):
346 if self
._items
_stored
!= num
:
347 self
._items
_stored
= num
348 return property(**locals())
351 def poll_frequency():
354 return self
._poll
_freq
355 def fset(self
, freq
):
356 if self
._poll
_freq
!= freq
:
357 self
._poll
_freq
= freq
358 return property(**locals())
364 return self
._last
_poll
365 def fset(self
, time
):
366 if self
._last
_poll
!= time
:
367 self
._last
_poll
= time
368 return property(**locals())
371 def number_of_unread():
372 doc
= "the number of unread items for this feed"
374 return self
._n
_items
_unread
376 self
._n
_items
_unread
= n
378 return property(**locals())
385 def fset(self
, error
):
386 if self
._error
!= error
:
389 return property(**locals())
392 def process_status():
395 return self
._process
_status
396 def fset(self
, status
):
397 if status
!= self
._process
_status
:
398 self
._process
_status
= status
400 return property(**locals())
407 def fset(self
, parent
):
408 self
._parent
= parent
409 return property(**locals())
412 def next_refresh(self
):
413 """ return the feed's next refresh (time)"""
415 if self
._poll
_freq
== self
.DEFAULT
:
416 increment
= Config
.get_instance().poll_frequency
418 increment
= self
._poll
_freq
420 nr
= self
.last_poll
+ increment
425 for f
, default
in self
.__save
_fields
:
426 fl
[f
] = self
.__getattribute
__(f
)
429 def undump(self
, dict):
430 for f
, default
in self
.__save
_fields
:
431 self
.__setattr
__(f
, dict.get(f
, default
))
435 self
.emit('poll-done')
437 def add_items(self
, items
):
438 items
= sorted(items
, key
=operator
.attrgetter('pub_date'))
439 config
= Config
.get_instance()
440 cutpoint
= self
.number_of_items_stored
441 if cutpoint
== Feed
.DEFAULT
:
442 cutpoint
= config
.number_of_items_stored
443 self
._items
.set_number_of_entries(cutpoint
)
446 maxid
= reduce(max, [item
.id for item
in self
._items
])
447 print "MAX ID IS ", maxid
453 newitems
.append(item
)
454 self
._items
[item
.id] = item
455 self
.emit('items-updated', newitems
)
457 def restore_items(self
, items
):
458 items
= sorted(items
, key
=operator
.attrgetter('pub_date'))
459 cutpoint
= self
.number_of_items_stored
460 if cutpoint
== Feed
.DEFAULT
:
461 config
= Config
.get_instance()
462 cutpoint
= config
.number_of_items_stored
463 self
._items
.set_number_of_entries(cutpoint
)
465 for idx
, item
in enumerate(items
):
466 if not item
.sticky
and idx
> cutpoint
:
468 olditems
.append(item
)
471 self
._items
[item
.id] = item
472 print "\told: %s, new:%s" % (len(olditems
),len(self
._items
))
474 self
.emit('items-deleted', olditems
)
477 def item_read_cb(self
, item
):
478 print "ITEM READ! -> ", item
480 def delete_all_items(self
):
481 self
.emit('items-deleted', self
._items
)
485 if not self
._items
_loaded
:
487 return self
._items
.itervalues()
489 def mark_all_items_as_read(self
):
490 def mark(item
): item
.seen
= True
491 unread
= [item
for item
in self
._items
if not item
.seen
]
493 self
.emit('items-read', unread
)
495 def load_contents(self
):
496 if self
._items
_loaded
:
498 itemstore
= ItemStore
.get_instance()
499 items
= itemstore
.read_feed_items(self
)
500 print "feed.load_contents->items: ", len(items
)
502 self
.restore_items(items
)
503 self
._items
_loaded
= True
504 return self
._items
_loaded
506 def unload_contents(self
):
508 Unloads the items by disconnecting the signals and reinitialising the
511 TODO: unload seems to lose some circular references. garbage collector
512 will find them, though, so maybe it's not a problem.
514 if not self
._items
_loaded
:
517 self
._items
_loaded
= False
520 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
523 f
._location
= location
524 f
._id
= Config
.get_instance().next_feed_id_seq()
525 f
._username
= username
526 f
._password
= password
530 def create_empty_feed(klass
):
536 from collections
import deque
538 class FifoCache(object, UserDict
.DictMixin
):
539 ''' A mapping that remembers the last 'num_entries' items that were set '''
541 def __init__(self
, num_entries
, dct
=()):
542 self
.num_entries
= num_entries
547 return '%r(%r,%r)' % (
548 self
.__class
__.__name
__, self
.num_entries
, self
.dct
)
551 return self
.__class
__(self
.num_entries
, self
.dct
)
554 return list(self
.lst
)
556 def __getitem__(self
, key
):
559 def __setitem__(self
, key
, value
):
563 self
.remove_from_deque(lst
, key
)
566 if len(lst
) > self
.num_entries
:
567 del dct
[lst
.popleft()]
569 def __delitem__(self
, key
):
571 self
.remove_from_deque(self
.lst
, key
)
573 # a method explicitly defined only as an optimization
574 def __contains__(self
, item
):
575 return item
in self
.dct
577 has_key
= __contains__
579 def remove_from_deque(self
, d
, x
):
580 for i
, v
in enumerate(d
):
584 raise ValueError, '%r not in %r' % (x
,d
)
586 def set_number_of_entries(self
, num
):
587 self
.num_entries
= num
588 while len(self
.lst
) > num
:
589 del self
.dct
[self
.lst
.popleft()]