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
21 from gettext
import gettext
as _
22 from StringIO
import StringIO
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
)
39 class FeedList(gobject
.GObject
):
42 'changed' : (gobject
.SIGNAL_RUN_LAST
, gobject
.TYPE_NONE
, ())
45 def __init__(self
, init_seq
= []):
46 gobject
.GObject
.__init
__(self
)
51 return iter(self
.feedlist
)
54 def _load(feedlist
, parent
):
56 if isinstance(df
, list):
59 f
= Feed
.create_empty_feed()
61 self
.append(parent
, f
)
63 feedlist
= Config
.get_instance().feeds
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
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
)
89 f
.connect('changed', self
.feed_detail_changed
)
92 def append(self
, parent
, value
):
93 self
.feedlist
.append(value
)
95 value
.connect('changed', self
.feed_detail_changed
)
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
)
104 def index(self
, feed
):
105 return self
.feedlist
.index(feed
)
107 def reorder(self
, move
, delta
):
113 if move
[0] == 0 and delta
< 0 or move
[-1] == (len(self
.feedlist
) - 1) and delta
> 0:
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
])
121 def __delitem__(self
, value
):
122 feed
= self
.feedlist
[value
]
123 list.__delitem
__(self
.feedlist
, value
)
124 feed
.delete_all_items()
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
]
133 def feed_detail_changed(self
, feed
):
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
)
146 items
= self
._sort
_dsu
(indices
)
147 for i
,x
in enumerate(items
):
148 list.__setitem
__(self
, indices
[i
], items
[i
])
150 # self.emit('changed')
154 for item
in self
.feedlist
:
158 def get_feed_with_id(self
, id):
159 for f
in self
.flatten_list():
164 def flatten_list(self
, ob
=None):
169 if isinstance(o
, list):
170 l
= l
+ self
.flatten_list(o
)
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 '''
188 filename
= os
.path
.join(straw
.STRAW_DATA_DIR
, 'feed.png')
189 self
.icon
= gtk
.gdk
.pixbuf_new_from_file(filename
)
199 class PollingState(object):
200 ''' state when feed is polling '''
202 self
._icon
= gtk
.Image()
203 self
._icon
.set_from_stock(gtk
.STOCK_EXECUTE
, gtk
.ICON_SIZE_MENU
)
207 return self
._icon
.get_pixbuf()
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
)
223 return self
._icon
.get_pixbuf()
229 class Feed(gobject
.GObject
):
230 "A Feed object stores information set by user about a RSS feed."
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
),
252 ('_n_items_unread',0))
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
)
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
285 self
._n
_items
_unread
= 0
286 self
._process
_status
= self
.STATUS_IDLE
287 self
.router
= FeedDataRouter
.FeedDataRouter(self
)
289 self
._items
_stored
= Feed
.DEFAULT
290 self
._poll
_freq
= Feed
.DEFAULT
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
304 return "Feed '%s' from %s" % (self
._title
, self
._location
)
312 doc
= "A ParsedSummary object generated from the summary file"
315 def fset(self
, parsed
):
316 self
._parsed
= parsed
317 return property(**locals())
321 doc
= "The title of this Feed (as defined by user)"
327 def fset(self
, title
):
328 if self
._title
!= title
:
331 return property(**locals())
335 doc
= "A tuple of location, username, password"
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
343 return property(**locals())
349 return self
._location
350 def fset(self
, location
):
351 if self
._location
!= location
:
352 self
._location
= location
354 return property(**locals())
361 if self
._channel
_title
:
362 text
= self
._channel
_title
365 changed
= self
._channel
_title
!= t
366 self
._channel
_title
= t
369 return property(**locals())
372 def channel_description():
376 if self
._channel
_description
:
377 text
= self
._channel
_description
380 changed
= self
._channel
_description
!= t
381 self
._channel
_description
= t
384 return property(**locals())
390 return self
._channel
_link
392 changed
= self
._channel
_link
!= t
393 self
._channel
_link
= t
396 return property(**locals())
399 def channel_copyright():
402 return self
._channel
_copyright
404 changed
= self
._channel
_copyright
!= t
405 self
._channel
_copyright
= t
408 return property(**locals())
411 def number_of_items_stored():
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())
421 def poll_frequency():
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())
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())
441 def number_of_unread():
442 doc
= "the number of unread items for this feed"
444 return self
._n
_items
_unread
446 self
._n
_items
_unread
= n
448 return property(**locals())
455 def fset(self
, error
):
456 if self
._error
!= error
:
459 return property(**locals())
462 def process_status():
465 return self
._process
_status
466 def fset(self
, status
):
467 if status
!= self
._process
_status
:
468 self
._process
_status
= status
470 return property(**locals())
477 def fset(self
, parent
):
478 self
._parent
= parent
479 return property(**locals())
482 def next_refresh(self
):
483 """ return the feed's next refresh (time)"""
485 if self
._poll
_freq
== self
.DEFAULT
:
486 increment
= self
.config
.poll_frequency
488 increment
= self
._poll
_freq
490 nr
= self
.last_poll
+ increment
495 for f
, default
in self
.__save
_fields
:
496 fl
[f
] = self
.__getattribute
__(f
)
499 def undump(self
, dict):
500 for f
, default
in self
.__save
_fields
:
501 self
.__setattr
__(f
, dict.get(f
, default
))
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
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())
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
:
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
)
545 for idx
, item
in enumerate(items
):
546 if not item
.sticky
and idx
>= cutpoint
:
548 olditems
.append(item
)
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()
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
):
564 self
.emit('items-deleted', self
._items
.values())
568 if not self
._items
_loaded
:
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
]
576 self
.emit('items-changed', unread
)
578 def load_contents(self
):
579 if self
._items
_loaded
:
581 itemstore
= ItemStore
.get_instance()
582 items
= itemstore
.read_feed_items(self
)
583 print "feed.load_contents->items: ", len(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
:
593 self
._items
_loaded
= False
596 def create_new_feed(klass
, title
, location
="", username
="", password
=""):
599 f
._location
= location
600 f
._id
= Config
.get_instance().next_feed_id_seq()
601 f
._username
= username
602 f
._password
= password
606 def create_empty_feed(klass
):
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
623 return '%r(%r,%r)' % (
624 self
.__class
__.__name
__, self
.num_entries
, self
.dct
)
627 return self
.__class
__(self
.num_entries
, self
.dct
)
630 return list(self
.lst
)
632 def __getitem__(self
, key
):
635 def __setitem__(self
, key
, value
):
639 self
.remove_from_deque(lst
, key
)
642 if len(lst
) > self
.num_entries
:
643 del dct
[lst
.popleft()]
645 def __delitem__(self
, 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
):
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
):
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
, ())
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
],
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()
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 []
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
709 fc
= FeedCategory(head
['title'])
710 sub
= head
.get('subscription', None)
712 fc
.subscription
= undump_subscription(sub
)
714 # backwards compatibility for stuff saved with versions <= 0.23
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
724 error
.log("%s (%d) was already in %s, skipping" % (str(feed
), fid
, str(fc
)))
726 fc
.append_feed(feed
, from_sub
)
727 categorized
[feed
] = True
728 # User categories: connect pseudos later
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)
745 self
.un_category
.append_feed(f
, False)
746 pseudos_changed
= True
749 for cat
in self
.pseudo_categories
:
750 cat
.connect('changed', self
.pseudo_category_changed
)
753 Config
.get_instance().categories
= [
754 cat
.dump() for cat
in self
]
756 def pseudo_category_changed(self
, category
, *args
):
758 self
.emit('pseudo-changed')
760 def category_changed(self
, signal
):
761 if signal
.feed
is not None:
763 for cat
in self
.user_categories
:
764 if signal
.feed
in cat
.feeds
:
765 uncategorized
= False
768 self
.un_category
.append_feed(signal
.feed
, False)
771 self
.un_category
.remove_feed(signal
.feed
)
775 self
.emit('category-changed')
777 def remove_feed(self
, feed
):
781 del self
._feedlist
[feed
]
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
:
789 category
.insert_feed(index
, feed
, False)
791 category
.append_feed(feed
, False)
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
)
803 print 'feed imported ', feeds
804 self
.un_category
.extend_feed(feeds
, from_sub
)
805 self
.all_category
.extend_feed(feeds
, from_sub
)
808 def user_categories(self
):
809 return self
._user
_categories
812 def pseudo_categories(self
):
813 return self
._pseudo
_categories
816 def all_categories(self
):
817 return self
.pseudo_categories
+ tuple(self
.user_categories
)
820 def all_category(self
):
821 return self
._all
_category
824 def un_category(self
):
825 return self
._un
_category
827 class CategoryIterator
:
828 def __init__(self
, fclist
):
829 self
._fclist
= fclist
838 uclen
= len(self
._fclist
.user_categories
)
840 return self
._fclist
.user_categories
[i
]
841 elif i
< uclen
+ len(self
._fclist
.pseudo_categories
):
842 return self
._fclist
.pseudo_categories
[i
- uclen
]
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
]
858 self
._user
_categories
= [x
[1] for x
in auxlist
]
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
)
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
):
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
895 return self
._location
896 def fset(self
, location
):
897 self
._location
= location
899 return property(**locals())
905 return self
._username
906 def fset(self
, username
):
907 self
._username
= username
909 return property(**locals())
915 return self
._password
916 def fset(self
, password
):
917 self
._password
= password
919 return property(**locals())
925 return self
._frequency
926 def fset(self
, freq
):
927 self
._frequency
= freq
929 return property(**locals())
935 return self
._last
_poll
936 def fset(self
, last_poll
):
937 self
._last
_poll
= last_poll
939 return property(**locals())
946 def fset(self
, error
):
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
963 return self
._contents
966 def undump(klass
, dictionary
):
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')
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
,
986 def undump_subscription(dictionary
):
988 if dictionary
.get('type') == 'opml':
989 return OPMLCategorySubscription
.undump(dictionary
)
991 error
.log("exception while undumping subscription: " + str(e
))
994 class CategoryMember(object):
995 def __init__(self
, feed
=None, from_sub
=False):
997 self
._from
_subscription
= from_sub
1004 def fset(self
, feed
):
1006 return property(**locals())
1009 def from_subscription():
1012 return self
._from
_subscription
1014 self
._from
_subscription
= p
1015 return property(**locals())
1017 class FeedCategory(gobject
.GObject
):
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
)
1031 self
._subscription
= None
1038 def fset(self
, title
):
1040 self
.emit('changed')
1041 return property(**locals())
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:
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)
1072 index
= self
.index_feed(allfeeds
[f
])
1073 member
= self
.feedlist
[index
]
1074 if member
.from_subscription
:
1075 self
.remove_feed(allfeeds
[f
])
1078 def _subscription_changed(self
, *args
):
1079 self
.emit('changed')
1081 def _subscription_contents_updated(self
, *args
):
1082 self
.read_contents_from_subscription()
1085 return "FeedCategory %s" % self
.title
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
]
1112 raise ValueError(value
)
1113 self
.emit('feed-removed', value
)
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
:
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
)
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
):
1142 if target
== source
:
1146 self
.feedlist
.insert(target
, t
)
1147 self
.emit('changed')
1150 head
= {'title': self
.title
}
1151 if self
.subscription
is not None:
1152 head
['subscription'] = self
.subscription
.dump()
1154 {'id': f
.feed
.id, 'from_subscription': f
.from_subscription
}
1155 for f
in self
.feedlist
]
1159 return [f
.feed
for f
in self
.feedlist
]
1161 def __eq__(self
, ob
):
1162 if isinstance(ob
, types
.NoneType
):
1164 elif isinstance(ob
, FeedCategory
):
1165 return self
.title
== ob
.title
and list.__eq
__(self
.feedlist
, ob
)
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
1181 return "PseudoCategory %s" % self
.title
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
):
1189 FeedCategory
.append_feed(self
, feed
, False)
1191 def insert_feed(self
, index
, feed
, from_sub
):
1193 FeedCategory
.insert_feed(self
, index
, feed
, False)
1197 def get_category_list():
1200 fclist
= FeedCategoryList()
1203 category_list
= get_category_list()