Implemented "mark all as read" functionality, cleanup.
[straw.git] / straw / FeedManager.py
blob3c9f287d015a4c79c7a969baa8a0a1e10cba62b1
1 from Constants import *
2 from error import debug
3 from gobject import GObject, SIGNAL_RUN_FIRST
4 from model import Feed, Item, Category
5 from storage import DAO, Storage
6 import FeedUpdater
7 import ItemManager
8 import gobject
9 import opml
11 _storage_path = None
13 def init():
14 fm = _get_instance()
15 fm.init_storage(_storage_path)
17 def setup(storage_path = None):
18 global _storage_path
19 _storage_path = storage_path
21 def import_opml(path, category):
22 fm = _get_instance()
23 fm.import_opml(path, category)
25 def export_opml(root_id, filename):
26 fm = _get_instance()
27 fm.export_opml(root_id, filename)
29 def lookup_feed(id):
30 fm = _get_instance()
31 return fm.lookup_feed(id)
33 def lookup_category(ctg_id):
34 fm = _get_instance()
35 return fm.lookup_category(ctg_id)
37 def get_feeds():
38 fm = _get_instance()
39 return fm.get_feeds()
41 def update_all_feeds(observers):
42 fm = _get_instance()
43 return fm.update_all_feeds(observers)
45 def is_update_all_running():
46 fm = _get_instance()
47 return fm.update_all_id != None
49 def stop_update_all():
50 fm = _get_instance()
51 return fm.stop_update_all()
53 def update_nodes(nodes, observers = {}):
54 fm = _get_instance()
55 return fm.update_nodes(nodes, observers)
57 def save_category(category):
58 fm = _get_instance()
59 return fm.save_category(category)
61 def save_feed(feed):
62 fm = _get_instance()
63 result = fm.save_feed(feed)
64 return result
66 def save_all(save_list):
67 fm = _get_instance()
68 fm.save_all(save_list)
70 def get_model():
71 fm = _get_instance()
72 return fm.nodes
74 def get_children_feeds(node_id):
75 node = lookup_category(node_id)
77 if not node:
78 return None
80 return [_node for _node in node.all_children() if _node.type == "F"]
82 def categories(root_id = 1):
83 nodes = get_model()
85 if not nodes.has_key(root_id):
86 return
88 yield nodes[root_id]
90 for node in nodes[root_id].children:
91 if node.type == "C":
92 for child in categories(node.id):
93 yield child
95 def move_node(node, target_path):
96 fm = _get_instance()
97 fm.move_node(node, target_path)
99 def delete_nodes(node_ids):
100 fm = _get_instance()
101 fm.delete_nodes(node_ids)
103 class FeedManager(GObject):
104 __gsignals__ = {
105 "item-added": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
106 "feed-added": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
107 "feed-status-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
108 "category-added": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)),
109 "update-all-done": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
112 def __init__(self):
113 GObject.__init__(self)
115 self.storage = None
116 self.update_all_id = None
118 def emit(self, *args):
119 gobject.idle_add(gobject.GObject.emit, self, *args)
121 def init_storage(self, path):
122 self.storage = Storage(path)
123 self.dao = DAO(self.storage)
124 self.nodes = self.dao.get_nodes()
126 self._prepare_model()
128 def _prepare_model(self):
129 for node in self.nodes.values():
130 self._setup_node_signals(node)
132 if node.parent_id != None:
133 node.parent = self.nodes[node.parent_id]
134 node.parent.append_child(node)
135 else:
136 node.parent = None
138 if isinstance(node, Feed):
139 self._set_feed_status(node, FS_IDLE)
141 def _setup_node_signals(self, node):
142 node.connect("parent-changed", self.on_parent_changed)
143 node.connect("norder-changed", self.on_norder_changed)
144 node.connect("mark-all-items-as-read", self.on_mark_all_items_as_read)
146 def _set_feed_status(self, feed, status):
147 if not hasattr(feed, "status"):
148 feed.status = None
150 if feed.status != status:
151 feed.status = status
152 self.emit("feed-status-changed", feed)
154 def import_opml(self, path, category):
155 # XXX: We should support remote locations for OPML import!
156 url = "file://" + path
157 opml.import_opml(url, category, [ { "opml-imported": [ self._on_opml_imported ] } ])
159 def _on_opml_imported(self, handler, save_list):
160 if save_list:
161 self.dao.tx_begin()
162 self.save_all(save_list)
163 self.dao.tx_commit()
165 def export_opml(self, root_id, filename):
166 if not self.nodes.has_key(root_id):
167 return None
169 opml.export(self.nodes[root_id], filename)
171 def _model_add_node(self, node):
172 self.nodes[node.parent_id].add_child(node)
174 def move_node(self, node, target_path):
175 debug("fm:move_node %d to %s" % (node.id, str(target_path)))
176 new_parent = self.nodes[1].get_by_path(target_path[:-1])
178 if not new_parent:
179 new_parent = self.nodes[1]
181 self.dao.tx_begin()
182 node.move(new_parent, target_path[-1:][0])
183 self.dao.tx_commit()
185 def delete_nodes(self, node_ids):
186 self.dao.tx_begin()
188 for id in node_ids:
189 self.delete_node(id)
191 self.dao.tx_commit()
193 def delete_node(self, id):
194 if not self.nodes.has_key(id):
195 return
197 node = self.nodes[id]
199 children = list(node.children)
200 children.reverse()
202 for child_node in children:
203 self.delete_node(child_node.id)
205 node.parent.remove_child(node)
206 del self.nodes[id]
208 if node.type == "F":
209 debug("deleting F %s" % id)
210 self.dao.delete_feed(id)
211 elif node.type == "C":
212 debug("deleting C %s" % id)
213 self.dao.delete_category(id)
215 def save_all(self, save_list):
216 for item in save_list:
217 if isinstance(item, Feed):
218 self._set_feed_status(item, FS_IDLE)
219 item.parent_id = 1
221 if item.parent:
222 item.parent_id = item.parent.id
223 else:
224 item.parent_id = 1
225 item.parent = self.nodes[1]
227 self.save_feed(item)
228 else:
229 if item.parent:
230 item.parent_id = item.parent.id
231 else:
232 item.parent_id = 1
233 item.parent = self.nodes[1]
235 self.save_category(item)
237 def lookup_feed(self, id):
238 return self.nodes[id]
240 def lookup_category(self, id):
241 return self.nodes[id]
243 def save_feed(self, feed):
244 if not feed.parent_id:
245 feed.parent_id = 1
247 category = self.lookup_category(feed.parent_id)
249 if not category:
250 return None
252 if feed.id:
253 result = self.dao.save(feed)
254 else:
255 result = self.dao.save(feed)
256 self._model_add_node(feed)
257 self.dao.save(feed)
259 self.nodes[feed.id] = feed
261 self._setup_node_signals(feed)
262 self.emit("feed-added", feed)
264 return result
266 def update_all_feeds(self, observers):
267 feeds = [node for node in self.nodes.values() if node.type == "F"]
269 self.update_all_id = FeedUpdater.update(feeds, [{
270 "job-done": [ self._on_update_all_done ],
271 "update-started": [ self._on_update_feed_start ],
272 "update-done": [ self._on_update_feed_done ]
273 }, observers])
275 def update_nodes(self, nodes, observers):
276 feeds = []
278 for node in nodes:
279 if node.type == "F":
280 feeds.append(node)
281 else:
282 feeds.extend([child_node for child_node in node.all_children() if child_node.type == "F"])
284 FeedUpdater.update(feeds, [{
285 "update-started": [ self._on_update_feed_start ],
286 "update-done": [ self._on_update_feed_done ]
287 }, observers])
289 def stop_update_all(self):
290 FeedUpdater.stop(self.update_all_id)
292 def _on_update_all_done(self, handler, data):
293 self.update_all_id = None
294 self.emit("update-all-done")
296 def _on_update_feed_start(self, handler, feed):
297 self._set_feed_status(feed, FS_UPDATING)
299 def _on_update_feed_done(self, handler, update_result):
300 feed = update_result.feed
302 if update_result.error:
303 feed.update_result = update_result
304 self._set_feed_status(feed, FS_ERROR)
305 return
307 self._set_feed_status(feed, FS_IDLE)
309 self.dao.tx_begin()
311 # Some metadata could change between the updates.
312 self.save_feed(feed)
314 for item in feed.items:
315 item.feed_id = feed.id
317 if not ItemManager.feed_item_exists(item):
318 self.dao.save(item)
319 feed.props.unread_count += 1
320 self.emit("item-added", item)
322 self.dao.tx_commit()
324 def save_category(self, category):
325 debug("saving category %s with parent %s" % (category.name, str(category.parent_id)))
327 if category.parent and not category.parent_id:
328 category.parent_id = category.parent.id
330 self.dao.tx_begin()
332 self.dao.save(category)
333 self._model_add_node(category)
334 self.dao.save(category)
336 self.dao.tx_commit()
338 self.nodes[category.id] = category
340 self._setup_node_signals(category)
341 self.emit("category-added", category)
343 def on_parent_changed(self, obj, old_parent):
344 #debug("parent changed, saving %d" % (obj.id))
345 self.dao.save(obj)
347 def on_norder_changed(self, obj, old_norder):
348 #debug("norder changed, saving %d" % (obj.id))
349 self.dao.save(obj)
351 def on_mark_all_items_as_read(self, node):
352 self.dao.tx_begin()
353 result = self.dao.query("UPDATE items SET is_read = 1 WHERE feed_id = %d" % node.id)
354 self.dao.tx_commit()
356 node.props.unread_count -= node.props.unread_count
358 _instance = None
360 def _get_instance():
361 global _instance
362 if not _instance:
363 _instance = FeedManager()
364 return _instance