Fixed typo for widget
[feedingit] / src / rss_sqlite.py
1 #!/usr/bin/env python2.5
2
3
4 # Copyright (c) 2007-2008 INdT.
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 #  This program is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #  GNU Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public License
16 #  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #
18
19 # ============================================================================
20 # Name        : FeedingIt.py
21 # Author      : Yves Marcoz
22 # Version     : 0.5.4
23 # Description : Simple RSS Reader
24 # ============================================================================
25
26 import sqlite3
27 from os.path import isfile, isdir
28 from shutil import rmtree
29 from os import mkdir, remove, utime
30 import md5
31 import feedparser
32 import time
33 import urllib2
34 from BeautifulSoup import BeautifulSoup
35 from urlparse import urljoin
36 from calendar import timegm
37
38 def getId(string):
39     return md5.new(string).hexdigest()
40
41 class Feed:
42     def __init__(self, configdir, key):
43         self.key = key
44         self.configdir = configdir
45         self.dir = "%s/%s.d" %(self.configdir, self.key)
46         if not isdir(self.dir):
47             mkdir(self.dir)
48         if not isfile("%s/%s.db" %(self.dir, self.key)):
49             self.db = sqlite3.connect("%s/%s.db" %(self.dir, self.key) )
50             self.db.execute("CREATE TABLE feed (id text, title text, contentLink text, date float, updated float, link text, read int);")
51             self.db.execute("CREATE TABLE images (id text, imagePath text);")
52             self.db.commit()
53         else:
54             self.db = sqlite3.connect("%s/%s.db" %(self.dir, self.key) )
55
56     def addImage(self, configdir, key, baseurl, url):
57         filename = configdir+key+".d/"+getId(url)
58         if not isfile(filename):
59             try:
60                 f = urllib2.urlopen(urljoin(baseurl,url))
61                 outf = open(filename, "w")
62                 outf.write(f.read())
63                 f.close()
64                 outf.close()
65             except:
66                 print "Could not download " + url
67         else:
68             #open(filename,"a").close()  # "Touch" the file
69             file = open(filename,"a")
70             utime(filename, None)
71             file.close()
72         return filename
73
74     def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
75         # Expiry time is in hours
76         if proxy == None:
77             tmp=feedparser.parse(url, etag = etag, modified = modified)
78         else:
79             tmp=feedparser.parse(url, etag = etag, modified = modified, handlers = [proxy])
80         expiry = float(expiryTime) * 3600.
81
82         currentTime = 0
83         # Check if the parse was succesful (number of entries > 0, else do nothing)
84         if len(tmp["entries"])>0:
85            currentTime = time.time()
86            # The etag and modified value should only be updated if the content was not null
87            try:
88                etag = tmp["etag"]
89            except KeyError:
90                etag = None
91            try:
92                modified = tmp["modified"]
93            except KeyError:
94                modified = None
95            try:
96                f = urllib2.urlopen(urljoin(tmp["feed"]["link"],"/favicon.ico"))
97                data = f.read()
98                f.close()
99                outf = open(self.dir+"/favicon.ico", "w")
100                outf.write(data)
101                outf.close()
102                del data
103            except:
104                #import traceback
105                #traceback.print_exc()
106                 pass
107
108
109            #reversedEntries = self.getEntries()
110            #reversedEntries.reverse()
111
112            ids = self.getIds()
113
114            tmp["entries"].reverse()
115            for entry in tmp["entries"]:
116                date = self.extractDate(entry)
117                try:
118                    entry["title"]
119                except:
120                    entry["title"] = "No Title"
121                try:
122                    entry["link"]
123                except:
124                    entry["link"] = ""
125                try:
126                    entry["author"]
127                except:
128                    entry["author"] = None
129                tmpEntry = {"title":entry["title"], "content":self.extractContent(entry),
130                             "date":date, "link":entry["link"], "author":entry["author"]}
131                id = self.generateUniqueId(tmpEntry)
132                
133                #articleTime = time.mktime(self.entries[id]["dateTuple"])
134                if not id in ids:
135                    soup = BeautifulSoup(self.getArticle(tmpEntry)) #tmpEntry["content"])
136                    images = soup('img')
137                    baseurl = tmpEntry["link"]
138                    if imageCache:
139                       for img in images:
140                           try:
141                             filename = self.addImage(configdir, self.key, baseurl, img['src'])
142                             img['src']=filename
143                             self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
144                           except:
145                               import traceback
146                               traceback.print_exc()
147                               print "Error downloading image %s" % img
148                    tmpEntry["contentLink"] = configdir+self.key+".d/"+id+".html"
149                    file = open(tmpEntry["contentLink"], "w")
150                    file.write(soup.prettify())
151                    file.close()
152                    values = (id, tmpEntry["title"], tmpEntry["contentLink"], tmpEntry["date"], currentTime, tmpEntry["link"], 0)
153                    self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
154                else:
155                    try:
156                        self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
157                        self.db.commit()
158                        filename = configdir+self.key+".d/"+id+".html"
159                        file = open(filename,"a")
160                        utime(filename, None)
161                        file.close()
162                        images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
163                        for image in images:
164                             file = open(image[0],"a")
165                             utime(image[0], None)
166                             file.close()
167                    except:
168                        pass
169            self.db.commit()
170             
171            
172         rows = self.db.execute("SELECT id FROM feed WHERE (read=0 AND updated<?) OR (read=1 AND updated<?);", (currentTime-2*expiry, currentTime-expiry))
173         for row in rows:
174            self.removeEntry(row[0])
175         
176         from glob import glob
177         from os import stat
178         for file in glob(configdir+self.key+".d/*"):
179             #
180             stats = stat(file)
181             #
182             # put the two dates into matching format
183             #
184             lastmodDate = stats[8]
185             #
186             expDate = time.time()-expiry*3
187             # check if image-last-modified-date is outdated
188             #
189             if expDate > lastmodDate:
190                 #
191                 try:
192                     #
193                     #print 'Removing', file
194                     #
195                     remove(file) # commented out for testing
196                     #
197                 except OSError:
198                     #
199                     print 'Could not remove', file
200         updateTime = 0
201         rows = self.db.execute("SELECT MAX(date) FROM feed;")
202         for row in rows:
203             updateTime=row[0]
204         return (updateTime, etag, modified)
205     
206     def setEntryRead(self, id):
207         self.db.execute("UPDATE feed SET read=1 WHERE id=?;", (id,) )
208         self.db.commit()
209         
210     def setEntryUnread(self, id):
211         self.db.execute("UPDATE feed SET read=0 WHERE id=?;", (id,) )
212         self.db.commit()     
213         
214     def markAllAsRead(self):
215         self.db.execute("UPDATE feed SET read=1 WHERE read=0;")
216         self.db.commit()
217
218     def isEntryRead(self, id):
219         read_status = self.db.execute("SELECT read FROM feed WHERE id=?;", (id,) ).fetchone()[0]
220         return read_status==1  # Returns True if read==1, and False if read==0
221     
222     def getTitle(self, id):
223         return self.db.execute("SELECT title FROM feed WHERE id=?;", (id,) ).fetchone()[0]
224     
225     def getContentLink(self, id):
226         return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
227     
228     def getExternalLink(self, id):
229         return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
230     
231     def getDate(self, id):
232         dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
233         return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(dateStamp))
234
235     def getDateTuple(self, id):
236         dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
237         return time.localtime(dateStamp)
238     
239     def getDateStamp(self, id):
240         return self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
241     
242     def generateUniqueId(self, entry):
243         return getId(str(entry["date"]) + str(entry["title"]))
244     
245     def getIds(self, onlyUnread=False):
246         if onlyUnread:
247             rows = self.db.execute("SELECT id FROM feed where read=0 ORDER BY date DESC;").fetchall()
248         else:
249             rows = self.db.execute("SELECT id FROM feed ORDER BY date DESC;").fetchall()
250         ids = []
251         for row in rows:
252             ids.append(row[0])
253         #ids.reverse()
254         return ids
255     
256     def getNextId(self, id):
257         ids = self.getIds()
258         index = ids.index(id)
259         return ids[(index+1)%len(ids)]
260         
261     def getPreviousId(self, id):
262         ids = self.getIds()
263         index = ids.index(id)
264         return ids[(index-1)%len(ids)]
265     
266     def getNumberOfUnreadItems(self):
267         return self.db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
268     
269     def getNumberOfEntries(self):
270         return self.db.execute("SELECT count(*) FROM feed;").fetchone()[0]
271
272     def getArticle(self, entry):
273         #self.setEntryRead(id)
274         #entry = self.entries[id]
275         title = entry['title']
276         #content = entry.get('content', entry.get('summary_detail', {}))
277         content = entry["content"]
278
279         link = entry['link']
280         author = entry['author']
281         date = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(entry["date"]) )
282
283         #text = '''<div style="color: black; background-color: white;">'''
284         text = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
285         text += "<html><head><title>" + title + "</title>"
286         text += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
287         #text += '<style> body {-webkit-user-select: none;} </style>'
288         text += '</head><body><div><a href=\"' + link + '\">' + title + "</a>"
289         if author != None:
290             text += "<BR /><small><i>Author: " + author + "</i></small>"
291         text += "<BR /><small><i>Date: " + date + "</i></small></div>"
292         text += "<BR /><BR />"
293         text += content
294         text += "</body></html>"
295         return text
296    
297     def getContent(self, id):
298         contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
299         try:
300             file = open(self.entries[id]["contentLink"])
301             content = file.read()
302             file.close()
303         except:
304             content = "Content unavailable"
305         return content
306     
307     def extractDate(self, entry):
308         if entry.has_key("updated_parsed"):
309             return timegm(entry["updated_parsed"])
310         elif entry.has_key("published_parsed"):
311             return timegm(entry["published_parsed"])
312         else:
313             return 0
314         
315     def extractContent(self, entry):
316         content = ""
317         if entry.has_key('summary'):
318             content = entry.get('summary', '')
319         if entry.has_key('content'):
320             if len(entry.content[0].value) > len(content):
321                 content = entry.content[0].value
322         if content == "":
323             content = entry.get('description', '')
324         return content
325     
326     def removeEntry(self, id):
327         contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
328         if contentLink:
329             try:
330                 os.remove(contentLink)
331             except:
332                 print "File not found for deletion: %s" % contentLink
333         self.db.execute("DELETE FROM feed WHERE id=?;", (id,) )
334         self.db.execute("DELETE FROM images WHERE id=?;", (id,) )
335         self.db.commit()
336  
337 class ArchivedArticles(Feed):    
338     def addArchivedArticle(self, title, link, date, configdir):
339         id = self.generateUniqueId({"date":date, "title":title})
340         values = (id, title, link, date, 0, link, 0)
341         self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
342         self.db.commit()
343
344     def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
345         currentTime = 0
346         rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
347         for row in rows:
348             currentTime = time.time()
349             id = row[0]
350             link = row[1]
351             f = urllib2.urlopen(link)
352             #entry["content"] = f.read()
353             html = f.read()
354             f.close()
355             soup = BeautifulSoup(html)
356             images = soup('img')
357             baseurl = link
358             for img in images:
359                 filename = self.addImage(configdir, self.key, baseurl, img['src'])
360                 img['src']=filename
361                 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
362             contentLink = configdir+self.key+".d/"+id+".html"
363             file = open(contentLink, "w")
364             file.write(soup.prettify())
365             file.close()
366             
367             self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
368             self.db.commit()
369         return (currentTime, None, None)
370     
371     def purgeReadArticles(self):
372         rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
373         #ids = self.getIds()
374         for row in rows:
375             self.removeArticle(row[0])
376
377     def removeArticle(self, id):
378         rows = self.db.execute("SELECT imagePath FROM images WHERE id=?;", (id,) )
379         for row in rows:
380             try:
381                 count = self.db.execute("SELECT count(*) FROM images WHERE id!=? and imagePath=?;", (id,row[0]) ).fetchone()[0]
382                 if count == 0:
383                     os.remove(row[0])
384             except:
385                 pass
386         self.removeEntry(id)
387
388 class Listing:
389     # Lists all the feeds in a dictionary, and expose the data
390     def __init__(self, configdir):
391         self.configdir = configdir
392         
393         self.db = sqlite3.connect("%s/feeds.db" % self.configdir)
394         
395         try:
396             table = self.db.execute("SELECT sql FROM sqlite_master").fetchone()
397             if table == None:
398                 self.db.execute("CREATE TABLE feeds(id text, url text, title text, unread int, updateTime float, rank int, etag text, modified text, widget int);")
399                 if isfile(self.configdir+"feeds.pickle"):
400                     self.importOldFormatFeeds()
401                 else:
402                     self.addFeed("Maemo News", "http://maemo.org/news/items.xml")    
403             else:
404                 from string import find, upper
405                 if find(upper(table[0]), "WIDGET")<0:
406                     self.db.execute("ALTER TABLE feeds ADD COLUMN (widget int);")
407                     self.db.execute("UPDATE feeds SET widget=1;")
408                     self.db.commit()
409         except:
410             pass
411
412     def importOldFormatFeeds(self):
413         """This function loads feeds that are saved in an outdated format, and converts them to sqlite"""
414         import rss
415         listing = rss.Listing(self.configdir)
416         rank = 0
417         for id in listing.getListOfFeeds():
418             try:
419                 rank += 1
420                 values = (id, listing.getFeedTitle(id) , listing.getFeedUrl(id), 0, time.time(), rank, None, "None", 1)
421                 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?);", values)
422                 self.db.commit()
423                 
424                 feed = listing.getFeed(id)
425                 new_feed = self.getFeed(id)
426                 
427                 items = feed.getIds()[:]
428                 items.reverse()
429                 for item in items:
430                         if feed.isEntryRead(item):
431                             read_status = 1
432                         else:
433                             read_status = 0 
434                         date = timegm(feed.getDateTuple(item))
435                         title = feed.getTitle(item)
436                         newId = new_feed.generateUniqueId({"date":date, "title":title})
437                         values = (newId, title , feed.getContentLink(item), date, time.time(), feed.getExternalLink(item), read_status)
438                         new_feed.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
439                         new_feed.db.commit()
440                         try:
441                             images = feed.getImages(item)
442                             for image in images:
443                                 new_feed.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (item, image) )
444                                 new_feed.db.commit()
445                         except:
446                             pass
447                 self.updateUnread(id)
448             except:
449                 import traceback
450                 traceback.print_exc()
451         remove(self.configdir+"feeds.pickle")
452                 
453         
454     def addArchivedArticle(self, key, index):
455         feed = self.getFeed(key)
456         title = feed.getTitle(index)
457         link = feed.getExternalLink(index)
458         date = feed.getDate(index)
459         count = self.db.execute("SELECT count(*) FROM feeds where id=?;", ("ArchivedArticles",) ).fetchone()[0]
460         if count == 0:
461             self.addFeed("Archived Articles", "", id="ArchivedArticles")
462
463         archFeed = self.getFeed("ArchivedArticles")
464         archFeed.addArchivedArticle(title, link, date, self.configdir)
465         self.updateUnread("ArchivedArticles")
466         
467     def updateFeed(self, key, expiryTime=24, proxy=None, imageCache=False):
468         feed = self.getFeed(key)
469         db = sqlite3.connect("%s/feeds.db" % self.configdir)
470         (url, etag, modified) = db.execute("SELECT url, etag, modified FROM feeds WHERE id=?;", (key,) ).fetchone()
471         (updateTime, etag, modified) = feed.updateFeed(self.configdir, url, etag, eval(modified), expiryTime, proxy, imageCache)
472         if updateTime > 0:
473             db.execute("UPDATE feeds SET updateTime=?, etag=?, modified=? WHERE id=?;", (updateTime, etag, str(modified), key) )
474         else:
475             db.execute("UPDATE feeds SET etag=?, modified=? WHERE id=?;", (etag, str(modified), key) )
476         db.commit()
477         self.updateUnread(key, db=db)
478         
479     def getFeed(self, key):
480         if key == "ArchivedArticles":
481             return ArchivedArticles(self.configdir, key)
482         return Feed(self.configdir, key)
483         
484     def editFeed(self, key, title, url):
485         self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
486         self.db.commit()
487         
488     def getFeedUpdateTime(self, key):
489         return time.ctime(self.db.execute("SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0])
490         
491     def getFeedNumberOfUnreadItems(self, key):
492         return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
493         
494     def getFeedTitle(self, key):
495         return self.db.execute("SELECT title FROM feeds WHERE id=?;", (key,)).fetchone()[0]
496         
497     def getFeedUrl(self, key):
498         return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
499         
500     def getListOfFeeds(self):
501         rows = self.db.execute("SELECT id FROM feeds ORDER BY rank;" )
502         keys = []
503         for row in rows:
504             if row[0]:
505                 keys.append(row[0])
506         return keys
507     
508     def getSortedListOfKeys(self, order, onlyUnread=False):
509         if   order == "Most unread":
510             tmp = "ORDER BY unread DESC"
511             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
512         elif order == "Least unread":
513             tmp = "ORDER BY unread"
514             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
515         elif order == "Most recent":
516             tmp = "ORDER BY updateTime DESC"
517             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
518         elif order == "Least recent":
519             tmp = "ORDER BY updateTime"
520             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
521         else: # order == "Manual" or invalid value...
522             tmp = "ORDER BY rank"
523             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
524         if onlyUnread:
525             sql = "SELECT id FROM feeds WHERE unread>0 " + tmp
526         else:
527             sql = "SELECT id FROM feeds " + tmp
528         rows = self.db.execute(sql)
529         keys = []
530         for row in rows:
531             if row[0]:
532                 keys.append(row[0])
533         return keys
534     
535     def getFavicon(self, key):
536         filename = "%s%s.d/favicon.ico" % (self.configdir, key)
537         if isfile(filename):
538             return filename
539         else:
540             return False
541         
542     def updateUnread(self, key, db=None):
543         if db == None:
544             db = self.db
545         feed = self.getFeed(key)
546         db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
547         db.commit()
548
549     def addFeed(self, title, url, id=None):
550         if not id:
551             id = getId(title)
552         count = self.db.execute("SELECT count(*) FROM feeds WHERE id=?;", (id,) ).fetchone()[0]
553         if count == 0:
554             max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
555             if max_rank == None:
556                 max_rank = 0
557             values = (id, title, url, 0, 0, max_rank+1, None, "None", 1)
558             self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?);", values)
559             self.db.commit()
560             # Ask for the feed object, it will create the necessary tables
561             self.getFeed(id)
562             return True
563         else:
564             return False
565     
566     def removeFeed(self, key):
567         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
568         self.db.execute("DELETE FROM feeds WHERE id=?;", (key, ))
569         self.db.execute("UPDATE feeds SET rank=rank-1 WHERE rank>?;", (rank,) )
570         self.db.commit()
571
572         if isdir(self.configdir+key+".d/"):
573            rmtree(self.configdir+key+".d/")
574         #self.saveConfig()
575         
576     #def saveConfig(self):
577     #    self.listOfFeeds["feedingit-order"] = self.sortedKeys
578     #    file = open(self.configdir+"feeds.pickle", "w")
579     #    pickle.dump(self.listOfFeeds, file)
580     #    file.close()
581         
582     def moveUp(self, key):
583         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
584         if rank>0:
585             self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank-1) )
586             self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank-1, key) )
587             self.db.commit()
588         
589     def moveDown(self, key):
590         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
591         max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
592         if rank<max_rank:
593             self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank+1) )
594             self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank+1, key) )
595             self.db.commit()
596             
597             
598