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