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):
65 openers.append (progress_handler)
67 openers.append(HTTPProgressHandler(download_callback))
70 openers.append (proxy)
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(configdir, self.key, baseurl, img['src'], proxy=proxy)
363 img['src']="file://%s" %filename
364 count = self.db.execute("SELECT count(1) FROM images where id=? and imagePath=?;", (id, filename )).fetchone()[0]
366 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
370 object_size += os.path.getsize (filename)
371 except os.error, exception:
372 logger.error ("Error getting size of %s: %s"
373 % (filename, exception))
374 self.serial_execution_lock.acquire ()
375 have_serial_execution_lock = True
377 tmpEntry["contentLink"] = configdir+self.key+".d/"+id+".html"
378 file = open(tmpEntry["contentLink"], "w")
379 file.write(soup.prettify())
382 self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
385 values = (id, tmpEntry["title"], tmpEntry["contentLink"], tmpEntry["date"], currentTime, tmpEntry["link"], 0)
386 self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
390 # self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
392 # filename = configdir+self.key+".d/"+id+".html"
393 # file = open(filename,"a")
394 # utime(filename, None)
396 # images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
397 # for image in images:
398 # file = open(image[0],"a")
399 # utime(image[0], None)
404 # Register the object with Woodchuck and mark it as
409 obj = wc()[self.key].object_register(
410 object_identifier=id,
411 human_readable_name=tmpEntry["title"])
412 except woodchuck.ObjectExistsError:
413 obj = wc()[self.key][id]
415 # If the entry does not contain a publication
416 # time, the attribute won't exist.
417 pubtime = entry.get ('date_parsed', None)
419 obj.publication_time = time.mktime (pubtime)
421 received = (progress_handler.stats['received']
423 sent = progress_handler.stats['sent'] - sent_base
425 indicator=(woodchuck.Indicator.ApplicationVisual
426 |woodchuck.Indicator.StreamWide),
427 transferred_down=received,
429 object_size=object_size)
430 mainthread.execute(e, async=True)
434 "%s: Update successful: transferred: %d/%d; objects: %d)"
436 progress_handler.stats['sent'],
437 progress_handler.stats['received'],
439 mainthread.execute (wc_success, async=True)
442 rows = self.db.execute("SELECT id FROM feed WHERE (read=0 AND updated<?) OR (read=1 AND updated<?);", (currentTime-2*expiry, currentTime-expiry))
444 self.removeEntry(row[0])
446 from glob import glob
448 for file in glob(configdir+self.key+".d/*"):
452 # put the two dates into matching format
454 lastmodDate = stats[8]
456 expDate = time.time()-expiry*3
457 # check if image-last-modified-date is outdated
459 if expDate > lastmodDate:
463 #print 'Removing', file
465 # XXX: Tell woodchuck.
466 remove(file) # commented out for testing
468 except OSError, exception:
470 logger.error('Could not remove %s: %s'
471 % (file, str (exception)))
472 logger.debug("updated %s: %fs in download, %fs in processing"
473 % (self.key, download_duration,
474 time.time () - process_start))
476 logger.error("Updating %s: %s" % (self.key, traceback.format_exc()))
480 if have_serial_execution_lock:
481 self.serial_execution_lock.release ()
485 rows = self.db.execute("SELECT MAX(date) FROM feed;")
489 logger.error("Fetching update time: %s: %s"
490 % (str(e), traceback.format_exc()))
497 title = tmp.feed.title
498 except (AttributeError, UnboundLocalError), exception:
500 if postFeedUpdateFunc is not None:
501 postFeedUpdateFunc (self.key, updateTime, etag, modified,
502 title, *postFeedUpdateFuncArgs)
504 def setEntryRead(self, id):
505 self.db.execute("UPDATE feed SET read=1 WHERE id=?;", (id,) )
511 wc()[self.key][id].used()
515 def setEntryUnread(self, id):
516 self.db.execute("UPDATE feed SET read=0 WHERE id=?;", (id,) )
519 def markAllAsRead(self):
520 self.db.execute("UPDATE feed SET read=1 WHERE read=0;")
523 def isEntryRead(self, id):
524 read_status = self.db.execute("SELECT read FROM feed WHERE id=?;", (id,) ).fetchone()[0]
525 return read_status==1 # Returns True if read==1, and False if read==0
527 def getTitle(self, id):
528 return self.db.execute("SELECT title FROM feed WHERE id=?;", (id,) ).fetchone()[0]
530 def getContentLink(self, id):
531 return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
533 def getExternalLink(self, id):
534 return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
536 def getDate(self, id):
537 dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
538 return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(dateStamp))
540 def getDateTuple(self, id):
541 dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
542 return time.localtime(dateStamp)
544 def getDateStamp(self, id):
545 return self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
547 def generateUniqueId(self, entry):
549 Generate a stable identifier for the article. For the same
550 entry, this should result in the same identifier. If
551 possible, the identifier should remain the same even if the
554 # Prefer the entry's id, which is supposed to be globally
556 key = entry.get('id', None)
558 # Next, try the link to the content.
559 key = entry.get('link', None)
561 # Ok, the title and the date concatenated are likely to be
563 key = entry.get('title', None) + entry.get('date', None)
565 # Hmm, the article's content will at least guarantee no
566 # false negatives (i.e., missing articles)
567 key = entry.get('content', None)
569 # If all else fails, just use a random number.
570 key = str (random.random ())
573 def getIds(self, onlyUnread=False):
575 rows = self.db.execute("SELECT id FROM feed where read=0 ORDER BY date DESC;").fetchall()
577 rows = self.db.execute("SELECT id FROM feed ORDER BY date DESC;").fetchall()
584 def getNextId(self, id, forward=True):
590 index = ids.index(id)
591 return ids[(index + delta) % len(ids)]
593 def getPreviousId(self, id):
594 return self.getNextId(id, forward=False)
596 def getNumberOfUnreadItems(self):
597 return self.db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
599 def getNumberOfEntries(self):
600 return self.db.execute("SELECT count(*) FROM feed;").fetchone()[0]
602 def getArticle(self, entry):
603 #self.setEntryRead(id)
604 #entry = self.entries[id]
605 title = entry['title']
606 #content = entry.get('content', entry.get('summary_detail', {}))
607 content = entry["content"]
610 author = entry['author']
611 date = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(entry["date"]) )
613 #text = '''<div style="color: black; background-color: white;">'''
614 text = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
615 text += "<html><head><title>" + title + "</title>"
616 text += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
617 #text += '<style> body {-webkit-user-select: none;} </style>'
618 text += '</head><body bgcolor=\"#ffffff\"><div><a href=\"' + link + '\">' + title + "</a>"
620 text += "<BR /><small><i>Author: " + author + "</i></small>"
621 text += "<BR /><small><i>Date: " + date + "</i></small></div>"
622 text += "<BR /><BR />"
624 text += "</body></html>"
627 def getContent(self, id):
628 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
630 file = open(self.entries[id]["contentLink"])
631 content = file.read()
634 content = "Content unavailable"
637 def extractDate(self, entry):
638 if entry.has_key("updated_parsed"):
639 return timegm(entry["updated_parsed"])
640 elif entry.has_key("published_parsed"):
641 return timegm(entry["published_parsed"])
645 def extractContent(self, entry):
647 if entry.has_key('summary'):
648 content = entry.get('summary', '')
649 if entry.has_key('content'):
650 if len(entry.content[0].value) > len(content):
651 content = entry.content[0].value
653 content = entry.get('description', '')
656 def removeEntry(self, id):
657 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
661 except OSError, exception:
662 logger.error("Deleting %s: %s" % (contentLink, str (exception)))
663 self.db.execute("DELETE FROM feed WHERE id=?;", (id,) )
664 self.db.execute("DELETE FROM images WHERE id=?;", (id,) )
670 wc()[self.key][id].files_deleted (
671 woodchuck.DeletionResponse.Deleted)
672 del wc()[self.key][id]
675 mainthread.execute (e, async=True)
677 class ArchivedArticles(Feed):
678 def addArchivedArticle(self, title, link, date, configdir):
679 id = self.generateUniqueId({"date":date, "title":title})
680 values = (id, title, link, date, 0, link, 0)
681 self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
684 def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
686 rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
688 currentTime = time.time()
691 f = urllib2.urlopen(link)
692 #entry["content"] = f.read()
695 soup = BeautifulSoup(html)
699 filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
701 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
703 contentLink = configdir+self.key+".d/"+id+".html"
704 file = open(contentLink, "w")
705 file.write(soup.prettify())
708 self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
710 return (currentTime, None, None)
712 def purgeReadArticles(self):
713 rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
716 self.removeArticle(row[0])
718 def removeArticle(self, id):
719 rows = self.db.execute("SELECT imagePath FROM images WHERE id=?;", (id,) )
722 count = self.db.execute("SELECT count(*) FROM images WHERE id!=? and imagePath=?;", (id,row[0]) ).fetchone()[0]
733 except AttributeError:
734 db = sqlite3.connect("%s/feeds.db" % self.configdir, timeout=120)
737 db = property(_getdb)
739 # Lists all the feeds in a dictionary, and expose the data
740 def __init__(self, config, configdir):
742 self.configdir = configdir
744 self.tls = threading.local ()
747 table = self.db.execute("SELECT sql FROM sqlite_master").fetchone()
749 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);")
750 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
751 self.addCategory("Default Category")
752 if isfile(self.configdir+"feeds.pickle"):
753 self.importOldFormatFeeds()
755 self.addFeed("Maemo News", "http://maemo.org/news/items.xml")
757 from string import find, upper
758 if find(upper(table[0]), "WIDGET")<0:
759 self.db.execute("ALTER TABLE feeds ADD COLUMN widget int;")
760 self.db.execute("UPDATE feeds SET widget=1;")
762 if find(upper(table[0]), "CATEGORY")<0:
763 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
764 self.addCategory("Default Category")
765 self.db.execute("ALTER TABLE feeds ADD COLUMN category int;")
766 self.db.execute("UPDATE feeds SET category=1;")
771 # Check that Woodchuck's state is up to date with respect our
773 updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
774 wc_init (self, True if updater else False)
775 if wc().available() and updater:
776 # The list of known streams.
777 streams = wc().streams_list ()
778 stream_ids = [s.identifier for s in streams]
780 # Register any unknown streams. Remove known streams from
782 for key in self.getListOfFeeds():
783 title = self.getFeedTitle(key)
784 # XXX: We should also check whether the list of
785 # articles/objects in each feed/stream is up to date.
786 if key not in stream_ids:
788 "Registering previously unknown channel: %s (%s)"
790 # Use a default refresh interval of 6 hours.
791 wc().stream_register (key, title, 6 * 60 * 60)
793 # Make sure the human readable name is up to date.
794 if wc()[key].human_readable_name != title:
795 wc()[key].human_readable_name = title
796 stream_ids.remove (key)
799 # Unregister any streams that are no longer subscribed to.
800 for id in stream_ids:
801 logger.debug("Unregistering %s" % (id,))
802 w.stream_unregister (id)
804 def importOldFormatFeeds(self):
805 """This function loads feeds that are saved in an outdated format, and converts them to sqlite"""
807 listing = rss.Listing(self.configdir)
809 for id in listing.getListOfFeeds():
812 values = (id, listing.getFeedTitle(id) , listing.getFeedUrl(id), 0, time.time(), rank, None, "None", 1)
813 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?, 1);", values)
816 feed = listing.getFeed(id)
817 new_feed = self.getFeed(id)
819 items = feed.getIds()[:]
822 if feed.isEntryRead(item):
826 date = timegm(feed.getDateTuple(item))
827 title = feed.getTitle(item)
828 newId = new_feed.generateUniqueId({"date":date, "title":title})
829 values = (newId, title , feed.getContentLink(item), date, tuple(time.time()), feed.getExternalLink(item), read_status)
830 new_feed.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
833 images = feed.getImages(item)
835 new_feed.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (item, image) )
839 self.updateUnread(id)
841 logger.error("importOldFormatFeeds: %s"
842 % (traceback.format_exc(),))
843 remove(self.configdir+"feeds.pickle")
846 def addArchivedArticle(self, key, index):
847 feed = self.getFeed(key)
848 title = feed.getTitle(index)
849 link = feed.getExternalLink(index)
850 date = feed.getDate(index)
851 count = self.db.execute("SELECT count(*) FROM feeds where id=?;", ("ArchivedArticles",) ).fetchone()[0]
853 self.addFeed("Archived Articles", "", id="ArchivedArticles")
855 archFeed = self.getFeed("ArchivedArticles")
856 archFeed.addArchivedArticle(title, link, date, self.configdir)
857 self.updateUnread("ArchivedArticles")
859 def updateFeed(self, key, expiryTime=None, proxy=None, imageCache=None,
861 if expiryTime is None:
862 expiryTime = self.config.getExpiry()
864 # Default to 24 hours
867 (use_proxy, proxy) = self.config.getProxy()
870 if imageCache is None:
871 imageCache = self.config.getImageCache()
873 feed = self.getFeed(key)
874 (url, etag, modified) = self.db.execute("SELECT url, etag, modified FROM feeds WHERE id=?;", (key,) ).fetchone()
876 modified = time.struct_time(eval(modified))
880 self.configdir, url, etag, modified, expiryTime, proxy, imageCache,
881 priority, postFeedUpdateFunc=self._queuePostFeedUpdate)
883 def _queuePostFeedUpdate(self, *args, **kwargs):
884 mainthread.execute (self._postFeedUpdate, async=True, *args, **kwargs)
886 def _postFeedUpdate(self, key, updateTime, etag, modified, title):
890 modified=str(tuple(modified))
892 self.db.execute("UPDATE feeds SET updateTime=?, etag=?, modified=? WHERE id=?;", (updateTime, etag, modified, key) )
894 self.db.execute("UPDATE feeds SET etag=?, modified=? WHERE id=?;", (etag, modified, key) )
896 if title is not None:
897 self.db.execute("UPDATE feeds SET title=(case WHEN title=='' THEN ? ELSE title END) where id=?;",
900 self.updateUnread(key)
902 update_server_object().ArticleCountUpdated()
904 stats = JobManager().stats()
906 completed = stats['jobs-completed'] - jobs_at_start
907 in_progress = stats['jobs-in-progress']
908 queued = stats['jobs-queued']
910 percent = (100 * ((completed + in_progress / 2.))
911 / (completed + in_progress + queued))
913 update_server_object().UpdateProgress(
914 percent, completed, in_progress, queued, 0, 0, 0, key)
916 if in_progress == 0 and queued == 0:
917 jobs_at_start = stats['jobs-completed']
919 def getFeed(self, key):
920 if key == "ArchivedArticles":
921 return ArchivedArticles(self.configdir, key)
922 return Feed(self.configdir, key)
924 def editFeed(self, key, title, url, category=None):
926 self.db.execute("UPDATE feeds SET title=?, url=?, category=? WHERE id=?;", (title, url, category, key))
928 self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
933 wc()[key].human_readable_name = title
935 logger.debug("Feed %s (%s) unknown." % (key, title))
937 def getFeedUpdateTime(self, key):
938 return time.ctime(self.db.execute("SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0])
940 def getFeedNumberOfUnreadItems(self, key):
941 return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
943 def getFeedTitle(self, key):
944 (title, url) = self.db.execute("SELECT title, url FROM feeds WHERE id=?;", (key,)).fetchone()
949 def getFeedUrl(self, key):
950 return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
952 def getFeedCategory(self, key):
953 return self.db.execute("SELECT category FROM feeds WHERE id=?;", (key,)).fetchone()[0]
955 def getListOfFeeds(self, category=None):
957 rows = self.db.execute("SELECT id FROM feeds WHERE category=? ORDER BY rank;", (category, ) )
959 rows = self.db.execute("SELECT id FROM feeds ORDER BY rank;" )
966 def getListOfCategories(self):
967 rows = self.db.execute("SELECT id FROM categories ORDER BY rank;" )
974 def getCategoryTitle(self, id):
975 row = self.db.execute("SELECT title FROM categories WHERE id=?;", (id, )).fetchone()
978 def getSortedListOfKeys(self, order, onlyUnread=False, category=1):
979 if order == "Most unread":
980 tmp = "ORDER BY unread DESC"
981 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
982 elif order == "Least unread":
983 tmp = "ORDER BY unread"
984 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
985 elif order == "Most recent":
986 tmp = "ORDER BY updateTime DESC"
987 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
988 elif order == "Least recent":
989 tmp = "ORDER BY updateTime"
990 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
991 else: # order == "Manual" or invalid value...
992 tmp = "ORDER BY rank"
993 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
995 sql = "SELECT id FROM feeds WHERE unread>0 AND category=%s " %category + tmp
997 sql = "SELECT id FROM feeds WHERE category=%s " %category + tmp
998 rows = self.db.execute(sql)
1005 def getFavicon(self, key):
1006 filename = "%s%s.d/favicon.ico" % (self.configdir, key)
1007 if isfile(filename):
1012 def updateUnread(self, key):
1013 feed = self.getFeed(key)
1014 self.db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
1017 def addFeed(self, title, url, id=None, category=1):
1020 count = self.db.execute("SELECT count(*) FROM feeds WHERE id=?;", (id,) ).fetchone()[0]
1022 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1023 if max_rank == None:
1025 values = (id, title, url, 0, 0, max_rank+1, None, "None", 1, category)
1026 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?,?);", values)
1028 # Ask for the feed object, it will create the necessary tables
1031 if wc().available():
1032 # Register the stream with Woodchuck. Update approximately
1034 wc().stream_register(stream_identifier=id,
1035 human_readable_name=title,
1042 def addCategory(self, title):
1043 rank = self.db.execute("SELECT MAX(rank)+1 FROM categories;").fetchone()[0]
1046 id = self.db.execute("SELECT MAX(id)+1 FROM categories;").fetchone()[0]
1049 self.db.execute("INSERT INTO categories (id, title, unread, rank) VALUES (?, ?, 0, ?)", (id, title, rank))
1052 def removeFeed(self, key):
1053 if wc().available ():
1057 logger.debug("Removing unregistered feed %s failed" % (key,))
1059 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
1060 self.db.execute("DELETE FROM feeds WHERE id=?;", (key, ))
1061 self.db.execute("UPDATE feeds SET rank=rank-1 WHERE rank>?;", (rank,) )
1064 if isdir(self.configdir+key+".d/"):
1065 rmtree(self.configdir+key+".d/")
1067 def removeCategory(self, key):
1068 if self.db.execute("SELECT count(*) FROM categories;").fetchone()[0] > 1:
1069 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,) ).fetchone()[0]
1070 self.db.execute("DELETE FROM categories WHERE id=?;", (key, ))
1071 self.db.execute("UPDATE categories SET rank=rank-1 WHERE rank>?;", (rank,) )
1072 self.db.execute("UPDATE feeds SET category=1 WHERE category=?;", (key,) )
1075 #def saveConfig(self):
1076 # self.listOfFeeds["feedingit-order"] = self.sortedKeys
1077 # file = open(self.configdir+"feeds.pickle", "w")
1078 # pickle.dump(self.listOfFeeds, file)
1081 def moveUp(self, key):
1082 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1084 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank-1) )
1085 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank-1, key) )
1088 def moveCategoryUp(self, key):
1089 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1091 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank-1) )
1092 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank-1, key) )
1095 def moveDown(self, key):
1096 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1097 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1099 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank+1) )
1100 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank+1, key) )
1103 def moveCategoryDown(self, key):
1104 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1105 max_rank = self.db.execute("SELECT MAX(rank) FROM categories;").fetchone()[0]
1107 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank+1) )
1108 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank+1, key) )