psa: Full width articles
authorYves Marcoz <yves@marcoz.org>
Fri, 6 Jan 2012 05:02:37 +0000 (21:02 -0800)
committerYves Marcoz <yves@marcoz.org>
Fri, 6 Jan 2012 05:02:37 +0000 (21:02 -0800)
28 files changed:
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/build/scripts-2.6/feedingit
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/changelog
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/DEBIAN/control
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/DEBIAN/md5sums
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/bin/feedingit
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/applications/feedingit.desktop
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/doc/feedingit/changelog.Debian.gz
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/feedingit/config.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/feedingit/qml/ArticleDisplay.qml
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/feedingit/qml/ArticleViewer.qml
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/feedingit/rss_sqlite.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/feedingit/wc.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/rules
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/feedingit
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/feedingit.desktop
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/pysrc/config.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/pysrc/rss_sqlite.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/pysrc/wc.py
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/qml/ArticleDisplay.qml
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/qml/ArticleViewer.qml
psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/setup.py
psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1.debian.tar.gz
psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1.dsc
psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1_all.deb
psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1_i386.changes
psa_harmattan/feedingit/deb_dist/feedingit_0.1.0.orig.tar.gz
psa_harmattan/feedingit/qml/ArticleDisplay.qml
psa_harmattan/feedingit/qml/ArticleViewer.qml

index 3903c7e..698f459 100755 (executable)
@@ -1,8 +1,11 @@
 #!/bin/sh
 
 case "$1" in
+update)
+    dbus-send --print-reply --dest='org.marcoz.feedingit' --session /org/marcoz/feedingit/update org.marcoz.feedingit.UpdateAll
+    ;;
 dbus)
-    nice python /usr/share/feedingit/update_feeds.py
+    nice python /usr/share/feedingit/update_feeds.py --daemon
     ;;
 *)
   cd /usr/share/feedingit
index db01aab..c7198f6 100644 (file)
@@ -2,4 +2,4 @@ feedingit (0.1.0-1) unstable; urgency=low
 
   * source package automatically created by stdeb 0.6.0+git
 
- -- Slocan <slocan@marcoz.org>  Tue, 29 Nov 2011 21:32:12 -0800
+ -- Slocan <slocan@marcoz.org>  Thu, 05 Jan 2012 20:58:44 -0800
index f1194c5..65e43f3 100644 (file)
@@ -2,7 +2,7 @@ Package: feedingit
 Version: 0.1.0-1
 Architecture: all
 Maintainer: Slocan <slocan@marcoz.org>
-Installed-Size: 792
+Installed-Size: 816
 Depends: python-pyside.qtgui, python-pyside.qtopengl, python-pyside.qtdeclarative, python-dbus, python-gconf
 Breaks: python (<< 2.6)
 Section: user/network
index 92861b4..ef6b60f 100644 (file)
@@ -1,11 +1,11 @@
-4420c31f88de68fe6e1b7637abb06196  usr/bin/feedingit
-7fe75a25d71f563a6391a9dbebd259f8  usr/share/applications/feedingit.desktop
+ab6be1fce6e02f7f10857cd79755bf18  usr/bin/feedingit
+1af83cdf3d338e6460a92c00537c375b  usr/share/applications/feedingit.desktop
 eda8cc6ffe8d842d6dfe0244b01b3042  usr/share/dbus-1/services/feedingit_status.service
-bc91edfc49d3edeec67619bea0bfba5d  usr/share/doc/feedingit/changelog.Debian.gz
+3f2da2596c2f5788fee074a589c9bbd0  usr/share/doc/feedingit/changelog.Debian.gz
 bac2be6ae9673ee5096e20e8b714c9cd  usr/share/feedingit/BeautifulSoup.py
 8f6e980f2e1154103a90763be6c00dc4  usr/share/feedingit/XmlHandler.py
 d41d8cd98f00b204e9800998ecf8427e  usr/share/feedingit/__init__.py
-f35343aa0ea95526b6af4ed8193526c2  usr/share/feedingit/config.py
+3485a6a12a47b67c26e4134c558cc3c9  usr/share/feedingit/config.py
 b4b00de5dccaf56d81a9dab1eeac63e1  usr/share/feedingit/debugging.py
 fae02e730b76761d43a626fe19828d5e  usr/share/feedingit/download.py
 4db69341f53742ba38afb8173ae9ef32  usr/share/feedingit/feedingit.py
@@ -16,8 +16,9 @@ f1e9ba0f44786f513659a7fa3111fc8a  usr/share/feedingit/jobmanager.py
 d9c0665dfdd5cf19f1529ce88af95134  usr/share/feedingit/opml.py
 7c3358bb2e3cba866248e75556a159d3  usr/share/feedingit/opml_lib.py
 4ef0e77dd5685db3a51306fd7ccf585f  usr/share/feedingit/qml/AddFeed.qml
-af27062fdba0bc7a3df92116e8340d19  usr/share/feedingit/qml/ArticleDisplay.qml
-0931ce1681f6691f2e229c6a5bdcb993  usr/share/feedingit/qml/ArticleViewer.qml
+538dac5d47b6d46b428d71b8c579e419  usr/share/feedingit/qml/ArticleDelegate.qml
+6a0c407a7931a4b362bd07921f70c009  usr/share/feedingit/qml/ArticleDisplay.qml
+d6b1c6bb6e2d09cf95c1db69227ccdd2  usr/share/feedingit/qml/ArticleViewer.qml
 15083e9a1fac05c8efaaa085dfabcbcb  usr/share/feedingit/qml/Articles.qml
 77bf6a1d2d0f265ee16c492886f96ede  usr/share/feedingit/qml/AutomaticUpdate.qml
 bd7579a3d822222caca98684212c4f42  usr/share/feedingit/qml/Categories.qml
@@ -62,10 +63,11 @@ af11b73b195513d08c17723b41db0b04  usr/share/feedingit/qml/common/images/wmBackIc
 7790a99425dd7c1046e6ae3b1ee72a03  usr/share/feedingit/qml/i18n/qml_en.qm
 1674fcce45bcf3319e61d19a9adf4fdd  usr/share/feedingit/qml/i18n/qml_en.ts
 5e9106fb8689ca72675d28123b36c2a5  usr/share/feedingit/qml/main.qml
-e3b4184531f29bbf86d812f9b8adacc2  usr/share/feedingit/rss_sqlite.py
+d6a20bd58cfc71b071bc2de09bf1518f  usr/share/feedingit/rss_sqlite.py
+6e88950ff9c416c362c6ca17cf4317f7  usr/share/feedingit/splash.jpg
 721777a26cd2a5b8466ce2aa2b99fad7  usr/share/feedingit/update_feeds.py
 6ccf12dc4379e91800ae8505b2e86082  usr/share/feedingit/updatedbus.py
-833ff79caab7c1fa89d6ff4a2f3bb3fd  usr/share/feedingit/wc.py
+686f9bcf1180690c08f64b1ef6a10d97  usr/share/feedingit/wc.py
 a30b3cb2decc0a3de4cafc18ca739d7a  usr/share/icons/hicolor/80x80/apps/feedingit.png
 3b48b22b6732560282d323eb8a638967  usr/share/pyshared/feedingit-0.1.0.egg-info
 1ce7b7194658769bb4173134a725d1ce  usr/share/python/runtime.d/feedingit.rtupdate
index 3903c7e..698f459 100755 (executable)
@@ -1,8 +1,11 @@
 #!/bin/sh
 
 case "$1" in
+update)
+    dbus-send --print-reply --dest='org.marcoz.feedingit' --session /org/marcoz/feedingit/update org.marcoz.feedingit.UpdateAll
+    ;;
 dbus)
-    nice python /usr/share/feedingit/update_feeds.py
+    nice python /usr/share/feedingit/update_feeds.py --daemon
     ;;
 *)
   cd /usr/share/feedingit
index 544c906..95bf65c 100644 (file)
@@ -3,6 +3,6 @@ Encoding=UTF-8
 Version=1.0
 Type=Application
 Name=FeedingIt RSS Reader
-Exec=invoker --single-instance --type=e /usr/bin/feedingit
+Exec=invoker --single-instance --splash=/usr/share/feedingit/splash.jpg --type=e /usr/bin/python /usr/share/feedingit/feedingit.py
 Icon=/usr/share/icons/hicolor/80x80/apps/feedingit.png
 Categories=Network;News;
index d830bf3..9908032 100644 (file)
Binary files a/psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/doc/feedingit/changelog.Debian.gz and b/psa_harmattan/feedingit/deb_dist/feedingit-0.1.0/debian/feedingit/usr/share/doc/feedingit/changelog.Debian.gz differ
index 3d37d37..7911299 100644 (file)
@@ -2,6 +2,7 @@
 
 # 
 # Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield.
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
@@ -36,10 +37,10 @@ from mainthread import mainthread
 import logging
 logger = logging.getLogger(__name__)
 
-VERSION = "52"
+VERSION = "120"
 
 section = "FeedingIt"
-ranges = { "updateInterval":[0.5, 1, 2, 4, 12, 24], "expiry":[24, 48, 72, 144, 288], "fontSize":range(12,24), "orientation":["Automatic", "Landscape", "Portrait"], "artFontSize":[10, 12, 14, 16, 18, 20], "feedsort":["Manual", "Most unread", "Least unread", "Most recent", "Least recent"] }
+ranges = { "updateInterval":[0.5, 1, 2, 4, 8, 12, 24], "expiry":[24, 48, 72, 144, 288], "fontSize":range(12,24), "orientation":["Automatic", "Landscape", "Portrait"], "artFontSize":[10, 12, 14, 16, 18, 20], "feedsort":["Manual", "Most unread", "Least unread", "Most recent", "Least recent"] }
 titles = {"updateInterval":"Auto-update interval", "expiry":"Delete articles", "fontSize":"List font size", "orientation":"Display orientation", "artFontSize":"Article font size","feedsort":"Feed sort order"}
 subtitles = {"updateInterval":"Every %s hours", "expiry":"After %s hours", "fontSize":"%s pixels", "orientation":"%s", "artFontSize":"%s pixels", "feedsort":"%s"}
 
@@ -115,13 +116,35 @@ class Config():
 
 
         heading('Updating')
-        button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        button.set_label("Automatically update feeds")
-        button.set_active(self.config["autoupdate"])
-        button.connect("toggled", self.button_toggled, "autoupdate")
-        vbox.pack_start(button, expand=False)
-        add_setting('updateInterval')
-        add_setting('expiry')
+        label = gtk.Label(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        label.set_label("Use Woodchuck network daemon, or the home-screen widget for automatic updates.")
+        label.set_line_wrap(True)
+        vbox.pack_start(label, expand=False)
+
+        try:
+            import woodchuck
+            woodchuck_installed = True
+        except ImportError:
+            woodchuck_installed = False
+
+        if not woodchuck_installed:
+            def install_woodchuck_clicked(button):
+                from FeedingIt import open_in_browser
+                open_in_browser("http://maemo.org/downloads/product/raw/Maemo5/murmeltier?get_installfile")
+
+            button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
+            button.set_label("Install Woodchuck")
+            button.connect("clicked", install_woodchuck_clicked)
+            button.set_alignment(0,0,1,1)
+            vbox.pack_start(button, expand=False)
+        else:
+            button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+            button.set_label("Woodchuck-Based Automatic Update")
+            button.set_active(self.config["woodchuck"])
+            button.connect("toggled", self.button_toggled, "woodchuck")
+            vbox.pack_start(button, expand=False)
+            add_setting('updateInterval')
+            add_setting('expiry')
 
         heading('Network')
         button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
@@ -175,6 +198,14 @@ class Config():
         else:
             self.config[configName] = False
         #print "autoup",  self.autoupdate
+
+        if configName == 'woodchuck':
+            try:
+                from wc import wc_disable_set
+                wc_disable_set(not self.config['woodchuck'])
+            except Exception:
+                logger.exception("Disabling Woodchuck")
+
         self.saveConfig()
         
     def selection_changed(self, selector, button, setting):
@@ -190,47 +221,39 @@ class Config():
         
     def loadConfig(self):
         self.config = {}
+
+        configParser = RawConfigParser()
         try:
-            configParser = RawConfigParser()
             configParser.read(self.configFilename)
-            self.config["fontSize"] = configParser.getint(section, "fontSize")
-            self.config["artFontSize"] = configParser.getint(section, "artFontSize")
-            self.config["expiry"] = configParser.getint(section, "expiry")
-            self.config["autoupdate"] = configParser.getboolean(section, "autoupdate")
-            self.config["updateInterval"] = configParser.getfloat(section, "updateInterval")
-            self.config["orientation"] = configParser.get(section, "orientation")
-            self.config["imageCache"] = configParser.getboolean(section, "imageCache")
-        except:
-            self.config["fontSize"] = 17
-            self.config["artFontSize"] = 14
-            self.config["expiry"] = 24
-            self.config["autoupdate"] = False
-            self.config["updateInterval"] = 4
-            self.config["orientation"] = "Automatic"
-            self.config["imageCache"] = False
-        try:
-            self.config["proxy"] = configParser.getboolean(section, "proxy")
-        except:
-            self.config["proxy"] = True
-        try:
-            self.config["hidereadfeeds"] = configParser.getboolean(section, "hidereadfeeds")
-            self.config["hidereadarticles"] = configParser.getboolean(section, "hidereadarticles")
-        except:
-            self.config["hidereadfeeds"] = False
-            self.config["hidereadarticles"] = False
-        try:
-            self.config["extBrowser"] = configParser.getboolean(section, "extBrowser")
-        except:
-            self.config["extBrowser"] = False
-        try:
-            self.config["feedsort"] = configParser.get(section, "feedsort")
-        except:
-            self.config["feedsort"] = "Manual"
-        try:
-            self.config["theme"] = configParser.get(section, "theme")
-        except:
-            self.config["theme"] = True
-        
+        except Exception:
+            logger.exception("Reading %s", self.configFilename)
+
+        # The function to use to fetch the parameter, the parameter's
+        # name and the default value.
+        values = ((configParser.getint, "fontSize", 17),
+                  (configParser.getint, "artFontSize", 14),
+                  (configParser.getint, "expiry", 24),
+                  (configParser.getboolean, "autoupdate", False),
+                  (configParser.getboolean, "woodchuck", True),
+                  (configParser.getboolean, "askedAboutWoodchuck", False),
+                  (configParser.getfloat, "updateInterval", 4),
+                  (configParser.get, "orientation", "Automatic"),
+                  (configParser.getboolean, "imageCache", False),
+                  (configParser.getboolean, "proxy", True),
+                  (configParser.getboolean, "hidereadfeeds", False),
+                  (configParser.getboolean, "hidereadarticles", False),
+                  (configParser.getboolean, "extBrowser", False),
+                  (configParser.getboolean, "theme", True),
+                  (configParser.get, "feedsort", "Manual"))
+
+        for fetcher, name, default in values:
+            try:
+                v = fetcher(section, name)
+            except Exception:
+                logger.exception("Reading config variable %s", name)
+                v = default
+            self.config[name] = v
+
     def saveConfig(self):
         configParser = RawConfigParser()
         configParser.add_section(section)
@@ -239,6 +262,8 @@ class Config():
         configParser.set(section, 'expiry', str(self.config["expiry"]))
         configParser.set(section, 'autoupdate', str(self.config["autoupdate"]))
         configParser.set(section, 'updateInterval', str(self.config["updateInterval"]))
+        configParser.set(section, 'woodchuck', str(self.config["woodchuck"]))
+        configParser.set(section, 'askedAboutWoodchuck', str(self.config["askedAboutWoodchuck"]))
         configParser.set(section, 'orientation', str(self.config["orientation"]))
         configParser.set(section, 'imageCache', str(self.config["imageCache"]))
         configParser.set(section, 'proxy', str(self.config["proxy"]))
@@ -282,13 +307,20 @@ class Config():
         return self.config["autoupdate"]
     def setAutoUpdateEnabled(self, value):
         self.config["autoupdate"] = value
+    def getWoodchuckEnabled(self):
+        return self.config["woodchuck"]
+    def getAskedAboutWoodchuck(self):
+        return self.config["askedAboutWoodchuck"]
+    def setAskedAboutWoodchuck(self, value):
+        self.config["askedAboutWoodchuck"] = value
+        self.saveConfig()
     def getUpdateInterval(self):
         return float(self.config["updateInterval"])
     def getReadFont(self):
         return "sans italic %s" % self.config["fontSize"]
     def getUnreadFont(self):
         return "sans %s" % self.config["fontSize"]
-    def getOrientation(self, index):
+    def getOrientation(self):
         return ranges["orientation"].index(self.config["orientation"])
     def getOrientationChoices(self):
         return ranges["orientation"]
index 1e5e98a..e014865 100644 (file)
@@ -4,7 +4,7 @@ import "common" as Common
 
 Rectangle {
     /*x: parent.width; height: parent.height;*/
-    width: parent.width;
+    width: flickable.width;
     height: parent.height
     property alias zoomEnabled: slider.visible;
     property alias value: slider.value;
@@ -15,7 +15,7 @@ Rectangle {
         id: flickable
         //anchors.fill: screen;
         height: parent.height;
-        width: parent.width;
+        width: webView.width;
         contentWidth: webView.width*webView.scale; //Math.max(screen.width,webView.width*webView.scale)
         contentHeight: Math.max(articleViewer.height,webView.height*webView.scale)
         //contentWidth: childrenRect.width; contentHeight: childrenRect.height
@@ -27,12 +27,12 @@ Rectangle {
             id: webView
             //url: flipItem.url;
             html: flipItem.html;
-            preferredWidth: flickable.width
-            preferredHeight: flickable.height
+            preferredWidth: articleView.width
+            //preferredHeight: articleView.height
             //scale: 1.25;
             transformOrigin: Item.TopLeft
             scale: slider.value;
-            settings.defaultFontSize: 24
+            //settings.defaultFontSize: 24
         }
 
 //        onFlickStarted: {
index 7e58501..f4a18a9 100644 (file)
@@ -69,7 +69,7 @@ Item {
         width: parent.width;
         height: updateBarArticles.visible? parent.height-updateBarArticles.height : parent.height;
         visible: false; z:8
-        //onCurrentIndexChanged: photosGridView.positionViewAtIndex(currentIndex, GridView.Contain)
+        onCurrentIndexChanged: articlesView.positionViewAtIndex(currentIndex, ListView.Contain)
         highlightRangeMode: ListView.StrictlyEnforceRange; snapMode: ListView.SnapOneItem
         //cacheBuffer: 5;
         onMovementStarted: articleViewer.vertPanningEnabled=false;
@@ -165,7 +165,7 @@ Item {
                     font.pointSize: settings.articleListingTextSize
                 }
                 MouseArea { anchors.fill: listItem;
-                    onClicked: { articleView.positionViewAtIndex(index, ListView.Contain); articleView.visible = true; }
+                    onClicked: { articleView.currentIndex = index; articleView.visible = true; }
                 }
             }
 
@@ -175,11 +175,13 @@ Item {
         id: viewer
         Item {
             id: flipItem;
-            width: articleViewer.width; height: articleViewer.height;
+            width: articleDisplay.width;
+            height: articleView.height;
 
             //property string url: (articleView.visible && Math.abs(articleView.currentIndex-index)<2) ? path: "";
             property string html: controller.getArticle(articleViewer.feedid, articleid)
             ArticleDisplay {
+                id: articleDisplay
                 zoomEnabled: articleViewer.zoomEnabled;
                 property bool vertPanningEnabled: articleViewer.vertPanningEnabled;
 
index bcefd42..867e1af 100644 (file)
@@ -54,6 +54,9 @@ import logging
 logger = logging.getLogger(__name__)
 
 def getId(string):
+    if issubclass(string.__class__, unicode):
+        string = string.encode('utf8', 'replace')
+
     return md5.new(string).hexdigest()
 
 def download_callback(connection):
@@ -226,14 +229,24 @@ class Feed(BaseObject):
         self.key = key
         self.configdir = configdir
         self.dir = "%s/%s.d" %(self.configdir, self.key)
-        self.tls = threading.local ()
+        self.tls = threading.local()
 
         if not isdir(self.dir):
             mkdir(self.dir)
-        if not isfile("%s/%s.db" %(self.dir, self.key)):
-            self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, date float, updated float, link text, read int);")
+        filename = "%s/%s.db" % (self.dir, self.key)
+        if not isfile(filename):
+            self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, contentHash text, date float, updated float, link text, read int);")
             self.db.execute("CREATE TABLE images (id text, imagePath text);")
             self.db.commit()
+        else:
+            try:
+                self.db.execute("ALTER TABLE feed ADD COLUMN contentHash text")
+                self.db.commit()
+            except sqlite3.OperationalError, e:
+                if 'duplicate column name' in str(e):
+                    pass
+                else:
+                    logger.exception("Add column contentHash to %s", filename)
 
     def addImage(self, configdir, key, baseurl, url, proxy=None, opener=None):
         filename = configdir+key+".d/"+getId(url)
@@ -486,33 +499,43 @@ class Feed(BaseObject):
                    if(not(entry.has_key("id"))):
                        entry["id"] = None
                    content = self.extractContent(entry)
+                   contentHash = getId(content)
                    object_size = len (content)
                    tmpEntry = {"title":entry["title"], "content":content,
                                 "date":date, "link":entry["link"], "author":entry["author"], "id":entry["id"]}
                    id = self.generateUniqueId(tmpEntry)
                    
                    current_version = self.db.execute(
-                       'select date, ROWID from feed where id=?',
+                       'select date, ROWID, contentHash from feed where id=?',
                        (id,)).fetchone()
                    if (current_version is not None
-                       and current_version[0] == date):
+                       # To detect updates, don't compare by date:
+                       # compare by content.
+                       #
+                       # - If an article update is just a date change
+                       #   and the content remains the same, we don't
+                       #   want to register an update.
+                       #
+                       # - If an article's content changes but not the
+                       #   date, we want to recognize an update.
+                       and current_version[2] == contentHash):
                        logger.debug("ALREADY DOWNLOADED %s (%s)"
                                     % (entry["title"], entry["link"]))
-                       ## This article is already present in the feed listing. Update the "updated" time, so it doesn't expire                                                      
-                       self.db.execute("UPDATE feed SET updated=? WHERE id=?;",(currentTime,id))                                                                                    
-                       try:                                                                                                                                                         
-                           logger.debug("Updating already downloaded files for %s" %(id))                                                                                           
-                           filename = configdir+self.key+".d/"+id+".html"                                                                                                           
-                           file = open(filename,"a")                                                                                                                                
-                           utime(filename, None)                                                                                                                                    
-                           file.close()                                                                                                                                             
-                           images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()                                                                  
-                           for image in images:                                                                                                                                     
-                                file = open(image[0],"a")                                                                                                                           
-                                utime(image[0], None)                                                                                                                               
-                                file.close()                                                                                                                                        
-                       except:                                                                                                                                                      
-                           logger.debug("Error in refreshing images for %s" % (id))                                                                                                 
+                       ## This article is already present in the feed listing. Update the "updated" time, so it doesn't expire 
+                       self.db.execute("UPDATE feed SET updated=? WHERE id=?;",(currentTime,id))
+                       try: 
+                           logger.debug("Updating already downloaded files for %s" %(id))
+                           filename = configdir+self.key+".d/"+id+".html"
+                           file = open(filename,"a")
+                           utime(filename, None)
+                           file.close()
+                           images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
+                           for image in images:
+                                file = open(image[0],"a")
+                                utime(image[0], None)
+                                file.close()
+                       except:
+                           logger.debug("Error in refreshing images for %s" % (id))
                        self.db.commit()
                        continue                       
 
@@ -520,7 +543,6 @@ class Feed(BaseObject):
                        # The version was updated.  Mark it as unread.
                        logger.debug("UPDATED: %s (%s)"
                                     % (entry["title"], entry["link"]))
-                       self.setEntryUnread(id)
                        updated_objects += 1
                    else:
                        logger.debug("NEW: %s (%s)"
@@ -531,7 +553,6 @@ class Feed(BaseObject):
                    soup = BeautifulSoup(self.getArticle(tmpEntry)) #tmpEntry["content"])
                    images = soup('img')
                    baseurl = tmpEntry["link"]
-                   #if not id in ids:
                    if imageCache and len(images) > 0:
                        self.serial_execution_lock.release ()
                        have_serial_execution_lock = False
@@ -565,6 +586,7 @@ class Feed(BaseObject):
                    values = {'id': id,
                              'title': tmpEntry["title"],
                              'contentLink': tmpEntry["contentLink"],
+                             'contentHash': contentHash,
                              'date': tmpEntry["date"],
                              'updated': currentTime,
                              'link': tmpEntry["link"],
@@ -738,6 +760,9 @@ class Feed(BaseObject):
     def getContentLink(self, id):
         return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
     
+    def getContentHash(self, id):
+        return self.db.execute("SELECT contentHash FROM feed WHERE id=?;", (id,) ).fetchone()[0]
+    
     def getExternalLink(self, id):
         return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
     
@@ -833,13 +858,18 @@ class Feed(BaseObject):
         return text
    
     def getContent(self, id):
-        contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
+        """
+        Return the content of the article with the specified ID.  If
+        the content is not available, returns None.
+        """
+        contentLink = self.getContentLink(id)
         try:
-            file = open(self.entries[id]["contentLink"])
-            content = file.read()
-            file.close()
-        except:
-            content = "Content unavailable"
+            with open(contentLink, 'r') as file:
+                content = file.read()
+        except Exception:
+            logger.exception("Failed get content for %s: reading %s failed",
+                             id, contentLink)
+            content = None
         return content
     
     def extractDate(self, entry):
@@ -889,33 +919,41 @@ class ArchivedArticles(Feed):
         self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
         self.db.commit()
 
-    def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
+    # Feed.UpdateFeed calls this function.
+    def _updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, priority=0, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
         currentTime = 0
         rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
         for row in rows:
-            currentTime = time.time()
-            id = row[0]
-            link = row[1]
-            f = urllib2.urlopen(link)
-            #entry["content"] = f.read()
-            html = f.read()
-            f.close()
-            soup = BeautifulSoup(html)
-            images = soup('img')
-            baseurl = link
-            for img in images:
-                filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
-                img['src']=filename
-                self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
+            try:
+                currentTime = time.time()
+                id = row[0]
+                link = row[1]
+                f = urllib2.urlopen(link)
+                #entry["content"] = f.read()
+                html = f.read()
+                f.close()
+                soup = BeautifulSoup(html)
+                images = soup('img')
+                baseurl = link
+                for img in images:
+                    filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
+                    img['src']=filename
+                    self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
+                    self.db.commit()
+                contentLink = configdir+self.key+".d/"+id+".html"
+                file = open(contentLink, "w")
+                file.write(soup.prettify())
+                file.close()
+                
+                self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
                 self.db.commit()
-            contentLink = configdir+self.key+".d/"+id+".html"
-            file = open(contentLink, "w")
-            file.write(soup.prettify())
-            file.close()
-            
-            self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
-            self.db.commit()
-        return (currentTime, None, None)
+            except:
+                logger.error("Error updating Archived Article: %s %s"
+                             % (link,traceback.format_exc(),))
+
+        if postFeedUpdateFunc is not None:
+            postFeedUpdateFunc (self.key, currentTime, None, None, None,
+                                *postFeedUpdateFuncArgs)
     
     def purgeReadArticles(self):
         rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
@@ -986,7 +1024,7 @@ class Listing(BaseObject):
         # state.
         try:
             updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
-            wc_init (self, True if updater else False)
+            wc_init(config, self, True if updater else False)
             if wc().available() and updater:
                 # The list of known streams.
                 streams = wc().streams_list ()
@@ -1002,19 +1040,22 @@ class Listing(BaseObject):
                         logger.debug(
                             "Registering previously unknown channel: %s (%s)"
                             % (key, title,))
-                        # Use a default refresh interval of 6 hours.
-                        wc().stream_register (key, title, 6 * 60 * 60)
+                        wc().stream_register(
+                            key, title,
+                            self.config.getUpdateInterval() * 60 * 60)
                     else:
                         # Make sure the human readable name is up to date.
                         if wc()[key].human_readable_name != title:
                             wc()[key].human_readable_name = title
                         stream_ids.remove (key)
+                        wc()[key].freshness \
+                            = self.config.getUpdateInterval() * 60 * 60
                         
     
                 # Unregister any streams that are no longer subscribed to.
                 for id in stream_ids:
                     logger.debug("Unregistering %s" % (id,))
-                    w.stream_unregister (id)
+                    wc().stream_unregister (id)
         except Exception:
             logger.exception("Registering streams with Woodchuck")
 
@@ -1223,7 +1264,7 @@ class Listing(BaseObject):
     
     def getCategoryTitle(self, id):
         return self.lookup('categories', 'title', id)
-
+    
     def getCategoryUnread(self, id):
         count = 0
         for key in self.getListOfFeeds(category=id):
@@ -1294,6 +1335,7 @@ class Listing(BaseObject):
                                      human_readable_name=title,
                                      freshness=6*60*60)
 
+            self.cache_invalidate('feeds')
             return True
         else:
             return False
@@ -1313,7 +1355,7 @@ class Listing(BaseObject):
         if wc().available ():
             try:
                 del wc()[key]
-            except KeyError:
+            except KeyError, woodchuck.Error:
                 logger.debug("Removing unregistered feed %s failed" % (key,))
 
         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
index c28d1d7..d9a2efd 100644 (file)
@@ -32,7 +32,7 @@ except ImportError, exception:
         % traceback.format_exc ())
     woodchuck_imported = False
     class PyWoodchuck (object):
-        def available(self):
+        def available(self, *args, **kwargs):
             return False
     woodchuck = None
 
@@ -40,7 +40,7 @@ except ImportError, exception:
 refresh_interval = 6 * 60 * 60
 
 class mywoodchuck (PyWoodchuck):
-    def __init__(self, listing, human_readable_name, identifier,
+    def __init__(self, config, listing, human_readable_name, identifier,
                  request_feedback):
         try:
             PyWoodchuck.__init__ (self, human_readable_name, identifier,
@@ -52,9 +52,22 @@ class mywoodchuck (PyWoodchuck):
             self.available = self.not_available
             return
 
+        self.config = config
         self.listing = listing
 
-    def not_available(self):
+        try:
+            self.enabled = config.getWoodchuckEnabled()
+        except Exception:
+            logging.exception("Setting enabled")
+
+    def available(self, check_config=True):
+        if not PyWoodchuck.available(self):
+            return False
+        if check_config:
+            return self.config.getWoodchuckEnabled()
+        return True
+
+    def not_available(self, *args, **kwargs):
         return False
 
     # Woodchuck upcalls.
@@ -85,12 +98,12 @@ class mywoodchuck (PyWoodchuck):
                            str(e)))
 
 _w = None
-def wc_init(listing, request_feedback=False):
+def wc_init(config, listing, request_feedback=False):
     """Connect to the woodchuck server and initialize any state."""
     global _w
     assert _w is None
     
-    _w = mywoodchuck (listing, "FeedingIt", "org.marcoz.feedingit",
+    _w = mywoodchuck (config, listing, "FeedingIt", "org.marcoz.feedingit",
                       request_feedback)
 
     if not woodchuck_imported or not _w.available ():
@@ -98,6 +111,27 @@ def wc_init(listing, request_feedback=False):
     else:
         logger.debug("Woodchuck appears to be available.")
 
+def wc_disable_set(disable=True):
+    """Disable Woodchuck."""
+    if disable:
+        logger.info("Disabling Woodchuck")
+    else:
+        logger.info("Enabling Woodchuck")
+
+    global _w
+    if _w is None:
+        logging.info("Woodchuck not loaded.  Not doing anything.")
+        return
+
+    if not _w.available(check_config=False):
+        logging.info("Woodchuck not available.  Not doing anything.")
+        return
+
+    try:
+        _w.enabled = not disable
+    except Exception:
+        logger.exception("Disabling Woodchuck")
+
 def wc():
     """Return the Woodchuck singleton."""
     global _w
index fe23216..68ebf16 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/make -f
 
 # This file was automatically generated by stdeb 0.6.0+git at
-# Tue, 29 Nov 2011 21:32:12 -0800
+# Thu, 05 Jan 2012 20:58:44 -0800
 
 %:
        dh $@ --with python2 --buildsystem=python_distutils
index 3903c7e..698f459 100644 (file)
@@ -1,8 +1,11 @@
 #!/bin/sh
 
 case "$1" in
+update)
+    dbus-send --print-reply --dest='org.marcoz.feedingit' --session /org/marcoz/feedingit/update org.marcoz.feedingit.UpdateAll
+    ;;
 dbus)
-    nice python /usr/share/feedingit/update_feeds.py
+    nice python /usr/share/feedingit/update_feeds.py --daemon
     ;;
 *)
   cd /usr/share/feedingit
index 544c906..95bf65c 100644 (file)
@@ -3,6 +3,6 @@ Encoding=UTF-8
 Version=1.0
 Type=Application
 Name=FeedingIt RSS Reader
-Exec=invoker --single-instance --type=e /usr/bin/feedingit
+Exec=invoker --single-instance --splash=/usr/share/feedingit/splash.jpg --type=e /usr/bin/python /usr/share/feedingit/feedingit.py
 Icon=/usr/share/icons/hicolor/80x80/apps/feedingit.png
 Categories=Network;News;
index 3d37d37..7911299 100644 (file)
@@ -2,6 +2,7 @@
 
 # 
 # Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield.
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
@@ -36,10 +37,10 @@ from mainthread import mainthread
 import logging
 logger = logging.getLogger(__name__)
 
-VERSION = "52"
+VERSION = "120"
 
 section = "FeedingIt"
-ranges = { "updateInterval":[0.5, 1, 2, 4, 12, 24], "expiry":[24, 48, 72, 144, 288], "fontSize":range(12,24), "orientation":["Automatic", "Landscape", "Portrait"], "artFontSize":[10, 12, 14, 16, 18, 20], "feedsort":["Manual", "Most unread", "Least unread", "Most recent", "Least recent"] }
+ranges = { "updateInterval":[0.5, 1, 2, 4, 8, 12, 24], "expiry":[24, 48, 72, 144, 288], "fontSize":range(12,24), "orientation":["Automatic", "Landscape", "Portrait"], "artFontSize":[10, 12, 14, 16, 18, 20], "feedsort":["Manual", "Most unread", "Least unread", "Most recent", "Least recent"] }
 titles = {"updateInterval":"Auto-update interval", "expiry":"Delete articles", "fontSize":"List font size", "orientation":"Display orientation", "artFontSize":"Article font size","feedsort":"Feed sort order"}
 subtitles = {"updateInterval":"Every %s hours", "expiry":"After %s hours", "fontSize":"%s pixels", "orientation":"%s", "artFontSize":"%s pixels", "feedsort":"%s"}
 
@@ -115,13 +116,35 @@ class Config():
 
 
         heading('Updating')
-        button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        button.set_label("Automatically update feeds")
-        button.set_active(self.config["autoupdate"])
-        button.connect("toggled", self.button_toggled, "autoupdate")
-        vbox.pack_start(button, expand=False)
-        add_setting('updateInterval')
-        add_setting('expiry')
+        label = gtk.Label(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        label.set_label("Use Woodchuck network daemon, or the home-screen widget for automatic updates.")
+        label.set_line_wrap(True)
+        vbox.pack_start(label, expand=False)
+
+        try:
+            import woodchuck
+            woodchuck_installed = True
+        except ImportError:
+            woodchuck_installed = False
+
+        if not woodchuck_installed:
+            def install_woodchuck_clicked(button):
+                from FeedingIt import open_in_browser
+                open_in_browser("http://maemo.org/downloads/product/raw/Maemo5/murmeltier?get_installfile")
+
+            button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT, hildon.BUTTON_ARRANGEMENT_VERTICAL)
+            button.set_label("Install Woodchuck")
+            button.connect("clicked", install_woodchuck_clicked)
+            button.set_alignment(0,0,1,1)
+            vbox.pack_start(button, expand=False)
+        else:
+            button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+            button.set_label("Woodchuck-Based Automatic Update")
+            button.set_active(self.config["woodchuck"])
+            button.connect("toggled", self.button_toggled, "woodchuck")
+            vbox.pack_start(button, expand=False)
+            add_setting('updateInterval')
+            add_setting('expiry')
 
         heading('Network')
         button = hildon.CheckButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
@@ -175,6 +198,14 @@ class Config():
         else:
             self.config[configName] = False
         #print "autoup",  self.autoupdate
+
+        if configName == 'woodchuck':
+            try:
+                from wc import wc_disable_set
+                wc_disable_set(not self.config['woodchuck'])
+            except Exception:
+                logger.exception("Disabling Woodchuck")
+
         self.saveConfig()
         
     def selection_changed(self, selector, button, setting):
@@ -190,47 +221,39 @@ class Config():
         
     def loadConfig(self):
         self.config = {}
+
+        configParser = RawConfigParser()
         try:
-            configParser = RawConfigParser()
             configParser.read(self.configFilename)
-            self.config["fontSize"] = configParser.getint(section, "fontSize")
-            self.config["artFontSize"] = configParser.getint(section, "artFontSize")
-            self.config["expiry"] = configParser.getint(section, "expiry")
-            self.config["autoupdate"] = configParser.getboolean(section, "autoupdate")
-            self.config["updateInterval"] = configParser.getfloat(section, "updateInterval")
-            self.config["orientation"] = configParser.get(section, "orientation")
-            self.config["imageCache"] = configParser.getboolean(section, "imageCache")
-        except:
-            self.config["fontSize"] = 17
-            self.config["artFontSize"] = 14
-            self.config["expiry"] = 24
-            self.config["autoupdate"] = False
-            self.config["updateInterval"] = 4
-            self.config["orientation"] = "Automatic"
-            self.config["imageCache"] = False
-        try:
-            self.config["proxy"] = configParser.getboolean(section, "proxy")
-        except:
-            self.config["proxy"] = True
-        try:
-            self.config["hidereadfeeds"] = configParser.getboolean(section, "hidereadfeeds")
-            self.config["hidereadarticles"] = configParser.getboolean(section, "hidereadarticles")
-        except:
-            self.config["hidereadfeeds"] = False
-            self.config["hidereadarticles"] = False
-        try:
-            self.config["extBrowser"] = configParser.getboolean(section, "extBrowser")
-        except:
-            self.config["extBrowser"] = False
-        try:
-            self.config["feedsort"] = configParser.get(section, "feedsort")
-        except:
-            self.config["feedsort"] = "Manual"
-        try:
-            self.config["theme"] = configParser.get(section, "theme")
-        except:
-            self.config["theme"] = True
-        
+        except Exception:
+            logger.exception("Reading %s", self.configFilename)
+
+        # The function to use to fetch the parameter, the parameter's
+        # name and the default value.
+        values = ((configParser.getint, "fontSize", 17),
+                  (configParser.getint, "artFontSize", 14),
+                  (configParser.getint, "expiry", 24),
+                  (configParser.getboolean, "autoupdate", False),
+                  (configParser.getboolean, "woodchuck", True),
+                  (configParser.getboolean, "askedAboutWoodchuck", False),
+                  (configParser.getfloat, "updateInterval", 4),
+                  (configParser.get, "orientation", "Automatic"),
+                  (configParser.getboolean, "imageCache", False),
+                  (configParser.getboolean, "proxy", True),
+                  (configParser.getboolean, "hidereadfeeds", False),
+                  (configParser.getboolean, "hidereadarticles", False),
+                  (configParser.getboolean, "extBrowser", False),
+                  (configParser.getboolean, "theme", True),
+                  (configParser.get, "feedsort", "Manual"))
+
+        for fetcher, name, default in values:
+            try:
+                v = fetcher(section, name)
+            except Exception:
+                logger.exception("Reading config variable %s", name)
+                v = default
+            self.config[name] = v
+
     def saveConfig(self):
         configParser = RawConfigParser()
         configParser.add_section(section)
@@ -239,6 +262,8 @@ class Config():
         configParser.set(section, 'expiry', str(self.config["expiry"]))
         configParser.set(section, 'autoupdate', str(self.config["autoupdate"]))
         configParser.set(section, 'updateInterval', str(self.config["updateInterval"]))
+        configParser.set(section, 'woodchuck', str(self.config["woodchuck"]))
+        configParser.set(section, 'askedAboutWoodchuck', str(self.config["askedAboutWoodchuck"]))
         configParser.set(section, 'orientation', str(self.config["orientation"]))
         configParser.set(section, 'imageCache', str(self.config["imageCache"]))
         configParser.set(section, 'proxy', str(self.config["proxy"]))
@@ -282,13 +307,20 @@ class Config():
         return self.config["autoupdate"]
     def setAutoUpdateEnabled(self, value):
         self.config["autoupdate"] = value
+    def getWoodchuckEnabled(self):
+        return self.config["woodchuck"]
+    def getAskedAboutWoodchuck(self):
+        return self.config["askedAboutWoodchuck"]
+    def setAskedAboutWoodchuck(self, value):
+        self.config["askedAboutWoodchuck"] = value
+        self.saveConfig()
     def getUpdateInterval(self):
         return float(self.config["updateInterval"])
     def getReadFont(self):
         return "sans italic %s" % self.config["fontSize"]
     def getUnreadFont(self):
         return "sans %s" % self.config["fontSize"]
-    def getOrientation(self, index):
+    def getOrientation(self):
         return ranges["orientation"].index(self.config["orientation"])
     def getOrientationChoices(self):
         return ranges["orientation"]
index bcefd42..867e1af 100644 (file)
@@ -54,6 +54,9 @@ import logging
 logger = logging.getLogger(__name__)
 
 def getId(string):
+    if issubclass(string.__class__, unicode):
+        string = string.encode('utf8', 'replace')
+
     return md5.new(string).hexdigest()
 
 def download_callback(connection):
@@ -226,14 +229,24 @@ class Feed(BaseObject):
         self.key = key
         self.configdir = configdir
         self.dir = "%s/%s.d" %(self.configdir, self.key)
-        self.tls = threading.local ()
+        self.tls = threading.local()
 
         if not isdir(self.dir):
             mkdir(self.dir)
-        if not isfile("%s/%s.db" %(self.dir, self.key)):
-            self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, date float, updated float, link text, read int);")
+        filename = "%s/%s.db" % (self.dir, self.key)
+        if not isfile(filename):
+            self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, contentHash text, date float, updated float, link text, read int);")
             self.db.execute("CREATE TABLE images (id text, imagePath text);")
             self.db.commit()
+        else:
+            try:
+                self.db.execute("ALTER TABLE feed ADD COLUMN contentHash text")
+                self.db.commit()
+            except sqlite3.OperationalError, e:
+                if 'duplicate column name' in str(e):
+                    pass
+                else:
+                    logger.exception("Add column contentHash to %s", filename)
 
     def addImage(self, configdir, key, baseurl, url, proxy=None, opener=None):
         filename = configdir+key+".d/"+getId(url)
@@ -486,33 +499,43 @@ class Feed(BaseObject):
                    if(not(entry.has_key("id"))):
                        entry["id"] = None
                    content = self.extractContent(entry)
+                   contentHash = getId(content)
                    object_size = len (content)
                    tmpEntry = {"title":entry["title"], "content":content,
                                 "date":date, "link":entry["link"], "author":entry["author"], "id":entry["id"]}
                    id = self.generateUniqueId(tmpEntry)
                    
                    current_version = self.db.execute(
-                       'select date, ROWID from feed where id=?',
+                       'select date, ROWID, contentHash from feed where id=?',
                        (id,)).fetchone()
                    if (current_version is not None
-                       and current_version[0] == date):
+                       # To detect updates, don't compare by date:
+                       # compare by content.
+                       #
+                       # - If an article update is just a date change
+                       #   and the content remains the same, we don't
+                       #   want to register an update.
+                       #
+                       # - If an article's content changes but not the
+                       #   date, we want to recognize an update.
+                       and current_version[2] == contentHash):
                        logger.debug("ALREADY DOWNLOADED %s (%s)"
                                     % (entry["title"], entry["link"]))
-                       ## This article is already present in the feed listing. Update the "updated" time, so it doesn't expire                                                      
-                       self.db.execute("UPDATE feed SET updated=? WHERE id=?;",(currentTime,id))                                                                                    
-                       try:                                                                                                                                                         
-                           logger.debug("Updating already downloaded files for %s" %(id))                                                                                           
-                           filename = configdir+self.key+".d/"+id+".html"                                                                                                           
-                           file = open(filename,"a")                                                                                                                                
-                           utime(filename, None)                                                                                                                                    
-                           file.close()                                                                                                                                             
-                           images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()                                                                  
-                           for image in images:                                                                                                                                     
-                                file = open(image[0],"a")                                                                                                                           
-                                utime(image[0], None)                                                                                                                               
-                                file.close()                                                                                                                                        
-                       except:                                                                                                                                                      
-                           logger.debug("Error in refreshing images for %s" % (id))                                                                                                 
+                       ## This article is already present in the feed listing. Update the "updated" time, so it doesn't expire 
+                       self.db.execute("UPDATE feed SET updated=? WHERE id=?;",(currentTime,id))
+                       try: 
+                           logger.debug("Updating already downloaded files for %s" %(id))
+                           filename = configdir+self.key+".d/"+id+".html"
+                           file = open(filename,"a")
+                           utime(filename, None)
+                           file.close()
+                           images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
+                           for image in images:
+                                file = open(image[0],"a")
+                                utime(image[0], None)
+                                file.close()
+                       except:
+                           logger.debug("Error in refreshing images for %s" % (id))
                        self.db.commit()
                        continue                       
 
@@ -520,7 +543,6 @@ class Feed(BaseObject):
                        # The version was updated.  Mark it as unread.
                        logger.debug("UPDATED: %s (%s)"
                                     % (entry["title"], entry["link"]))
-                       self.setEntryUnread(id)
                        updated_objects += 1
                    else:
                        logger.debug("NEW: %s (%s)"
@@ -531,7 +553,6 @@ class Feed(BaseObject):
                    soup = BeautifulSoup(self.getArticle(tmpEntry)) #tmpEntry["content"])
                    images = soup('img')
                    baseurl = tmpEntry["link"]
-                   #if not id in ids:
                    if imageCache and len(images) > 0:
                        self.serial_execution_lock.release ()
                        have_serial_execution_lock = False
@@ -565,6 +586,7 @@ class Feed(BaseObject):
                    values = {'id': id,
                              'title': tmpEntry["title"],
                              'contentLink': tmpEntry["contentLink"],
+                             'contentHash': contentHash,
                              'date': tmpEntry["date"],
                              'updated': currentTime,
                              'link': tmpEntry["link"],
@@ -738,6 +760,9 @@ class Feed(BaseObject):
     def getContentLink(self, id):
         return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
     
+    def getContentHash(self, id):
+        return self.db.execute("SELECT contentHash FROM feed WHERE id=?;", (id,) ).fetchone()[0]
+    
     def getExternalLink(self, id):
         return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
     
@@ -833,13 +858,18 @@ class Feed(BaseObject):
         return text
    
     def getContent(self, id):
-        contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
+        """
+        Return the content of the article with the specified ID.  If
+        the content is not available, returns None.
+        """
+        contentLink = self.getContentLink(id)
         try:
-            file = open(self.entries[id]["contentLink"])
-            content = file.read()
-            file.close()
-        except:
-            content = "Content unavailable"
+            with open(contentLink, 'r') as file:
+                content = file.read()
+        except Exception:
+            logger.exception("Failed get content for %s: reading %s failed",
+                             id, contentLink)
+            content = None
         return content
     
     def extractDate(self, entry):
@@ -889,33 +919,41 @@ class ArchivedArticles(Feed):
         self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
         self.db.commit()
 
-    def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
+    # Feed.UpdateFeed calls this function.
+    def _updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False, priority=0, postFeedUpdateFunc=None, *postFeedUpdateFuncArgs):
         currentTime = 0
         rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
         for row in rows:
-            currentTime = time.time()
-            id = row[0]
-            link = row[1]
-            f = urllib2.urlopen(link)
-            #entry["content"] = f.read()
-            html = f.read()
-            f.close()
-            soup = BeautifulSoup(html)
-            images = soup('img')
-            baseurl = link
-            for img in images:
-                filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
-                img['src']=filename
-                self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
+            try:
+                currentTime = time.time()
+                id = row[0]
+                link = row[1]
+                f = urllib2.urlopen(link)
+                #entry["content"] = f.read()
+                html = f.read()
+                f.close()
+                soup = BeautifulSoup(html)
+                images = soup('img')
+                baseurl = link
+                for img in images:
+                    filename = self.addImage(configdir, self.key, baseurl, img['src'], proxy=proxy)
+                    img['src']=filename
+                    self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
+                    self.db.commit()
+                contentLink = configdir+self.key+".d/"+id+".html"
+                file = open(contentLink, "w")
+                file.write(soup.prettify())
+                file.close()
+                
+                self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
                 self.db.commit()
-            contentLink = configdir+self.key+".d/"+id+".html"
-            file = open(contentLink, "w")
-            file.write(soup.prettify())
-            file.close()
-            
-            self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
-            self.db.commit()
-        return (currentTime, None, None)
+            except:
+                logger.error("Error updating Archived Article: %s %s"
+                             % (link,traceback.format_exc(),))
+
+        if postFeedUpdateFunc is not None:
+            postFeedUpdateFunc (self.key, currentTime, None, None, None,
+                                *postFeedUpdateFuncArgs)
     
     def purgeReadArticles(self):
         rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
@@ -986,7 +1024,7 @@ class Listing(BaseObject):
         # state.
         try:
             updater = os.path.basename(sys.argv[0]) == 'update_feeds.py'
-            wc_init (self, True if updater else False)
+            wc_init(config, self, True if updater else False)
             if wc().available() and updater:
                 # The list of known streams.
                 streams = wc().streams_list ()
@@ -1002,19 +1040,22 @@ class Listing(BaseObject):
                         logger.debug(
                             "Registering previously unknown channel: %s (%s)"
                             % (key, title,))
-                        # Use a default refresh interval of 6 hours.
-                        wc().stream_register (key, title, 6 * 60 * 60)
+                        wc().stream_register(
+                            key, title,
+                            self.config.getUpdateInterval() * 60 * 60)
                     else:
                         # Make sure the human readable name is up to date.
                         if wc()[key].human_readable_name != title:
                             wc()[key].human_readable_name = title
                         stream_ids.remove (key)
+                        wc()[key].freshness \
+                            = self.config.getUpdateInterval() * 60 * 60
                         
     
                 # Unregister any streams that are no longer subscribed to.
                 for id in stream_ids:
                     logger.debug("Unregistering %s" % (id,))
-                    w.stream_unregister (id)
+                    wc().stream_unregister (id)
         except Exception:
             logger.exception("Registering streams with Woodchuck")
 
@@ -1223,7 +1264,7 @@ class Listing(BaseObject):
     
     def getCategoryTitle(self, id):
         return self.lookup('categories', 'title', id)
-
+    
     def getCategoryUnread(self, id):
         count = 0
         for key in self.getListOfFeeds(category=id):
@@ -1294,6 +1335,7 @@ class Listing(BaseObject):
                                      human_readable_name=title,
                                      freshness=6*60*60)
 
+            self.cache_invalidate('feeds')
             return True
         else:
             return False
@@ -1313,7 +1355,7 @@ class Listing(BaseObject):
         if wc().available ():
             try:
                 del wc()[key]
-            except KeyError:
+            except KeyError, woodchuck.Error:
                 logger.debug("Removing unregistered feed %s failed" % (key,))
 
         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
index c28d1d7..d9a2efd 100644 (file)
@@ -32,7 +32,7 @@ except ImportError, exception:
         % traceback.format_exc ())
     woodchuck_imported = False
     class PyWoodchuck (object):
-        def available(self):
+        def available(self, *args, **kwargs):
             return False
     woodchuck = None
 
@@ -40,7 +40,7 @@ except ImportError, exception:
 refresh_interval = 6 * 60 * 60
 
 class mywoodchuck (PyWoodchuck):
-    def __init__(self, listing, human_readable_name, identifier,
+    def __init__(self, config, listing, human_readable_name, identifier,
                  request_feedback):
         try:
             PyWoodchuck.__init__ (self, human_readable_name, identifier,
@@ -52,9 +52,22 @@ class mywoodchuck (PyWoodchuck):
             self.available = self.not_available
             return
 
+        self.config = config
         self.listing = listing
 
-    def not_available(self):
+        try:
+            self.enabled = config.getWoodchuckEnabled()
+        except Exception:
+            logging.exception("Setting enabled")
+
+    def available(self, check_config=True):
+        if not PyWoodchuck.available(self):
+            return False
+        if check_config:
+            return self.config.getWoodchuckEnabled()
+        return True
+
+    def not_available(self, *args, **kwargs):
         return False
 
     # Woodchuck upcalls.
@@ -85,12 +98,12 @@ class mywoodchuck (PyWoodchuck):
                            str(e)))
 
 _w = None
-def wc_init(listing, request_feedback=False):
+def wc_init(config, listing, request_feedback=False):
     """Connect to the woodchuck server and initialize any state."""
     global _w
     assert _w is None
     
-    _w = mywoodchuck (listing, "FeedingIt", "org.marcoz.feedingit",
+    _w = mywoodchuck (config, listing, "FeedingIt", "org.marcoz.feedingit",
                       request_feedback)
 
     if not woodchuck_imported or not _w.available ():
@@ -98,6 +111,27 @@ def wc_init(listing, request_feedback=False):
     else:
         logger.debug("Woodchuck appears to be available.")
 
+def wc_disable_set(disable=True):
+    """Disable Woodchuck."""
+    if disable:
+        logger.info("Disabling Woodchuck")
+    else:
+        logger.info("Enabling Woodchuck")
+
+    global _w
+    if _w is None:
+        logging.info("Woodchuck not loaded.  Not doing anything.")
+        return
+
+    if not _w.available(check_config=False):
+        logging.info("Woodchuck not available.  Not doing anything.")
+        return
+
+    try:
+        _w.enabled = not disable
+    except Exception:
+        logger.exception("Disabling Woodchuck")
+
 def wc():
     """Return the Woodchuck singleton."""
     global _w
index 1e5e98a..e014865 100644 (file)
@@ -4,7 +4,7 @@ import "common" as Common
 
 Rectangle {
     /*x: parent.width; height: parent.height;*/
-    width: parent.width;
+    width: flickable.width;
     height: parent.height
     property alias zoomEnabled: slider.visible;
     property alias value: slider.value;
@@ -15,7 +15,7 @@ Rectangle {
         id: flickable
         //anchors.fill: screen;
         height: parent.height;
-        width: parent.width;
+        width: webView.width;
         contentWidth: webView.width*webView.scale; //Math.max(screen.width,webView.width*webView.scale)
         contentHeight: Math.max(articleViewer.height,webView.height*webView.scale)
         //contentWidth: childrenRect.width; contentHeight: childrenRect.height
@@ -27,12 +27,12 @@ Rectangle {
             id: webView
             //url: flipItem.url;
             html: flipItem.html;
-            preferredWidth: flickable.width
-            preferredHeight: flickable.height
+            preferredWidth: articleView.width
+            //preferredHeight: articleView.height
             //scale: 1.25;
             transformOrigin: Item.TopLeft
             scale: slider.value;
-            settings.defaultFontSize: 24
+            //settings.defaultFontSize: 24
         }
 
 //        onFlickStarted: {
index 7e58501..f4a18a9 100644 (file)
@@ -69,7 +69,7 @@ Item {
         width: parent.width;
         height: updateBarArticles.visible? parent.height-updateBarArticles.height : parent.height;
         visible: false; z:8
-        //onCurrentIndexChanged: photosGridView.positionViewAtIndex(currentIndex, GridView.Contain)
+        onCurrentIndexChanged: articlesView.positionViewAtIndex(currentIndex, ListView.Contain)
         highlightRangeMode: ListView.StrictlyEnforceRange; snapMode: ListView.SnapOneItem
         //cacheBuffer: 5;
         onMovementStarted: articleViewer.vertPanningEnabled=false;
@@ -165,7 +165,7 @@ Item {
                     font.pointSize: settings.articleListingTextSize
                 }
                 MouseArea { anchors.fill: listItem;
-                    onClicked: { articleView.positionViewAtIndex(index, ListView.Contain); articleView.visible = true; }
+                    onClicked: { articleView.currentIndex = index; articleView.visible = true; }
                 }
             }
 
@@ -175,11 +175,13 @@ Item {
         id: viewer
         Item {
             id: flipItem;
-            width: articleViewer.width; height: articleViewer.height;
+            width: articleDisplay.width;
+            height: articleView.height;
 
             //property string url: (articleView.visible && Math.abs(articleView.currentIndex-index)<2) ? path: "";
             property string html: controller.getArticle(articleViewer.feedid, articleid)
             ArticleDisplay {
+                id: articleDisplay
                 zoomEnabled: articleViewer.zoomEnabled;
                 property bool vertPanningEnabled: articleViewer.vertPanningEnabled;
 
index 5161a7c..f6f0fd2 100644 (file)
@@ -19,4 +19,5 @@ setup(name="feedingit",
                   ('share/feedingit/qml/common/images', glob.glob('qml/common/images/*')),
                   ('share/feedingit/qml/i18n', glob.glob('qml/i18n/*')),
                   ('share/dbus-1/services', ['feedingit_status.service']),
+                  ('share/feedingit', ['splash.jpg']),
                  ('share/feedingit', glob.glob('pysrc/*.py')) ],)
index 15a8c1b..42ee4a6 100644 (file)
Binary files a/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1.debian.tar.gz and b/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1.debian.tar.gz differ
index fa951e6..6dfb132 100644 (file)
@@ -7,11 +7,11 @@ Maintainer: Slocan <slocan@marcoz.org>
 Standards-Version: 3.9.1
 Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 7.4.3)
 Checksums-Sha1: 
- f72424b24806c14a04a45be31d817a954c29b61c 185518 feedingit_0.1.0.orig.tar.gz
- b51d6818e8859ac6d5bcf5bca329dd4f776597cd 888 feedingit_0.1.0-1.debian.tar.gz
+ e22e0955ddccc6762aa03b7d1f5baebacebb0ebb 199505 feedingit_0.1.0.orig.tar.gz
+ 883b59ceea4aa57b5c83cbe38b679feeb87a2c79 886 feedingit_0.1.0-1.debian.tar.gz
 Checksums-Sha256: 
- 70d0ed38abbf308935ec2f9dafc403ba3186bd9205aba0214c41a4c93bb4cead 185518 feedingit_0.1.0.orig.tar.gz
- 95f8ac99f327887646ab60d5a9ff0b2331450b7041795db56a8a87563f5ad1e6 888 feedingit_0.1.0-1.debian.tar.gz
+ edc0b7c4b001d19e3ecf9615642bffdddcdcabe375d6cabdeb4dfbe395303980 199505 feedingit_0.1.0.orig.tar.gz
+ 27904d190182f50bca80bcdbd18773325ab7b79cccede4659f3ec042e3582753 886 feedingit_0.1.0-1.debian.tar.gz
 Files: 
- 5c60aa33afabe2253b807b8990358c05 185518 feedingit_0.1.0.orig.tar.gz
- d79f2e7d466d8781614e7c1e468f1c57 888 feedingit_0.1.0-1.debian.tar.gz
+ 25b45241279c8c856489cd0d6a13ffab 199505 feedingit_0.1.0.orig.tar.gz
+ f93ba4e3ec741aabbd3a0c320f9862fa 886 feedingit_0.1.0-1.debian.tar.gz
index 2d9672d..796b2e9 100644 (file)
Binary files a/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1_all.deb and b/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0-1_all.deb differ
index 865ea22..1777770 100644 (file)
@@ -1,5 +1,5 @@
 Format: 1.8
-Date: Tue, 29 Nov 2011 21:32:12 -0800
+Date: Thu, 05 Jan 2012 20:58:44 -0800
 Source: feedingit
 Binary: feedingit
 Architecture: all
@@ -15,8 +15,8 @@ Changes:
  .
    * source package automatically created by stdeb 0.6.0+git
 Checksums-Sha1: 
- f3e7a0dd9894fb979b10aed034057eba7f56e4f0 172448 feedingit_0.1.0-1_all.deb
+ 147b11cafcf8b0c652325c30dcb84540795f9e59 187912 feedingit_0.1.0-1_all.deb
 Checksums-Sha256: 
- 582d42f71bb73dd510bfa63f925a080387ffa6eeb0e6f15c9c7ce90b87e63f5b 172448 feedingit_0.1.0-1_all.deb
+ fe4e11203b4e3cc25ec905172cc0c5acd92a236b2492a547ff088bbece0f243a 187912 feedingit_0.1.0-1_all.deb
 Files: 
- c9d015476c9e657e3aa457b1f5b0de3b 172448 user/network optional feedingit_0.1.0-1_all.deb
+ a91028946c0bc0b43bb2645a9a32f972 187912 user/network optional feedingit_0.1.0-1_all.deb
index f1875b9..4477927 100644 (file)
Binary files a/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0.orig.tar.gz and b/psa_harmattan/feedingit/deb_dist/feedingit_0.1.0.orig.tar.gz differ
index 1e5e98a..e014865 100644 (file)
@@ -4,7 +4,7 @@ import "common" as Common
 
 Rectangle {
     /*x: parent.width; height: parent.height;*/
-    width: parent.width;
+    width: flickable.width;
     height: parent.height
     property alias zoomEnabled: slider.visible;
     property alias value: slider.value;
@@ -15,7 +15,7 @@ Rectangle {
         id: flickable
         //anchors.fill: screen;
         height: parent.height;
-        width: parent.width;
+        width: webView.width;
         contentWidth: webView.width*webView.scale; //Math.max(screen.width,webView.width*webView.scale)
         contentHeight: Math.max(articleViewer.height,webView.height*webView.scale)
         //contentWidth: childrenRect.width; contentHeight: childrenRect.height
@@ -27,12 +27,12 @@ Rectangle {
             id: webView
             //url: flipItem.url;
             html: flipItem.html;
-            preferredWidth: flickable.width
-            preferredHeight: flickable.height
+            preferredWidth: articleView.width
+            //preferredHeight: articleView.height
             //scale: 1.25;
             transformOrigin: Item.TopLeft
             scale: slider.value;
-            settings.defaultFontSize: 24
+            //settings.defaultFontSize: 24
         }
 
 //        onFlickStarted: {
index c8e59e0..f4a18a9 100644 (file)
@@ -175,11 +175,13 @@ Item {
         id: viewer
         Item {
             id: flipItem;
-            width: articleViewer.width; height: articleViewer.height;
+            width: articleDisplay.width;
+            height: articleView.height;
 
             //property string url: (articleView.visible && Math.abs(articleView.currentIndex-index)<2) ? path: "";
             property string html: controller.getArticle(articleViewer.feedid, articleid)
             ArticleDisplay {
+                id: articleDisplay
                 zoomEnabled: articleViewer.zoomEnabled;
                 property bool vertPanningEnabled: articleViewer.vertPanningEnabled;