More 0.9 fixes
[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                if(not(entry.has_key("id"))):
130                    entry["id"] = None 
131                tmpEntry = {"title":entry["title"], "content":self.extractContent(entry),
132                             "date":date, "link":entry["link"], "author":entry["author"], "id":entry["id"]}
133                id = self.generateUniqueId(tmpEntry)
134                
135                #articleTime = time.mktime(self.entries[id]["dateTuple"])
136                soup = BeautifulSoup(self.getArticle(tmpEntry)) #tmpEntry["content"])
137                images = soup('img')
138                baseurl = tmpEntry["link"]
139                #if not id in ids:
140                if imageCache:
141                    for img in images:
142                       try:
143                         filename = self.addImage(configdir, self.key, baseurl, img['src'])
144                         img['src']="file://%s" %filename
145                         count = self.db.execute("SELECT count(1) FROM images where id=? and imagePath=?;", (id, filename )).fetchone()[0]
146                         if count == 0:
147                             self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
148                       except:
149                           import traceback
150                           traceback.print_exc()
151                           print "Error downloading image %s" % img
152                    tmpEntry["contentLink"] = configdir+self.key+".d/"+id+".html"
153                    file = open(tmpEntry["contentLink"], "w")
154                    file.write(soup.prettify())
155                    file.close()
156                    if id in ids:
157                        self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
158                        self.db.commit()
159                    else:
160                        values = (id, tmpEntry["title"], tmpEntry["contentLink"], tmpEntry["date"], currentTime, tmpEntry["link"], 0)
161                        self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
162 #               else:
163 #                   try:
164 #                       self.db.execute("UPDATE feed SET updated=? WHERE id=?;", (currentTime, id) )
165 #                       self.db.commit()
166 #                       filename = configdir+self.key+".d/"+id+".html"
167 #                       file = open(filename,"a")
168 #                       utime(filename, None)
169 #                       file.close()
170 #                       images = self.db.execute("SELECT imagePath FROM images where id=?;", (id, )).fetchall()
171 #                       for image in images:
172 #                            file = open(image[0],"a")
173 #                            utime(image[0], None)
174 #                            file.close()
175 #                   except:
176 #                       pass
177            self.db.commit()
178             
179            
180         rows = self.db.execute("SELECT id FROM feed WHERE (read=0 AND updated<?) OR (read=1 AND updated<?);", (currentTime-2*expiry, currentTime-expiry))
181         for row in rows:
182            self.removeEntry(row[0])
183         
184         from glob import glob
185         from os import stat
186         for file in glob(configdir+self.key+".d/*"):
187             #
188             stats = stat(file)
189             #
190             # put the two dates into matching format
191             #
192             lastmodDate = stats[8]
193             #
194             expDate = time.time()-expiry*3
195             # check if image-last-modified-date is outdated
196             #
197             if expDate > lastmodDate:
198                 #
199                 try:
200                     #
201                     #print 'Removing', file
202                     #
203                     remove(file) # commented out for testing
204                     #
205                 except OSError:
206                     #
207                     print 'Could not remove', file
208         updateTime = 0
209         rows = self.db.execute("SELECT MAX(date) FROM feed;")
210         for row in rows:
211             updateTime=row[0]
212         return (updateTime, etag, modified)
213     
214     def setEntryRead(self, id):
215         self.db.execute("UPDATE feed SET read=1 WHERE id=?;", (id,) )
216         self.db.commit()
217         
218     def setEntryUnread(self, id):
219         self.db.execute("UPDATE feed SET read=0 WHERE id=?;", (id,) )
220         self.db.commit()     
221         
222     def markAllAsRead(self):
223         self.db.execute("UPDATE feed SET read=1 WHERE read=0;")
224         self.db.commit()
225
226     def isEntryRead(self, id):
227         read_status = self.db.execute("SELECT read FROM feed WHERE id=?;", (id,) ).fetchone()[0]
228         return read_status==1  # Returns True if read==1, and False if read==0
229     
230     def getTitle(self, id):
231         return self.db.execute("SELECT title FROM feed WHERE id=?;", (id,) ).fetchone()[0]
232     
233     def getContentLink(self, id):
234         return self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,) ).fetchone()[0]
235     
236     def getExternalLink(self, id):
237         return self.db.execute("SELECT link FROM feed WHERE id=?;", (id,) ).fetchone()[0]
238     
239     def getDate(self, id):
240         dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
241         return time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(dateStamp))
242
243     def getDateTuple(self, id):
244         dateStamp = self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
245         return time.localtime(dateStamp)
246     
247     def getDateStamp(self, id):
248         return self.db.execute("SELECT date FROM feed WHERE id=?;", (id,) ).fetchone()[0]
249     
250     def generateUniqueId(self, entry):
251         if(entry["id"] != None):
252             return getId(str(entry["id"]))
253         else:
254             return getId(str(entry["date"]) + str(entry["title"]))
255     
256     def getIds(self, onlyUnread=False):
257         if onlyUnread:
258             rows = self.db.execute("SELECT id FROM feed where read=0 ORDER BY date DESC;").fetchall()
259         else:
260             rows = self.db.execute("SELECT id FROM feed ORDER BY date DESC;").fetchall()
261         ids = []
262         for row in rows:
263             ids.append(row[0])
264         #ids.reverse()
265         return ids
266     
267     def getNextId(self, id):
268         ids = self.getIds()
269         index = ids.index(id)
270         return ids[(index+1)%len(ids)]
271         
272     def getPreviousId(self, id):
273         ids = self.getIds()
274         index = ids.index(id)
275         return ids[(index-1)%len(ids)]
276     
277     def getNumberOfUnreadItems(self):
278         return self.db.execute("SELECT count(*) FROM feed WHERE read=0;").fetchone()[0]
279     
280     def getNumberOfEntries(self):
281         return self.db.execute("SELECT count(*) FROM feed;").fetchone()[0]
282
283     def getArticle(self, entry):
284         #self.setEntryRead(id)
285         #entry = self.entries[id]
286         title = entry['title']
287         #content = entry.get('content', entry.get('summary_detail', {}))
288         content = entry["content"]
289
290         link = entry['link']
291         author = entry['author']
292         date = time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(entry["date"]) )
293
294         #text = '''<div style="color: black; background-color: white;">'''
295         text = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
296         text += "<html><head><title>" + title + "</title>"
297         text += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>\n'
298         #text += '<style> body {-webkit-user-select: none;} </style>'
299         text += '</head><body bgcolor=\"#ffffff\"><div><a href=\"' + link + '\">' + title + "</a>"
300         if author != None:
301             text += "<BR /><small><i>Author: " + author + "</i></small>"
302         text += "<BR /><small><i>Date: " + date + "</i></small></div>"
303         text += "<BR /><BR />"
304         text += content
305         text += "</body></html>"
306         return text
307    
308     def getContent(self, id):
309         contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
310         try:
311             file = open(self.entries[id]["contentLink"])
312             content = file.read()
313             file.close()
314         except:
315             content = "Content unavailable"
316         return content
317     
318     def extractDate(self, entry):
319         if entry.has_key("updated_parsed"):
320             return timegm(entry["updated_parsed"])
321         elif entry.has_key("published_parsed"):
322             return timegm(entry["published_parsed"])
323         else:
324             return time.time()
325         
326     def extractContent(self, entry):
327         content = ""
328         if entry.has_key('summary'):
329             content = entry.get('summary', '')
330         if entry.has_key('content'):
331             if len(entry.content[0].value) > len(content):
332                 content = entry.content[0].value
333         if content == "":
334             content = entry.get('description', '')
335         return content
336     
337     def removeEntry(self, id):
338         contentLink = self.db.execute("SELECT contentLink FROM feed WHERE id=?;", (id,)).fetchone()[0]
339         if contentLink:
340             try:
341                 os.remove(contentLink)
342             except:
343                 print "File not found for deletion: %s" % contentLink
344         self.db.execute("DELETE FROM feed WHERE id=?;", (id,) )
345         self.db.execute("DELETE FROM images WHERE id=?;", (id,) )
346         self.db.commit()
347  
348 class ArchivedArticles(Feed):    
349     def addArchivedArticle(self, title, link, date, configdir):
350         id = self.generateUniqueId({"date":date, "title":title})
351         values = (id, title, link, date, 0, link, 0)
352         self.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
353         self.db.commit()
354
355     def updateFeed(self, configdir, url, etag, modified, expiryTime=24, proxy=None, imageCache=False):
356         currentTime = 0
357         rows = self.db.execute("SELECT id, link FROM feed WHERE updated=0;")
358         for row in rows:
359             currentTime = time.time()
360             id = row[0]
361             link = row[1]
362             f = urllib2.urlopen(link)
363             #entry["content"] = f.read()
364             html = f.read()
365             f.close()
366             soup = BeautifulSoup(html)
367             images = soup('img')
368             baseurl = link
369             for img in images:
370                 filename = self.addImage(configdir, self.key, baseurl, img['src'])
371                 img['src']=filename
372                 self.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (id, filename) )
373             contentLink = configdir+self.key+".d/"+id+".html"
374             file = open(contentLink, "w")
375             file.write(soup.prettify())
376             file.close()
377             
378             self.db.execute("UPDATE feed SET read=0, contentLink=?, updated=? WHERE id=?;", (contentLink, time.time(), id) )
379             self.db.commit()
380         return (currentTime, None, None)
381     
382     def purgeReadArticles(self):
383         rows = self.db.execute("SELECT id FROM feed WHERE read=1;")
384         #ids = self.getIds()
385         for row in rows:
386             self.removeArticle(row[0])
387
388     def removeArticle(self, id):
389         rows = self.db.execute("SELECT imagePath FROM images WHERE id=?;", (id,) )
390         for row in rows:
391             try:
392                 count = self.db.execute("SELECT count(*) FROM images WHERE id!=? and imagePath=?;", (id,row[0]) ).fetchone()[0]
393                 if count == 0:
394                     os.remove(row[0])
395             except:
396                 pass
397         self.removeEntry(id)
398
399 class Listing:
400     # Lists all the feeds in a dictionary, and expose the data
401     def __init__(self, configdir):
402         self.configdir = configdir
403         
404         self.db = sqlite3.connect("%s/feeds.db" % self.configdir)
405         
406         try:
407             table = self.db.execute("SELECT sql FROM sqlite_master").fetchone()
408             if table == None:
409                 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);")
410                 self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
411                 self.addCategory("Default Category")
412                 if isfile(self.configdir+"feeds.pickle"):
413                     self.importOldFormatFeeds()
414                 else:
415                     self.addFeed("Maemo News", "http://maemo.org/news/items.xml")    
416             else:
417                 from string import find, upper
418                 if find(upper(table[0]), "WIDGET")<0:
419                     self.db.execute("ALTER TABLE feeds ADD COLUMN widget int;")
420                     self.db.execute("UPDATE feeds SET widget=1;")
421                     self.db.commit()
422                 if find(upper(table[0]), "CATEGORY")<0:
423                     self.db.execute("CREATE TABLE categories(id text, title text, unread int, rank int);")
424                     self.addCategory("Default Category")
425                     self.db.execute("ALTER TABLE feeds ADD COLUMN category int;")
426                     self.db.execute("UPDATE feeds SET category=1;")
427                     self.db.commit()
428         except:
429             pass
430
431     def importOldFormatFeeds(self):
432         """This function loads feeds that are saved in an outdated format, and converts them to sqlite"""
433         import rss
434         listing = rss.Listing(self.configdir)
435         rank = 0
436         for id in listing.getListOfFeeds():
437             try:
438                 rank += 1
439                 values = (id, listing.getFeedTitle(id) , listing.getFeedUrl(id), 0, time.time(), rank, None, "None", 1)
440                 self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?, 1);", values)
441                 self.db.commit()
442                 
443                 feed = listing.getFeed(id)
444                 new_feed = self.getFeed(id)
445                 
446                 items = feed.getIds()[:]
447                 items.reverse()
448                 for item in items:
449                         if feed.isEntryRead(item):
450                             read_status = 1
451                         else:
452                             read_status = 0 
453                         date = timegm(feed.getDateTuple(item))
454                         title = feed.getTitle(item)
455                         newId = new_feed.generateUniqueId({"date":date, "title":title})
456                         values = (newId, title , feed.getContentLink(item), date, tuple(time.time()), feed.getExternalLink(item), read_status)
457                         new_feed.db.execute("INSERT INTO feed (id, title, contentLink, date, updated, link, read) VALUES (?, ?, ?, ?, ?, ?, ?);", values)
458                         new_feed.db.commit()
459                         try:
460                             images = feed.getImages(item)
461                             for image in images:
462                                 new_feed.db.execute("INSERT INTO images (id, imagePath) VALUES (?, ?);", (item, image) )
463                                 new_feed.db.commit()
464                         except:
465                             pass
466                 self.updateUnread(id)
467             except:
468                 import traceback
469                 traceback.print_exc()
470         remove(self.configdir+"feeds.pickle")
471                 
472         
473     def addArchivedArticle(self, key, index):
474         feed = self.getFeed(key)
475         title = feed.getTitle(index)
476         link = feed.getExternalLink(index)
477         date = feed.getDate(index)
478         count = self.db.execute("SELECT count(*) FROM feeds where id=?;", ("ArchivedArticles",) ).fetchone()[0]
479         if count == 0:
480             self.addFeed("Archived Articles", "", id="ArchivedArticles")
481
482         archFeed = self.getFeed("ArchivedArticles")
483         archFeed.addArchivedArticle(title, link, date, self.configdir)
484         self.updateUnread("ArchivedArticles")
485         
486     def updateFeed(self, key, expiryTime=24, proxy=None, imageCache=False):
487         feed = self.getFeed(key)
488         db = sqlite3.connect("%s/feeds.db" % self.configdir)
489         (url, etag, modified) = db.execute("SELECT url, etag, modified FROM feeds WHERE id=?;", (key,) ).fetchone()
490         try:
491             modified = time.struct_time(eval(modified))
492         except:
493             modified = None
494         (updateTime, etag, modified) = feed.updateFeed(self.configdir, url, etag, modified, expiryTime, proxy, imageCache)
495         if updateTime > 0:
496             db.execute("UPDATE feeds SET updateTime=?, etag=?, modified=? WHERE id=?;", (updateTime, etag, str(tuple(modified)), key) )
497         else:
498             db.execute("UPDATE feeds SET etag=?, modified=? WHERE id=?;", (etag, str(tuple(modified)), key) )
499         db.commit()
500         self.updateUnread(key, db=db)
501         
502     def getFeed(self, key):
503         if key == "ArchivedArticles":
504             return ArchivedArticles(self.configdir, key)
505         return Feed(self.configdir, key)
506         
507     def editFeed(self, key, title, url, category=None):
508         if category:
509             self.db.execute("UPDATE feeds SET title=?, url=?, category=? WHERE id=?;", (title, url, category, key))
510         else:
511             self.db.execute("UPDATE feeds SET title=?, url=? WHERE id=?;", (title, url, key))
512         self.db.commit()
513         
514     def getFeedUpdateTime(self, key):
515         return time.ctime(self.db.execute("SELECT updateTime FROM feeds WHERE id=?;", (key,)).fetchone()[0])
516         
517     def getFeedNumberOfUnreadItems(self, key):
518         return self.db.execute("SELECT unread FROM feeds WHERE id=?;", (key,)).fetchone()[0]
519         
520     def getFeedTitle(self, key):
521         return self.db.execute("SELECT title FROM feeds WHERE id=?;", (key,)).fetchone()[0]
522         
523     def getFeedUrl(self, key):
524         return self.db.execute("SELECT url FROM feeds WHERE id=?;", (key,)).fetchone()[0]
525     
526     def getFeedCategory(self, key):
527         return self.db.execute("SELECT category FROM feeds WHERE id=?;", (key,)).fetchone()[0]
528         
529     def getListOfFeeds(self, category=None):
530         if category:
531             rows = self.db.execute("SELECT id FROM feeds WHERE category=? ORDER BY rank;", (category, ) )
532         else:
533             rows = self.db.execute("SELECT id FROM feeds ORDER BY rank;" )
534         keys = []
535         for row in rows:
536             if row[0]:
537                 keys.append(row[0])
538         return keys
539     
540     def getListOfCategories(self):
541         rows = self.db.execute("SELECT id FROM categories ORDER BY rank;" )
542         keys = []
543         for row in rows:
544             if row[0]:
545                 keys.append(row[0])
546         return keys
547     
548     def getCategoryTitle(self, id):
549         row = self.db.execute("SELECT title FROM categories WHERE id=?;", (id, )).fetchone()
550         return row[0]
551     
552     def getSortedListOfKeys(self, order, onlyUnread=False, category=1):
553         if   order == "Most unread":
554             tmp = "ORDER BY unread DESC"
555             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1], reverse=True)
556         elif order == "Least unread":
557             tmp = "ORDER BY unread"
558             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][1])
559         elif order == "Most recent":
560             tmp = "ORDER BY updateTime DESC"
561             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2], reverse=True)
562         elif order == "Least recent":
563             tmp = "ORDER BY updateTime"
564             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][2])
565         else: # order == "Manual" or invalid value...
566             tmp = "ORDER BY rank"
567             #keyorder = sorted(feedInfo, key = lambda k: feedInfo[k][0])
568         if onlyUnread:
569             sql = "SELECT id FROM feeds WHERE unread>0 AND category=%s " %category + tmp 
570         else:
571             sql = "SELECT id FROM feeds WHERE category=%s " %category + tmp
572         rows = self.db.execute(sql)
573         keys = []
574         for row in rows:
575             if row[0]:
576                 keys.append(row[0])
577         return keys
578     
579     def getFavicon(self, key):
580         filename = "%s%s.d/favicon.ico" % (self.configdir, key)
581         if isfile(filename):
582             return filename
583         else:
584             return False
585         
586     def updateUnread(self, key, db=None):
587         if db == None:
588             db = self.db
589         feed = self.getFeed(key)
590         db.execute("UPDATE feeds SET unread=? WHERE id=?;", (feed.getNumberOfUnreadItems(), key))
591         db.commit()
592
593     def addFeed(self, title, url, id=None, category=1):
594         if not id:
595             id = getId(title)
596         count = self.db.execute("SELECT count(*) FROM feeds WHERE id=?;", (id,) ).fetchone()[0]
597         if count == 0:
598             max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
599             if max_rank == None:
600                 max_rank = 0
601             values = (id, title, url, 0, 0, max_rank+1, None, "None", 1, category)
602             self.db.execute("INSERT INTO feeds (id, title, url, unread, updateTime, rank, etag, modified, widget, category) VALUES (?, ?, ? ,? ,? ,?, ?, ?, ?,?);", values)
603             self.db.commit()
604             # Ask for the feed object, it will create the necessary tables
605             self.getFeed(id)
606             return True
607         else:
608             return False
609         
610     def addCategory(self, title):
611         rank = self.db.execute("SELECT MAX(rank)+1 FROM categories;").fetchone()[0]
612         if rank==None:
613             rank=1
614         id = self.db.execute("SELECT MAX(id)+1 FROM categories;").fetchone()[0]
615         if id==None:
616             id=1
617         self.db.execute("INSERT INTO categories (id, title, unread, rank) VALUES (?, ?, 0, ?)", (id, title, rank))
618         self.db.commit()
619     
620     def removeFeed(self, key):
621         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,) ).fetchone()[0]
622         self.db.execute("DELETE FROM feeds WHERE id=?;", (key, ))
623         self.db.execute("UPDATE feeds SET rank=rank-1 WHERE rank>?;", (rank,) )
624         self.db.commit()
625
626         if isdir(self.configdir+key+".d/"):
627            rmtree(self.configdir+key+".d/")
628            
629     def removeCategory(self, key):
630         if self.db.execute("SELECT count(*) FROM categories;").fetchone()[0] > 1:
631             rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,) ).fetchone()[0]
632             self.db.execute("DELETE FROM categories WHERE id=?;", (key, ))
633             self.db.execute("UPDATE categories SET rank=rank-1 WHERE rank>?;", (rank,) )
634             self.db.execute("UPDATE feeds SET category=1 WHERE category=?;", (key,) )
635             self.db.commit()
636         
637     #def saveConfig(self):
638     #    self.listOfFeeds["feedingit-order"] = self.sortedKeys
639     #    file = open(self.configdir+"feeds.pickle", "w")
640     #    pickle.dump(self.listOfFeeds, file)
641     #    file.close()
642         
643     def moveUp(self, key):
644         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
645         if rank>0:
646             self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank-1) )
647             self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank-1, key) )
648             self.db.commit()
649             
650     def moveCategoryUp(self, key):
651         rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
652         if rank>0:
653             self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank-1) )
654             self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank-1, key) )
655             self.db.commit()
656         
657     def moveDown(self, key):
658         rank = self.db.execute("SELECT rank FROM feeds WHERE id=?;", (key,)).fetchone()[0]
659         max_rank = self.db.execute("SELECT MAX(rank) FROM feeds;").fetchone()[0]
660         if rank<max_rank:
661             self.db.execute("UPDATE feeds SET rank=? WHERE rank=?;", (rank, rank+1) )
662             self.db.execute("UPDATE feeds SET rank=? WHERE id=?;", (rank+1, key) )
663             self.db.commit()
664             
665     def moveCategoryDown(self, key):
666         rank = self.db.execute("SELECT rank FROM categories WHERE id=?;", (key,)).fetchone()[0]
667         max_rank = self.db.execute("SELECT MAX(rank) FROM categories;").fetchone()[0]
668         if rank<max_rank:
669             self.db.execute("UPDATE categories SET rank=? WHERE rank=?;", (rank, rank+1) )
670             self.db.execute("UPDATE categories SET rank=? WHERE id=?;", (rank+1, key) )
671             self.db.commit()
672             
673