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):
586 index = ids.index(id)
587 return ids[(index+1)%len(ids)]
589 def getPreviousId(self, id):
591 index = ids.index(id)
592 return ids[(index-1)%len(ids)]
594 def getNumberOfUnreadItems(self):
595 return self.db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
597 def getNumberOfEntries(self):
598 return self.db.execute("SELECT count(*) FROM feed;").fetchone()[0]
600 def getArticle(self, entry):
601 #self.setEntryRead(id)
602 #entry = self.entries[id]
603 title = entry['title']
604 #content = entry.get('content', entry.get('summary_detail', {}))
605 content = entry["content"]
608 author = entry['author']
609 date = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(entry["date"]) )
611 #text = '''<div style="color: black; background-color: white;">'''
612 text = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
613 text += "<html><head><title>" + title + "</title>"
614 text += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
615 #text += '<style> body {-webkit-user-select: none;} </style>'
616 text += '</head><body bgcolor=\"#ffffff\"><div><a href=\"' + link + '\">' + title + "</a>"
618 text += "<BR /><small><i>Author: " + author + "</i></small>"
619 text += "<BR /><small><i>Date: " + date + "</i></small></div>"
620 text += "<BR /><BR />"
622 text += "</body></html>"
625 def getContent(self, id):
626 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
628 file = open(self.entries[id]["contentLink"])
629 content = file.read()
632 content = "Content unavailable"
635 def extractDate(self, entry):
636 if entry.has_key("updated_parsed"):
637 return timegm(entry["updated_parsed"])
638 elif entry.has_key("published_parsed"):
639 return timegm(entry["published_parsed"])
643 def extractContent(self, entry):
645 if entry.has_key('summary'):
646 content = entry.get('summary', '')
647 if entry.has_key('content'):
648 if len(entry.content[0].value) > len(content):
649 content = entry.content[0].value
651 content = entry.get('description', '')
654 def removeEntry(self, id):
655 contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
659 except OSError, exception:
660 logger.error("Deleting %s: %s" % (contentLink, str (exception)))
661 self.db.execute("DELETE FROM feed WHERE id=?;", (id,) )
662 self.db.execute("DELETE FROM images WHERE id=?;", (id,) )
668 wc()[self.key][id].files_deleted (
669 woodchuck.DeletionResponse.Deleted)
670 del wc()[self.key][id]
673 mainthread.execute (e, async=True)
675 class ArchivedArticles(Feed):
676 def addArchivedArticle(self, title, link, date, configdir):
677 id = self.generateUniqueId({"date":date, "title":title})
678 values = (id, title, link, date, 0, link, 0)
679 self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
682 def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
684 rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
686 currentTime = time.time()
689 f = urllib2.urlopen(link)
690 #entry["content"] = f.read()
693 soup = BeautifulSoup(html)
697 filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
699 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
701 contentLink = configdir+self.key+".d/"+id+".html"
702 file = open(contentLink, "w")
703 file.write(soup.prettify())
706 self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
708 return (currentTime, None, None)
710 def purgeReadArticles(self):
711 rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
714 self.removeArticle(row[0])
716 def removeArticle(self, id):
717 rows = self.db.execute("SELECT imagePath FROM images WHERE id=?;", (id,) )
720 count = self.db.execute("SELECT count(*) FROM images WHERE id!=? and imagePath=?;", (id,row[0]) ).fetchone()[0]
731 except AttributeError:
732 db = sqlite3.connect("%s/feeds.db" % self.configdir, timeout=120)
735 db = property(_getdb)
737 # Lists all the feeds in a dictionary, and expose the data
738 def __init__(self, config, configdir):
740 self.configdir = configdir
742 self.tls = threading.local ()
745 table = self.db.execute("SELECT sql FROM sqlite_master").fetchone()
747 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);")
748 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
749 self.addCategory("Default Category")
750 if isfile(self.configdir+"feeds.pickle"):
751 self.importOldFormatFeeds()
753 self.addFeed("Maemo News", "http://maemo.org/news/items.xml")
755 from string import find, upper
756 if find(upper(table[0]), "WIDGET")<0:
757 self.db.execute("ALTER TABLE feeds ADD COLUMN widget int;")
758 self.db.execute("UPDATE feeds SET widget=1;")
760 if find(upper(table[0]), "CATEGORY")<0:
761 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
762 self.addCategory("Default Category")
763 self.db.execute("ALTER TABLE feeds ADD COLUMN category int;")
764 self.db.execute("UPDATE feeds SET category=1;")
769 # Check that Woodchuck's state is up to date with respect our
771 updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
772 wc_init (self, True if updater else False)
773 if wc().available() and updater:
774 # The list of known streams.
775 streams = wc().streams_list ()
776 stream_ids = [s.identifier for s in streams]
778 # Register any unknown streams. Remove known streams from
780 for key in self.getListOfFeeds():
781 title = self.getFeedTitle(key)
782 # XXX: We should also check whether the list of
783 # articles/objects in each feed/stream is up to date.
784 if key not in stream_ids:
786 "Registering previously unknown channel: %s (%s)"
788 # Use a default refresh interval of 6 hours.
789 wc().stream_register (key, title, 6 * 60 * 60)
791 # Make sure the human readable name is up to date.
792 if wc()[key].human_readable_name != title:
793 wc()[key].human_readable_name = title
794 stream_ids.remove (key)
797 # Unregister any streams that are no longer subscribed to.
798 for id in stream_ids:
799 logger.debug("Unregistering %s" % (id,))
800 w.stream_unregister (id)
802 def importOldFormatFeeds(self):
803 """This function loads feeds that are saved in an outdated format, and converts them to sqlite"""
805 listing = rss.Listing(self.configdir)
807 for id in listing.getListOfFeeds():
810 values = (id, listing.getFeedTitle(id) , listing.getFeedUrl(id), 0, time.time(), rank, None, "None", 1)
811 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?, 1);", values)
814 feed = listing.getFeed(id)
815 new_feed = self.getFeed(id)
817 items = feed.getIds()[:]
820 if feed.isEntryRead(item):
824 date = timegm(feed.getDateTuple(item))
825 title = feed.getTitle(item)
826 newId = new_feed.generateUniqueId({"date":date, "title":title})
827 values = (newId, title , feed.getContentLink(item), date, tuple(time.time()), feed.getExternalLink(item), read_status)
828 new_feed.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
831 images = feed.getImages(item)
833 new_feed.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (item, image) )
837 self.updateUnread(id)
839 logger.error("importOldFormatFeeds: %s"
840 % (traceback.format_exc(),))
841 remove(self.configdir+"feeds.pickle")
844 def addArchivedArticle(self, key, index):
845 feed = self.getFeed(key)
846 title = feed.getTitle(index)
847 link = feed.getExternalLink(index)
848 date = feed.getDate(index)
849 count = self.db.execute("SELECT count(*) FROM feeds where id=?;", ("ArchivedArticles",) ).fetchone()[0]
851 self.addFeed("Archived Articles", "", id="ArchivedArticles")
853 archFeed = self.getFeed("ArchivedArticles")
854 archFeed.addArchivedArticle(title, link, date, self.configdir)
855 self.updateUnread("ArchivedArticles")
857 def updateFeed(self, key, expiryTime=None, proxy=None, imageCache=None,
859 if expiryTime is None:
860 expiryTime = self.config.getExpiry()
862 # Default to 24 hours
865 (use_proxy, proxy) = self.config.getProxy()
868 if imageCache is None:
869 imageCache = self.config.getImageCache()
871 feed = self.getFeed(key)
872 (url, etag, modified) = self.db.execute("SELECT url, etag, modified FROM feeds WHERE id=?;", (key,) ).fetchone()
874 modified = time.struct_time(eval(modified))
878 self.configdir, url, etag, modified, expiryTime, proxy, imageCache,
879 priority, postFeedUpdateFunc=self._queuePostFeedUpdate)
881 def _queuePostFeedUpdate(self, *args, **kwargs):
882 mainthread.execute (self._postFeedUpdate, async=True, *args, **kwargs)
884 def _postFeedUpdate(self, key, updateTime, etag, modified, title):
888 modified=str(tuple(modified))
890 self.db.execute("UPDATE feeds SET updateTime=?, etag=?, modified=? WHERE id=?;", (updateTime, etag, modified, key) )
892 self.db.execute("UPDATE feeds SET etag=?, modified=? WHERE id=?;", (etag, modified, key) )
894 if title is not None:
895 self.db.execute("UPDATE feeds SET title=(case WHEN title=='' THEN ? ELSE title END) where id=?;",
898 self.updateUnread(key)
900 update_server_object().ArticleCountUpdated()
902 stats = JobManager().stats()
904 completed = stats['jobs-completed'] - jobs_at_start
905 in_progress = stats['jobs-in-progress']
906 queued = stats['jobs-queued']
909 percent = (100 * ((completed + in_progress / 2.))
910 / (completed + in_progress + queued))
911 except ZeroDivisionError:
914 update_server_object().UpdateProgress(
915 percent, completed, in_progress, queued, 0, 0, 0, key)
917 if in_progress == 0 and queued == 0:
918 jobs_at_start = stats['jobs-completed']
920 def getFeed(self, key):
921 if key == "ArchivedArticles":
922 return ArchivedArticles(self.configdir, key)
923 return Feed(self.configdir, key)
925 def editFeed(self, key, title, url, category=None):
927 self.db.execute("UPDATE feeds SET title=?, url=?, category=? WHERE id=?;", (title, url, category, key))
929 self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
934 wc()[key].human_readable_name = title
936 logger.debug("Feed %s (%s) unknown." % (key, title))
938 def getFeedUpdateTime(self, key):
939 return time.ctime(self.db.execute("SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0])
941 def getFeedNumberOfUnreadItems(self, key):
942 return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
944 def getFeedTitle(self, key):
945 (title, url) = self.db.execute("SELECT title, url FROM feeds WHERE id=?;", (key,)).fetchone()
950 def getFeedUrl(self, key):
951 return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
953 def getFeedCategory(self, key):
954 return self.db.execute("SELECT category FROM feeds WHERE id=?;", (key,)).fetchone()[0]
956 def getListOfFeeds(self, category=None):
958 rows = self.db.execute("SELECT id FROM feeds WHERE category=? ORDER BY rank;", (category, ) )
960 rows = self.db.execute("SELECT id FROM feeds ORDER BY rank;" )
967 def getListOfCategories(self):
968 rows = self.db.execute("SELECT id FROM categories ORDER BY rank;" )
975 def getCategoryTitle(self, id):
976 row = self.db.execute("SELECT title FROM categories WHERE id=?;", (id, )).fetchone()
979 def getSortedListOfKeys(self, order, onlyUnread=False, category=1):
980 if order == "Most unread":
981 tmp = "ORDER BY unread DESC"
982 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
983 elif order == "Least unread":
984 tmp = "ORDER BY unread"
985 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
986 elif order == "Most recent":
987 tmp = "ORDER BY updateTime DESC"
988 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
989 elif order == "Least recent":
990 tmp = "ORDER BY updateTime"
991 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
992 else: # order == "Manual" or invalid value...
993 tmp = "ORDER BY rank"
994 #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
996 sql = "SELECT id FROM feeds WHERE unread>0 AND category=%s " %category + tmp
998 sql = "SELECT id FROM feeds WHERE category=%s " %category + tmp
999 rows = self.db.execute(sql)
1006 def getFavicon(self, key):
1007 filename = "%s%s.d/favicon.ico" % (self.configdir, key)
1008 if isfile(filename):
1013 def updateUnread(self, key):
1014 feed = self.getFeed(key)
1015 self.db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
1018 def addFeed(self, title, url, id=None, category=1):
1021 count = self.db.execute("SELECT count(*) FROM feeds WHERE id=?;", (id,) ).fetchone()[0]
1023 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1024 if max_rank == None:
1026 values = (id, title, url, 0, 0, max_rank+1, None, "None", 1, category)
1027 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?,?);", values)
1029 # Ask for the feed object, it will create the necessary tables
1032 if wc().available():
1033 # Register the stream with Woodchuck. Update approximately
1035 wc().stream_register(stream_identifier=id,
1036 human_readable_name=title,
1043 def addCategory(self, title):
1044 rank = self.db.execute("SELECT MAX(rank)+1 FROM categories;").fetchone()[0]
1047 id = self.db.execute("SELECT MAX(id)+1 FROM categories;").fetchone()[0]
1050 self.db.execute("INSERT INTO categories (id, title, unread, rank) VALUES (?, ?, 0, ?)", (id, title, rank))
1053 def removeFeed(self, key):
1054 if wc().available ():
1058 logger.debug("Removing unregistered feed %s failed" % (key,))
1060 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
1061 self.db.execute("DELETE FROM feeds WHERE id=?;", (key, ))
1062 self.db.execute("UPDATE feeds SET rank=rank-1 WHERE rank>?;", (rank,) )
1065 if isdir(self.configdir+key+".d/"):
1066 rmtree(self.configdir+key+".d/")
1068 def removeCategory(self, key):
1069 if self.db.execute("SELECT count(*) FROM categories;").fetchone()[0] > 1:
1070 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,) ).fetchone()[0]
1071 self.db.execute("DELETE FROM categories WHERE id=?;", (key, ))
1072 self.db.execute("UPDATE categories SET rank=rank-1 WHERE rank>?;", (rank,) )
1073 self.db.execute("UPDATE feeds SET category=1 WHERE category=?;", (key,) )
1076 #def saveConfig(self):
1077 # self.listOfFeeds["feedingit-order"] = self.sortedKeys
1078 # file = open(self.configdir+"feeds.pickle", "w")
1079 # pickle.dump(self.listOfFeeds, file)
1082 def moveUp(self, key):
1083 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1085 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank-1) )
1086 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank-1, key) )
1089 def moveCategoryUp(self, key):
1090 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1092 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank-1) )
1093 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank-1, key) )
1096 def moveDown(self, key):
1097 rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
1098 max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
1100 self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank+1) )
1101 self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank+1, key) )
1104 def moveCategoryDown(self, key):
1105 rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
1106 max_rank = self.db.execute("SELECT MAX(rank) FROM categories;").fetchone()[0]
1108 self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank+1) )
1109 self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank+1, key) )