1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2008 Thomas Perl and the gPodder Team
6 # gPodder is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # gPodder 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 General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 # dbsqlite.py -- SQLite interface
20 # Justin Forest <justin.forest@gmail.com> 2008-06-13
25 from sqlite3
import dbapi2
as sqlite
28 from pysqlite2
import dbapi2
as sqlite
32 # TODO: show a message box
34 print "Please install pysqlite2 or upgrade to Python 2.5."
38 from gpodder
.liblogger
import log
39 from email
.Utils
import mktime_tz
40 from email
.Utils
import parsedate_tz
41 from email
.Utils
import formatdate
42 from threading
import RLock
45 class Storage(object):
46 (STATE_NORMAL
, STATE_DOWNLOADED
, STATE_DELETED
) = range(3)
56 def setup(self
, settings
):
57 self
.settings
= settings
63 self
._db
= sqlite
.connect(self
.settings
['database'], check_same_thread
=False)
64 self
._db
.create_collation("unicode", lambda a
, b
: cmp(a
.lower().replace('the ', ''), b
.lower().replace('the ', '')))
65 log('SQLite connected', sender
=self
)
68 def cursor(self
, lock
=False):
71 return self
.db
.cursor()
77 except ProgrammingError
, e
:
78 log('Error commiting changes: %s', e
, sender
=self
, traceback
=True)
81 def __check_schema(self
):
83 Creates all necessary tables and indexes that don't exist.
85 log('Setting up SQLite database', sender
=self
)
87 cur
= self
.cursor(lock
=True)
89 cur
.execute("""CREATE TABLE IF NOT EXISTS channels (
90 id INTEGER PRIMARY KEY,
98 sync_to_devices INTEGER,
99 device_playlist_name TEXT,
106 cur
.execute("""CREATE UNIQUE INDEX IF NOT EXISTS idx_url ON channels (url)""")
107 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_sync_to_devices ON channels (sync_to_devices)""")
108 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_title ON channels (title)""")
109 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_deleted ON channels (deleted)""")
112 CREATE TABLE IF NOT EXISTS episodes (
113 id INTEGER PRIMARY KEY,
128 cur
.execute("""CREATE UNIQUE INDEX IF NOT EXISTS idx_guid ON episodes (guid)""")
129 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_channel_id ON episodes (channel_id)""")
130 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_pubDate ON episodes (pubDate)""")
131 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_state ON episodes (state)""")
132 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_played ON episodes (played)""")
133 cur
.execute("""CREATE INDEX IF NOT EXISTS idx_locked ON episodes (locked)""")
138 def get_channel_stat(self
, url_or_id
, state
=None, is_played
=None, is_locked
=None):
139 where
, params
= ((),())
141 if state
is not None:
142 where
+= ("state = ?", )
144 if is_played
is not None:
145 where
+= ("played = ?", )
146 params
+= (is_played
, )
147 if is_locked
is not None:
148 where
+= ("locked = ?", )
149 params
+= (is_locked
, )
150 if isinstance(url_or_id
, int):
151 where
+= ("channel_id = ?", )
152 params
+= (url_or_id
, )
154 where
+= ("channel_id IN (SELECT id FROM channels WHERE url = ?)", )
155 params
+= (url_or_id
, )
158 return self
.__get
__("SELECT COUNT(*) FROM episodes WHERE %s" % (' AND '.join(where
)), params
)
162 def load_channels(self
, factory
=None, url
=None):
164 Returns channel descriptions as a list of dictionaries or objects,
165 returned by the factory() function, which receives the dictionary
166 as the only argument.
169 cur
= self
.cursor(lock
=True)
181 device_playlist_name,
189 (deleted IS NULL OR deleted = 0)
191 title COLLATE unicode
195 for row
in cur
.fetchall():
200 'override_title': row
[3],
202 'description': row
[5],
204 'pubDate': self
.__formatdate
__(row
[7]),
205 'sync_to_devices': row
[8],
206 'device_playlist_name': row
[9],
209 'last_modified': row
[12],
214 # Maintain url/id relation for faster updates (otherwise
215 # we'd need to issue an extra query to find the channel id).
216 self
.channel_map
[channel
['url']] = channel
['id']
218 if url
is None or url
== channel
['url']:
220 result
.append(channel
)
222 result
.append(factory(channel
))
228 log('Channel list read, %d entries.', len(result
), sender
=self
)
230 log('Channel %s read from db', url
, sender
=self
)
234 def save_channel(self
, c
, bulk
=False):
236 c
.id = self
.find_channel_id(c
.url
)
238 cur
= self
.cursor(lock
=True)
241 cur
.execute("INSERT INTO channels (url, title, override_title, link, description, image, pubDate, sync_to_devices, device_playlist_name, username, password, last_modified, etag) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (c
.url
, c
.title
, c
.override_title
, c
.link
, c
.description
, c
.image
, self
.__mktime
__(c
.pubDate
), c
.sync_to_devices
, c
.device_playlist_name
, c
.username
, c
.password
, c
.last_modified
, c
.etag
, ))
242 self
.channel_map
[c
.url
] = cur
.lastrowid
243 log('Added channel %s[%d]', c
.url
, self
.channel_map
[c
.url
], sender
=self
)
245 cur
.execute("UPDATE channels SET url = ?, title = ?, override_title = ?, link = ?, description = ?, image = ?, pubDate = ?, sync_to_devices = ?, device_playlist_name = ?, username = ?, password = ?, last_modified = ?, etag = ?, deleted = 0 WHERE id = ?", (c
.url
, c
.title
, c
.override_title
, c
.link
, c
.description
, c
.image
, self
.__mktime
__(c
.pubDate
), c
.sync_to_devices
, c
.device_playlist_name
, c
.username
, c
.password
, c
.last_modified
, c
.etag
, c
.id, ))
250 def delete_channel(self
, channel
, purge
=False):
251 if channel
.id is None:
252 channel
.id = self
.find_channel_id(channel
.url
)
254 cur
= self
.cursor(lock
=True)
255 log('Deleting channel %d', channel
.id, sender
=self
)
258 cur
.execute("DELETE FROM channels WHERE id = ?", (channel
.id, ))
259 cur
.execute("DELETE FROM episodes WHERE channel_id = ?", (channel
.id, ))
260 if channel
.url
in self
.channel_map
:
261 del self
.channel_map
[channel
.url
]
263 cur
.execute("UPDATE channels SET deleted = 1 WHERE id = ?", (channel
.id, ))
264 cur
.execute("DELETE FROM episodes WHERE channel_id = ? AND state <> ?", (channel
.id, self
.STATE_DELETED
))
269 def __read_episodes(self
, factory
=None, where
=None, params
=None, commit
=True):
270 sql
= "SELECT url, title, length, mimetype, guid, description, link, pubDate, state, played, locked FROM episodes"
273 sql
= "%s %s" % (sql
, where
)
278 cur
= self
.cursor(lock
=True)
279 cur
.execute(sql
, params
)
282 for row
in cur
.fetchall():
289 'description': row
[5],
294 'is_locked': row
[10],
296 if episode
['state'] is None:
297 episode
['state'] = self
.STATE_NORMAL
299 result
.append(episode
)
301 result
.append(factory(episode
))
307 def load_episodes(self
, channel
, factory
=None, limit
=1000, state
=None):
308 if channel
.id is None:
309 channel
.id = self
.find_channel_id(channel
.url
)
312 return self
.__read
_episodes
(factory
= factory
, where
= """
313 WHERE channel_id = ? AND state = ? OR id IN
314 (SELECT id FROM episodes WHERE channel_id = ?
315 ORDER BY pubDate DESC LIMIT ?)
316 ORDER BY pubDate DESC
317 """, params
= (channel
.id, self
.STATE_DOWNLOADED
, channel
.id, limit
, ))
319 return self
.__read
_episodes
(factory
= factory
, where
= " WHERE channel_id = ? AND state = ? ORDER BY pubDate DESC LIMIT ?", params
= (channel
.id, state
, limit
, ))
321 def load_episode(self
, url
, factory
=None):
322 list = self
.__read
_episodes
(factory
= factory
, where
= " WHERE url = ?", params
= (url
, ))
326 def save_episode(self
, e
, bulk
=False):
328 log('Refusing to save an episode without guid: %s', e
)
335 channel_id
= self
.find_channel_id(e
.channel
.url
)
338 e
.id = self
.__get
__("SELECT id FROM episodes WHERE guid = ?", (e
.guid
, ))
341 log('Episode added: %s', e
.title
)
342 cur
.execute("INSERT INTO episodes (channel_id, url, title, length, mimetype, guid, description, link, pubDate, state, played, locked) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (channel_id
, e
.url
, e
.title
, e
.length
, e
.mimetype
, e
.guid
, e
.description
, e
.link
, self
.__mktime
__(e
.pubDate
), e
.state
, e
.is_played
, e
.is_locked
, ))
345 log('Episode updated: %s', e
.title
)
346 cur
.execute("UPDATE episodes SET title = ?, length = ?, mimetype = ?, description = ?, link = ?, pubDate = ? WHERE id = ?", (e
.title
, e
.length
, e
.mimetype
, e
.description
, e
.link
, self
.__mktime
__(e
.pubDate
), e
.id, ))
348 log('save_episode() failed: %s', e
, sender
=self
)
353 def mark_episode(self
, url
, state
=None, is_played
=None, is_locked
=None, toggle
=False):
354 cur
= self
.cursor(lock
=True)
355 cur
.execute("SELECT state, played, locked FROM episodes WHERE url = ?", (url
, ))
358 ( cur_state
, cur_played
, cur_locked
) = cur
.fetchone()
360 # This only happens when we try to mark an unknown episode,
361 # which is typical for database upgrade, so we just ignore it.
368 cur_played
= not cur_played
370 cur_locked
= not cur_locked
372 if state
is not None:
374 if is_played
is not None:
375 cur_played
= is_played
376 if is_locked
is not None:
377 cur_locked
= is_locked
382 cur
.execute("UPDATE episodes SET state = ?, played = ?, locked = ? WHERE url = ?", (cur_state
, cur_played
, cur_locked
, url
, ))
387 def __get__(self
, sql
, params
=None):
389 Returns the first cell of a query result, useful for COUNT()s.
391 cur
= self
.cursor(lock
=True)
396 cur
.execute(sql
, params
)
407 def __mktime__(self
, date
):
408 if isinstance(date
, float) or isinstance(date
, int):
410 if date
is None or '' == date
:
413 return mktime_tz(parsedate_tz(date
))
415 log('Could not convert "%s" to a unix timestamp.', date
)
418 def __formatdate__(self
, date
):
420 return formatdate(date
, localtime
=1)
422 log('Could not convert "%s" to a string date.', date
)
425 def find_channel_id(self
, url
):
427 Looks up the channel id in the map (which lists all undeleted
428 channels), then tries to look it up in the database, including
431 if url
in self
.channel_map
.keys():
432 return self
.channel_map
[url
]
434 return self
.__get
__("SELECT id FROM channels WHERE url = ?", (url
, ))
436 def force_last_new(self
, channel
):
437 old
= self
.__get
__("""SELECT COUNT(*) FROM episodes WHERE channel_id = ?
438 AND state IN (?, ?)""", (channel
.id, self
.STATE_DOWNLOADED
,
440 log('old episodes in (%d)%s: %d', channel
.id, channel
.url
, old
)
442 cur
= self
.cursor(lock
=True)
446 UPDATE episodes SET played = 1 WHERE channel_id = ?
447 AND played = 0 AND pubDate < (SELECT MAX(pubDate)
448 FROM episodes WHERE channel_id = ? AND state IN (?, ?))""",
449 (channel
.id, channel
.id, self
.STATE_DOWNLOADED
,
450 self
.STATE_DELETED
, ))
453 UPDATE episodes SET played = 1 WHERE channel_id = ?
454 AND pubDate <> (SELECT MAX(pubDate) FROM episodes
455 WHERE channel_id = ?)""", (channel
.id, channel
.id, ))