1 #!/usr/bin/env python2.5
4 # Copyright (c) 2007-2008 INdT.
5 # Copyright (c) 2011 Neal H. Walfield
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # ============================================================================
22 # Author : Yves Marcoz
24 # Description : Simple RSS Reader
25 # ============================================================================
28 from os.path import isfile, isdir
29 from shutil import rmtree
30 from os import mkdir, remove, utime
36 from BeautifulSoup import BeautifulSoup
37 from urlparse import urljoin
38 from calendar import timegm
41 from wc import wc, wc_init, woodchuck
44 from updatedbus import update_server_object
46 from jobmanager import JobManager
48 from httpprogresshandler import HTTPProgressHandler
52 logger = logging.getLogger(__name__)
55 return md5.new(string).hexdigest()
57 def download_callback(connection):
58 if JobManager().do_quit:
59 raise KeyboardInterrupt
61 def downloader(progress_handler=None, proxy=None):
64 if progress_handler is not None:
65 openers.append(progress_handler)
67 openers.append(HTTPProgressHandler(download_callback))
72 return urllib2.build_opener(*openers)
74 # If not None, a subprocess.Popen object corresponding to a
75 # update_feeds.py process.
76 update_feed_process = None
78 update_feeds_iface = None
83 serial_execution_lock = threading.Lock()
88 except AttributeError:
89 db = sqlite3.connect("%s/%s.db" % (self.dir, self.key), timeout=120)
94 def __init__(self, configdir, key):
96 self.configdir = configdir
97 self.dir = "%s/%s.d" %(self.configdir, self.key)
98 self.tls = threading.local ()
100 if not isdir(self.dir):
102 if not isfile("%s/%s.db" %(self.dir, self.key)):
103 self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, date float, updated float, link text, read int);")
104 self.db.execute("CREATE TABLE images (id text, imagePath text);")
107 def addImage(self, configdir, key, baseurl, url, proxy=None, opener=None):
108 filename = configdir+key+".d/"+getId(url)
109 if not isfile(filename):
112 opener = downloader(proxy=proxy)
114 abs_url = urljoin(baseurl,url)
115 f = opener.open(abs_url)
116 outf = open(filename, "w")
120 except (urllib2.HTTPError, urllib2.URLError, IOError), exception:
121 logger.info("Could not download image %s: %s"
122 % (abs_url, str (exception)))
125 exception = sys.exc_info()[0]
127 logger.info("Downloading image %s: %s" %
128 (abs_url, traceback.format_exc()))
136 #open(filename,"a").close() # "Touch" the file
137 file = open(filename,"a")
138 utime(filename, None)
142 def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, priority=0, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
143 if (os.path.basename(sys.argv[0]) == 'update_feeds.py'):
146 self._updateFeed(configdir, url, etag, modified, expiryTime, proxy, imageCache, postFeedUpdateFunc, *postFeedUpdateFuncArgs)
148 JobManager().execute(doit(), self.key, priority=priority)
150 def send_update_request():
151 global update_feeds_iface
152 if update_feeds_iface is None:
153 bus=dbus.SessionBus()
154 remote_object = bus.get_object(
155 "org.marcoz.feedingit", # Connection name
156 "/org/marcoz/feedingit/update" # Object's path
158 update_feeds_iface = dbus.Interface(
159 remote_object, 'org.marcoz.feedingit')
162 update_feeds_iface.Update(self.key)
164 logger.error("Invoking org.marcoz.feedingit.Update: %s"
166 update_feeds_iface = None
170 if send_update_request():
171 # Success! It seems we were able to start the update
172 # daemon via dbus (or, it was already running).
175 global update_feed_process
176 if (update_feed_process is None
177 or update_feed_process.poll() is not None):
178 # The update_feeds process is not running. Start it.
179 update_feeds = os.path.join(os.path.dirname(__file__),
181 argv = ['/usr/bin/env', 'python', update_feeds, '--daemon' ]
182 logger.debug("Starting update_feeds: running %s"
184 update_feed_process = subprocess.Popen(argv)
185 # Make sure the dbus calls go to the right process:
187 update_feeds_iface = None
190 if send_update_request():
194 def _updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
196 have_serial_execution_lock = False
198 download_start = time.time ()
200 progress_handler = HTTPProgressHandler(download_callback)
202 openers = [progress_handler]
204 openers.append (proxy)
205 kwargs = {'handlers':openers}
207 tmp=feedparser.parse(url, etag=etag, modified=modified, **kwargs)
208 download_duration = time.time () - download_start
210 opener = downloader(progress_handler, proxy)
212 if JobManager().do_quit:
213 raise KeyboardInterrupt
215 process_start = time.time()
217 # Expiry time is in hours
218 expiry = float(expiryTime) * 3600.
222 have_woodchuck = mainthread.execute (wc().available)
226 wc().stream_register (self.key, "", 6 * 60 * 60)
227 except woodchuck.ObjectExistsError:
230 wc()[self.key].updated (
231 indicator=(woodchuck.Indicator.ApplicationVisual
232 |woodchuck.Indicator.StreamWide),
233 transferred_down=progress_handler.stats['received'],
234 transferred_up=progress_handler.stats['sent'],
235 transfer_time=download_start,
236 transfer_duration=download_duration,
237 new_objects=len (tmp.entries),
238 objects_inline=len (tmp.entries))
241 "Failed to register update of %s with woodchuck!"
244 http_status = tmp.get ('status', 200)
246 # Check if the parse was succesful. If the http status code
247 # is 304, then the download was successful, but there is
248 # nothing new. Indeed, no content is returned. This make a
249 # 304 look like an error because there are no entries and the
250 # parse fails. But really, everything went great! Check for
252 if http_status == 304:
253 logger.debug("%s: No changes to feed." % (self.key,))
254 mainthread.execute (wc_success, async=True)
256 elif len(tmp["entries"])==0 and not tmp.version:
257 # An error occured fetching or parsing the feed. (Version
258 # will be either None if e.g. the connection timed our or
259 # '' if the data is not a proper feed)
261 "Error fetching %s: version is: %s: error: %s"
262 % (url, str (tmp.version),
263 str (tmp.get ('bozo_exception', 'Unknown error'))))
267 logger.debug("%s: stream update failed!" % self.key)
270 # It's not easy to get the feed's title from here.
271 # At the latest, the next time the application is
272 # started, we'll fix up the human readable name.
273 wc().stream_register (self.key, "", 6 * 60 * 60)
274 except woodchuck.ObjectExistsError:
276 ec = woodchuck.TransferStatus.TransientOther
277 if 300 <= http_status and http_status < 400:
278 ec = woodchuck.TransferStatus.TransientNetwork
279 if 400 <= http_status and http_status < 500:
280 ec = woodchuck.TransferStatus.FailureGone
281 if 500 <= http_status and http_status < 600:
282 ec = woodchuck.TransferStatus.TransientNetwork
283 wc()[self.key].update_failed(ec)
284 mainthread.execute (e, async=True)
286 currentTime = time.time()
287 # The etag and modified value should only be updated if the content was not null
293 modified = tmp["modified"]
297 abs_url = urljoin(tmp["feed"]["link"],"/favicon.ico")
298 f = opener.open(abs_url)
301 outf = open(self.dir+"/favicon.ico", "w")
305 except (urllib2.HTTPError, urllib2.URLError), exception:
306 logger.debug("Could not download favicon %s: %s"
307 % (abs_url, str (exception)))
309 self.serial_execution_lock.acquire ()
310 have_serial_execution_lock = True
312 #reversedEntries = self.getEntries()
313 #reversedEntries.reverse()
317 tmp["entries"].reverse()
318 for entry in tmp["entries"]:
319 # Yield so as to make the main thread a bit more
323 if JobManager().do_quit:
324 raise KeyboardInterrupt
326 received_base = progress_handler.stats['received']
327 sent_base = progress_handler.stats['sent']
330 date = self.extractDate(entry)
334 entry["title"] = "No Title"
342 entry["author"] = None
343 if(not(entry.has_key("id"))):
345 content = self.extractContent(entry)
346 object_size = len (content)
347 received_base -= len (content)
348 tmpEntry = {"title":entry["title"], "content":content,
349 "date":date, "link":entry["link"], "author":entry["author"], "id":entry["id"]}
350 id = self.generateUniqueId(tmpEntry)
352 #articleTime = time.mktime(self.entries[id]["dateTuple"])
353 soup = BeautifulSoup(self.getArticle(tmpEntry)) #tmpEntry["content"])
355 baseurl = tmpEntry["link"]
357 if imageCache and len(images) > 0:
358 self.serial_execution_lock.release ()
359 have_serial_execution_lock = False
361 filename = self.addImage(
362 configdir, self.key, baseurl, img['src'],
365 img['src']="file://%s" %filename
366 count = self.db.execute("SELECT count(1) FROM images where id=? and imagePath=?;", (id, filename )).fetchone()[0]
368 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
372 object_size += os.path.getsize (filename)
373 except os.error, exception:
374 logger.error ("Error getting size of %s: %s"
375 % (filename, exception))
376 self.serial_execution_lock.acquire ()
377 have_serial_execution_lock = True
379 tmpEntry["contentLink"] = configdir+self.key+".d/"+id+".html"
380 file = open(tmpEntry["contentLink"], "w")
381 file.write(soup.prettify())
384 self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
387 values = (id, tmpEntry["title"], tmpEntry["contentLink"], tmpEntry["date"], currentTime, tmpEntry["link"], 0)
388 self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
392 # self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
394 # filename = configdir+self.key+".d/"+id+".html"
395 # file = open(filename,"a")
396 # utime(filename, None)
398 # images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
399 # for image in images:
400 # file = open(image[0],"a")
401 # utime(image[0], None)
406 # Register the object with Woodchuck and mark it as
411 obj = wc()[self.key].object_register(
412 object_identifier=id,
413 human_readable_name=tmpEntry["title"])
414 except woodchuck.ObjectExistsError:
415 obj = wc()[self.key][id]
417 # If the entry does not contain a publication
418 # time, the attribute won't exist.
419 pubtime = entry.get ('date_parsed', None)
421 obj.publication_time = time.mktime (pubtime)
423 received = (progress_handler.stats['received']
425 sent = progress_handler.stats['sent'] - sent_base
427 indicator=(woodchuck.Indicator.ApplicationVisual
428 |woodchuck.Indicator.StreamWide),
429 transferred_down=received,
431 object_size=object_size)
432 mainthread.execute(e, async=True)
436 "%s: Update successful: transferred: %d/%d; objects: %d)"
438 progress_handler.stats['sent'],
439 progress_handler.stats['received'],
441 mainthread.execute (wc_success, async=True)
444 rows = self.db.execute("SELECT id FROM feed WHERE (read=0 AND updated<?) OR (read=1 AND updated<?);", (currentTime-2*expiry, currentTime-expiry))
446 self.removeEntry(row[0])
448 from glob import glob
450 for file in glob(configdir+self.key+".d/*"):
454 # put the two dates into matching format
456 lastmodDate = stats[8]
458 expDate = time.time()-expiry*3
459 # check if image-last-modified-date is outdated
461 if expDate > lastmodDate:
465 #print 'Removing', file
467 # XXX: Tell woodchuck.
468 remove(file) # commented out for testing
470 except OSError, exception:
472 logger.error('Could not remove %s: %s'
473 % (file, str (exception)))
474 logger.debug("updated %s: %fs in download, %fs in processing"
475 % (self.key, download_duration,
476 time.time () - process_start))
478 logger.error("Updating %s: %s" % (self.key, traceback.format_exc()))
482 if have_serial_execution_lock:
483 self.serial_execution_lock.release ()
487 rows = self.db.execute("SELECT MAX(date) FROM feed;")
491 logger.error("Fetching update time: %s: %s"
492 % (str(e), traceback.format_exc()))
499 title = tmp.feed.title
500 except (AttributeError, UnboundLocalError), exception:
502 if postFeedUpdateFunc is not None:
503 postFeedUpdateFunc (self.key, updateTime, etag, modified,
504 title, *postFeedUpdateFuncArgs)
506 def setEntryRead(self, id):
507 self.db.execute("UPDATE feed SET read=1 WHERE id=?;", (id,) )
513 wc()[self.key][id].used()
517 def setEntryUnread(self, id):
518 self.db.execute("UPDATE feed SET read=0 WHERE id=?;", (id,) )
521 def markAllAsRead(self):
522 self.db.execute("UPDATE feed SET read=1 WHERE read=0;")
525 def isEntryRead(self, id):
526 read_status = self.db.execute("SELECT read FROM feed WHERE id=?;", (id,) ).fetchone()[0]
527 return read_status==1 # Returns True if read==1, and False if read==0
529 def getTitle(self, id):
530 return self.db.execute("SELECT title FROM feed WHERE id=?;", (id,) ).fetchone()[0]
532 def getContentLink(self, id):
533 return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
535 def getExternalLink(self, id):
536 return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
538 def getDate(self, id):
539 dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
540 return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(dateStamp))
542 def getDateTuple(self, id):
543 dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
544 return time.localtime(dateStamp)
546 def getDateStamp(self, id):
547 return self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
549 def generateUniqueId(self, entry):
551 Generate a stable identifier for the article. For the same
552 entry, this should result in the same identifier. If
553 possible, the identifier should remain the same even if the
556 # Prefer the entry's id, which is supposed to be globally
558 key = entry.get('id', None)
560 # Next, try the link to the content.
561 key = entry.get('link', None)
563 # Ok, the title and the date concatenated are likely to be
565 key = entry.get('title', None) + entry.get('date', None)
567 # Hmm, the article's content will at least guarantee no
568 # false negatives (i.e., missing articles)
569 key = entry.get('content', None)
571 # If all else fails, just use a random number.
572 key = str (random.random ())
575 def getIds(self, onlyUnread=False):
577 rows = self.db.execute("SELECT id FROM feed where read=0 ORDER BY date DESC;").fetchall()
579 rows = self.db.execute("SELECT id FROM feed ORDER BY date DESC;").fetchall()
586 def getNextId(self, id, forward=True):
592 index = ids.index(id)
593 return ids[(index + delta) % len(ids)]
595 def getPreviousId(self, id):
596 return self.getNextId(id, forward=False)
598 def getNumberOfUnreadItems(self):
599 return self.db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
601 def getNumberOfEntries(self):
602 return self.db.execute("SELECT count(*) FROM feed;").fetchone()[0]
604 def getArticle(self, entry):
605 #self.setEntryRead(id)
606 #entry = self.entries[id]
607 title = entry['title']
608 #content = entry.get('content', entry.get('summary_detail', {}))
609 content = entry["content"]
612 author = entry['author']
613 date = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(entry["date"]) )
615 #text = '''<div style="color: black; background-color: white;">'''
616 text = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
617 text += "<html><head><title>" + title + "</title>"
618 text += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
619 #text += '<style> body {-webkit-user-select: none;} </style>'
620 text += '</head><body bgcolor=\"#ffffff\"><div><a href=\"' + link + '\">' + title + "</a>"
622 text += "<BR /><small><i>Author: " + author + "</i></small>"
623 text += "<BR /><small><i>Date: " + date + "</i></small></div>"
624 text += "<BR /><BR />"
626 text += "</body></html>"
629 def getContent(self, id):
630 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
632 file = open(self.entries[id]["contentLink"])
633 content = file.read()
636 content = "Content unavailable"
639 def extractDate(self, entry):
640 if entry.has_key("updated_parsed"):
641 return timegm(entry["updated_parsed"])
642 elif entry.has_key("published_parsed"):
643 return timegm(entry["published_parsed"])
647 def extractContent(self, entry):
649 if entry.has_key('summary'):
650 content = entry.get('summary', '')
651 if entry.has_key('content'):
652 if len(entry.content[0].value) > len(content):
653 content = entry.content[0].value
655 content = entry.get('description', '')
658 def removeEntry(self, id):
659 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
663 except OSError, exception:
664 logger.error("Deleting %s: %s" % (contentLink, str (exception)))
665 self.db.execute("DELETE FROM feed WHERE id=?;", (id,) )
666 self.db.execute("DELETE FROM images WHERE id=?;", (id,) )
672 wc()[self.key][id].files_deleted (
673 woodchuck.DeletionResponse.Deleted)
674 del wc()[self.key][id]
677 mainthread.execute (e, async=True)
679 class ArchivedArticles(Feed):
680 def addArchivedArticle(self, title, link, date, configdir):
681 id = self.generateUniqueId({"date":date, "title":title})
682 values = (id, title, link, date, 0, link, 0)
683 self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
686 def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
688 rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
690 currentTime = time.time()
693 f = urllib2.urlopen(link)
694 #entry["content"] = f.read()
697 soup = BeautifulSoup(html)
701 filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
703 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
705 contentLink = configdir+self.key+".d/"+id+".html"
706 file = open(contentLink, "w")
707 file.write(soup.prettify())
710 self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
712 return (currentTime, None, None)
714 def purgeReadArticles(self):
715 rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
718 self.removeArticle(row[0])
720 def removeArticle(self, id):
721 rows = self.db.execute("SELECT imagePath FROM images WHERE id=?;", (id,) )
724 count = self.db.execute("SELECT count(*) FROM images WHERE id!=? and imagePath=?;", (id,row[0]) ).fetchone()[0]
735 except AttributeError:
736 db = sqlite3.connect("%s/feeds.db" % self.configdir, timeout=120)
739 db = property(_getdb)
741 # Lists all the feeds in a dictionary, and expose the data
742 def __init__(self, config, configdir):
744 self.configdir = configdir
746 self.tls = threading.local ()
749 table = self.db.execute("SELECT sql FROM sqlite_master").fetchone()
751 self.db.execute("CREATE TABLE feeds(id text, url text, title text, unread int, updateTime float, rank int, etag text, modified text, widget int, category int);")
752 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
753 self.addCategory("Default Category")
754 if isfile(self.configdir+"feeds.pickle"):
755 self.importOldFormatFeeds()
757 self.addFeed("Maemo News", "http://maemo.org/news/items.xml")
759 from string import find, upper
760 if find(upper(table[0]), "WIDGET")<0:
761 self.db.execute("ALTER TABLE feeds ADD COLUMN widget int;")
762 self.db.execute("UPDATE feeds SET widget=1;")
764 if find(upper(table[0]), "CATEGORY")<0:
765 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
766 self.addCategory("Default Category")
767 self.db.execute("ALTER TABLE feeds ADD COLUMN category int;")
768 self.db.execute("UPDATE feeds SET category=1;")
773 # Check that Woodchuck's state is up to date with respect our
775 updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
776 wc_init (self, True if updater else False)
777 if wc().available() and updater:
778 # The list of known streams.
779 streams = wc().streams_list ()
780 stream_ids = [s.identifier for s in streams]
782 # Register any unknown streams. Remove known streams from
784 for key in self.getListOfFeeds():
785 title = self.getFeedTitle(key)
786 # XXX: We should also check whether the list of
787 # articles/objects in each feed/stream is up to date.
788 if key not in stream_ids:
790 "Registering previously unknown channel: %s (%s)"
792 # Use a default refresh interval of 6 hours.
793 wc().stream_register (key, title, 6 * 60 * 60)
795 # Make sure the human readable name is up to date.
796 if wc()[key].human_readable_name != title:
797 wc()[key].human_readable_name = title
798 stream_ids.remove (key)
801 # Unregister any streams that are no longer subscribed to.
802 for id in stream_ids:
803 logger.debug("Unregistering %s" % (id,))
804 w.stream_unregister (id)
806 def importOldFormatFeeds(self):
807 """This function loads feeds that are saved in an outdated format, and converts them to sqlite"""
809 listing = rss.Listing(self.configdir)
811 for id in listing.getListOfFeeds():
814 values = (id, listing.getFeedTitle(id) , listing.getFeedUrl(id), 0, time.time(), rank, None, "None", 1)
815 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?, 1);", values)
818 feed = listing.getFeed(id)
819 new_feed = self.getFeed(id)
821 items = feed.getIds()[:]
824 if feed.isEntryRead(item):
828 date = timegm(feed.getDateTuple(item))
829 title = feed.getTitle(item)
830 newId = new_feed.generateUniqueId({"date":date, "title":title})
831 values = (newId, title , feed.getContentLink(item), date, tuple(time.time()), feed.getExternalLink(item), read_status)
832 new_feed.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
835 images = feed.getImages(item)
837 new_feed.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (item, image) )
841 self.updateUnread(id)
843 logger.error("importOldFormatFeeds: %s"
844 % (traceback.format_exc(),))
845 remove(self.configdir+"feeds.pickle")
848 def addArchivedArticle(self, key, index):
849 feed = self.getFeed(key)
850 title = feed.getTitle(index)
851 link = feed.getExternalLink(index)
852 date = feed.getDate(index)
853 count = self.db.execute("SELECT count(*) FROM feeds where id=?;", ("ArchivedArticles",) ).fetchone()[0]
855 self.addFeed("Archived Articles", "", id="ArchivedArticles")
857 archFeed = self.getFeed("ArchivedArticles")
858 archFeed.addArchivedArticle(title, link, date, self.configdir)
859 self.updateUnread("ArchivedArticles")
861 def updateFeed(self, key, expiryTime=None, proxy=None, imageCache=None,
863 if expiryTime is None:
864 expiryTime = self.config.getExpiry()
866 # Default to 24 hours
869 (use_proxy, proxy) = self.config.getProxy()
872 if imageCache is None:
873 imageCache = self.config.getImageCache()
875 feed = self.getFeed(key)
876 (url, etag, modified) = self.db.execute("SELECT url, etag, modified FROM feeds WHERE id=?;", (key,) ).fetchone()
878 modified = time.struct_time(eval(modified))
882 self.configdir, url, etag, modified, expiryTime, proxy, imageCache,
883 priority, postFeedUpdateFunc=self._queuePostFeedUpdate)
885 def _queuePostFeedUpdate(self, *args, **kwargs):
886 mainthread.execute (self._postFeedUpdate, async=True, *args, **kwargs)
888 def _postFeedUpdate(self, key, updateTime, etag, modified, title):
892 modified=str(tuple(modified))
894 self.db.execute("UPDATE feeds SET updateTime=?, etag=?, modified=? WHERE id=?;", (updateTime, etag, modified, key) )
896 self.db.execute("UPDATE feeds SET etag=?, modified=? WHERE id=?;", (etag, modified, key) )
898 if title is not None:
899 self.db.execute("UPDATE feeds SET title=(case WHEN title=='' THEN ? ELSE title END) where id=?;",
902 self.updateUnread(key)
904 update_server_object().ArticleCountUpdated()
906 stats = JobManager().stats()
908 completed = stats['jobs-completed'] - jobs_at_start
909 in_progress = stats['jobs-in-progress']
910 queued = stats['jobs-queued']
912 percent = (100 * ((completed + in_progress / 2.))
913 / (completed + in_progress + queued))
915 update_server_object().UpdateProgress(
916 percent, completed, in_progress, queued, 0, 0, 0, key)
918 if in_progress == 0 and queued == 0:
919 jobs_at_start = stats['jobs-completed']
921 def getFeed(self, key):
922 if key == "ArchivedArticles":
923 return ArchivedArticles(self.configdir, key)
924 return Feed(self.configdir, key)
926 def editFeed(self, key, title, url, category=None):
928 self.db.execute("UPDATE feeds SET title=?, url=?, category=? WHERE id=?;", (title, url, category, key))
930 self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
935 wc()[key].human_readable_name = title
937 logger.debug("Feed %s (%s) unknown." % (key, title))
939 def getFeedUpdateTime(self, key):
940 return time.ctime(self.db.execute("SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0])
942 def getFeedNumberOfUnreadItems(self, key):
943 return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
945 def getFeedTitle(self, key):
946 (title, url) = self.db.execute("SELECT title, url FROM feeds WHERE id=?;", (key,)).fetchone()
951 def getFeedUrl(self, key):
952 return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
954 def getFeedCategory(self, key):
955 return self.db.execute("SELECT category FROM feeds WHERE id=?;", (key,)).fetchone()[0]
957 def getListOfFeeds(self, category=None):
959 rows = self.db.execute("SELECT id FROM feeds WHERE category=? ORDER BY rank;", (category, ) )
961 rows = self.db.execute("SELECT id FROM feeds ORDER BY rank;" )
968 def getListOfCategories(self):
969 rows = self.db.execute("SELECT id FROM categories ORDER BY rank;" )
976 def getCategoryTitle(self, id):
977 row = self.db.execute("SELECT title FROM categories WHERE id=?;", (id, )).fetchone()
980 def getSortedListOfKeys(self, order, onlyUnread=False, category=1):
981 if order == "Most unread":
982 tmp = "ORDER BY unread DESC"
983 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
984 elif order == "Least unread":
985 tmp = "ORDER BY unread"
986 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
987 elif order == "Most recent":
988 tmp = "ORDER BY updateTime DESC"
989 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
990 elif order == "Least recent":
991 tmp = "ORDER BY updateTime"
992 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
993 else: # order == "Manual" or invalid value...
994 tmp = "ORDER BY rank"
995 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
997 sql = "SELECT id FROM feeds WHERE unread>0 AND category=%s " %category + tmp
999 sql = "SELECT id FROM feeds WHERE category=%s " %category + tmp
1000 rows = self.db.execute(sql)
1007 def getFavicon(self, key):
1008 filename = "%s%s.d/favicon.ico" % (self.configdir, key)
1009 if isfile(filename):
1014 def updateUnread(self, key):
1015 feed = self.getFeed(key)
1016 self.db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
1019 def addFeed(self, title, url, id=None, category=1):
1022 count = self.db.execute("SELECT count(*) FROM feeds WHERE id=?;", (id,) ).fetchone()[0]
1024 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1025 if max_rank == None:
1027 values = (id, title, url, 0, 0, max_rank+1, None, "None", 1, category)
1028 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?,?);", values)
1030 # Ask for the feed object, it will create the necessary tables
1033 if wc().available():
1034 # Register the stream with Woodchuck. Update approximately
1036 wc().stream_register(stream_identifier=id,
1037 human_readable_name=title,
1044 def addCategory(self, title):
1045 rank = self.db.execute("SELECT MAX(rank)+1 FROM categories;").fetchone()[0]
1048 id = self.db.execute("SELECT MAX(id)+1 FROM categories;").fetchone()[0]
1051 self.db.execute("INSERT INTO categories (id, title, unread, rank) VALUES (?, ?, 0, ?)", (id, title, rank))
1054 def removeFeed(self, key):
1055 if wc().available ():
1059 logger.debug("Removing unregistered feed %s failed" % (key,))
1061 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
1062 self.db.execute("DELETE FROM feeds WHERE id=?;", (key, ))
1063 self.db.execute("UPDATE feeds SET rank=rank-1 WHERE rank>?;", (rank,) )
1066 if isdir(self.configdir+key+".d/"):
1067 rmtree(self.configdir+key+".d/")
1069 def removeCategory(self, key):
1070 if self.db.execute("SELECT count(*) FROM categories;").fetchone()[0] > 1:
1071 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,) ).fetchone()[0]
1072 self.db.execute("DELETE FROM categories WHERE id=?;", (key, ))
1073 self.db.execute("UPDATE categories SET rank=rank-1 WHERE rank>?;", (rank,) )
1074 self.db.execute("UPDATE feeds SET category=1 WHERE category=?;", (key,) )
1077 #def saveConfig(self):
1078 # self.listOfFeeds["feedingit-order"] = self.sortedKeys
1079 # file = open(self.configdir+"feeds.pickle", "w")
1080 # pickle.dump(self.listOfFeeds, file)
1083 def moveUp(self, key):
1084 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1086 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank-1) )
1087 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank-1, key) )
1090 def moveCategoryUp(self, key):
1091 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1093 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank-1) )
1094 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank-1, key) )
1097 def moveDown(self, key):
1098 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1099 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1101 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank+1) )
1102 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank+1, key) )
1105 def moveCategoryDown(self, key):
1106 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1107 max_rank = self.db.execute("SELECT MAX(rank) FROM categories;").fetchone()[0]
1109 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank+1) )
1110 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank+1, key) )