1 # -*- coding: utf-8 -*-
3 # gPodder - A media aggregator and podcast client
4 # Copyright (c) 2005-2018 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/>.
21 # dbsqlite.py -- SQLite persistence layer for gPodder
23 # 2008-06-13 Justin Forest <justin.forest@gmail.com>
24 # 2010-04-24 Thomas Perl <thp@gpodder.org>
29 from sqlite3
import dbapi2
as sqlite
32 from gpodder
import schema
, util
36 logger
= logging
.getLogger(__name__
)
39 class Database(object):
40 TABLE_PODCAST
= 'podcast'
41 TABLE_EPISODE
= 'episode'
43 def __init__(self
, filename
):
44 self
.database_file
= filename
46 self
.lock
= threading
.RLock()
52 self
.db
.isolation_level
= None
53 self
.db
.execute('VACUUM')
54 self
.db
.isolation_level
= ''
59 def purge(self
, max_episodes
, podcast_id
):
61 Deletes old episodes. Should be called
62 before adding new episodes to a podcast.
70 logger
.debug('Purge requested for podcast %d', podcast_id
)
76 (SELECT id FROM %s WHERE podcast_id = ?
77 ORDER BY published DESC LIMIT ?)""" % (self
.TABLE_EPISODE
, self
.TABLE_EPISODE
)
78 cur
.execute(sql
, (podcast_id
, gpodder
.STATE_DOWNLOADED
, podcast_id
, max_episodes
))
85 self
._db
= sqlite
.connect(self
.database_file
, check_same_thread
=False)
87 # Check schema version, upgrade if necessary
88 schema
.upgrade(self
._db
, self
.database_file
)
90 # Sanity checks for the data in the database
91 schema
.check_data(self
)
93 logger
.debug('Database opened.')
97 return self
.db
.cursor()
102 logger
.debug('Commit.')
104 except Exception as e
:
105 logger
.error('Cannot commit: %s', e
, exc_info
=True)
107 def get_content_types(self
, id):
108 """Given a podcast ID, returns the content types"""
111 cur
.execute('SELECT mime_type FROM %s WHERE podcast_id = ?' % self
.TABLE_EPISODE
, (id,))
112 for (mime_type
,) in cur
:
116 def get_podcast_statistics(self
, podcast_id
=None):
117 """Given a podcast ID, returns the statistics for it
119 If the podcast_id is omitted (using the default value), the
120 statistics will be calculated over all podcasts.
122 Returns a tuple (total, deleted, new, downloaded, unplayed)
124 total
, deleted
, new
, downloaded
, unplayed
= 0, 0, 0, 0, 0
128 if podcast_id
is not None:
129 cur
.execute('SELECT COUNT(*), state, is_new FROM %s '
130 'WHERE podcast_id = ? GROUP BY state, is_new'
131 % self
.TABLE_EPISODE
, (podcast_id
,))
133 cur
.execute('SELECT COUNT(*), state, is_new FROM %s '
134 'GROUP BY state, is_new' % self
.TABLE_EPISODE
)
135 for count
, state
, is_new
in cur
:
137 if state
== gpodder
.STATE_DELETED
:
139 elif state
== gpodder
.STATE_NORMAL
and is_new
:
141 elif state
== gpodder
.STATE_DOWNLOADED
:
148 return (total
, deleted
, new
, downloaded
, unplayed
)
150 def load_podcasts(self
, factory
):
151 logger
.info('Loading podcasts')
153 sql
= 'SELECT * FROM %s' % self
.TABLE_PODCAST
159 keys
= [desc
[0] for desc
in cur
.description
]
160 result
= [factory(dict(list(zip(keys
, row
))), self
) for row
in cur
]
165 def load_episodes(self
, podcast
, factory
):
168 logger
.info('Loading episodes for podcast %d', podcast
.id)
170 sql
= 'SELECT * FROM %s WHERE podcast_id = ? ORDER BY published DESC' % self
.TABLE_EPISODE
175 cur
.execute(sql
, args
)
177 keys
= [desc
[0] for desc
in cur
.description
]
178 result
= [factory(dict(list(zip(keys
, row
)))) for row
in cur
]
183 def delete_podcast(self
, podcast
):
188 logger
.debug('delete_podcast: %d (%s)', podcast
.id, podcast
.url
)
190 cur
.execute("DELETE FROM %s WHERE id = ?" % self
.TABLE_PODCAST
, (podcast
.id, ))
191 cur
.execute("DELETE FROM %s WHERE podcast_id = ?" % self
.TABLE_EPISODE
, (podcast
.id, ))
196 def save_podcast(self
, podcast
):
197 self
._save
_object
(podcast
, self
.TABLE_PODCAST
, schema
.PodcastColumns
)
199 def save_episode(self
, episode
):
200 self
._save
_object
(episode
, self
.TABLE_EPISODE
, schema
.EpisodeColumns
)
202 def _save_object(self
, o
, table
, columns
):
206 values
= [util
.convert_bytes(getattr(o
, name
))
210 qmarks
= ', '.join('?' * len(columns
))
211 sql
= 'INSERT INTO %s (%s) VALUES (%s)' % (table
, ', '.join(columns
), qmarks
)
212 cur
.execute(sql
, values
)
215 qmarks
= ', '.join('%s = ?' % name
for name
in columns
)
217 sql
= 'UPDATE %s SET %s WHERE id = ?' % (table
, qmarks
)
218 cur
.execute(sql
, values
)
219 except Exception as e
:
220 logger
.error('Cannot save %s: %s', o
, e
, exc_info
=True)
224 def get(self
, sql
, params
=None):
226 Returns the first cell of a query result, useful for COUNT()s.
234 cur
.execute(sql
, params
)
244 def podcast_download_folder_exists(self
, foldername
):
246 Returns True if a foldername for a channel exists.
249 foldername
= util
.convert_bytes(foldername
)
251 return self
.get("SELECT id FROM %s WHERE download_folder = ?" %
252 self
.TABLE_PODCAST
, (foldername
,)) is not None
254 def episode_filename_exists(self
, podcast_id
, filename
):
256 Returns True if a filename for an episode exists.
259 filename
= util
.convert_bytes(filename
)
261 return self
.get("SELECT id FROM %s WHERE podcast_id = ? AND download_filename = ?" %
262 self
.TABLE_EPISODE
, (podcast_id
, filename
,)) is not None
264 def get_last_published(self
, podcast
):
266 Look up the most recent publish date of a podcast.
268 return self
.get('SELECT MAX(published) FROM %s WHERE podcast_id = ?' % self
.TABLE_EPISODE
, (podcast
.id,))
270 def delete_episode_by_guid(self
, guid
, podcast_id
):
272 Deletes episodes that have a specific GUID for
273 a given channel. Used after feed updates for
274 episodes that have disappeared from the feed.
276 guid
= util
.convert_bytes(guid
)
280 cur
.execute('DELETE FROM %s WHERE podcast_id = ? AND guid = ?' %
281 self
.TABLE_EPISODE
, (podcast_id
, guid
))