1 # -*- coding: utf-8 -*-
3 # Copyright (c) 2009-2013 Jack Kaliko <jack@azylum.org>
4 # Copyright (c) 2009, Eric Casteleijn <thisfred@gmail.com>
5 # Copyright (c) 2008 Rick van Hattem
7 # This file is part of sima
9 # sima is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # sima is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with sima. If not, see <http://www.gnu.org/licenses/>.
23 """SQlite database library
27 # MuscicBrainz ID: <http://musicbrainz.org/doc/MusicBrainzIdentifier>
28 # Artists: <http://musicbrainz.org/doc/Artist_Name>
29 # <http://musicbrainz.org/doc/Same_Artist_With_Different_Names>
32 __HIST_DURATION__ = int(30 * 24) # in hours
36 from datetime import (datetime, timedelta)
37 from os.path import dirname, isdir
38 from os import (access, W_OK, F_OK)
39 from shutil import copyfile
42 class SimaDBError(Exception):
49 class SimaDBAccessError(SimaDBError):
50 """Error on accessing DB file"""
54 class SimaDBNoFile(SimaDBError):
55 """No DB file present"""
62 def __init__(self, db_path=None):
63 self._db_path = db_path
64 self.db_path_mod_control()
66 def db_path_mod_control(self):
67 """Controls DB path access & write permissions"""
68 db_path = self._db_path
69 # Controls directory access
70 if not isdir(dirname(db_path)):
71 raise SimaDBAccessError('Not a regular directory: "%s"' %
73 if not access(dirname(db_path), W_OK):
74 raise SimaDBAccessError('No write access to "%s"' % dirname(db_path))
75 # Is a file but no write access
76 if access(db_path, F_OK) and not access(db_path, W_OK | F_OK):
77 raise SimaDBAccessError('No write access to "%s"' % db_path)
79 if not access(db_path, F_OK):
80 raise SimaDBNoFile('No DB file in "%s"' % db_path)
82 def close_database_connection(self, connection):
83 """Close the database connection."""
86 def get_database_connection(self):
87 """get database reference"""
88 connection = sqlite3.connect(
89 self._db_path, timeout=5.0, isolation_level="immediate")
90 #connection.text_factory = str
93 def get_artist(self, artist_name, mbid=None,
94 with_connection=None, add_not=False):
95 """get artist information from the database.
96 if not in database insert new entry."""
98 connection = with_connection
100 connection = self.get_database_connection()
101 rows = connection.execute(
102 "SELECT * FROM artists WHERE name = ?", (artist_name,))
104 if not with_connection:
105 self.close_database_connection(connection)
108 if not with_connection:
109 self.close_database_connection(connection)
112 "INSERT INTO artists (name, mbid) VALUES (?, ?)",
115 rows = connection.execute(
116 "SELECT * FROM artists WHERE name = ?", (artist_name,))
118 if not with_connection:
119 self.close_database_connection(connection)
121 if not with_connection:
122 self.close_database_connection(connection)
124 def get_track(self, track, with_connection=None, add_not=False):
126 Get a track from Tracks table, add if not existing,
127 Attention: use Track() object!!
128 if not in database insert new entry."""
133 connection = with_connection
135 connection = self.get_database_connection()
136 art_id = self.get_artist(art, with_connection=connection)[0]
137 alb_id = self.get_album(track, with_connection=connection)[0]
138 rows = connection.execute(
139 "SELECT * FROM tracks WHERE name = ? AND"
140 " artist = ? AND file = ?", (nam, art_id, fil))
142 if not with_connection:
143 self.close_database_connection(connection)
148 "INSERT INTO tracks (artist, album, name, file) VALUES (?, ?, ?, ?)",
149 (art_id, alb_id, nam, fil))
151 rows = connection.execute(
152 "SELECT * FROM tracks WHERE name = ? AND"
153 " artist = ? AND album = ? AND file = ?",
154 (nam, art_id, alb_id, fil,))
156 if not with_connection:
157 self.close_database_connection(connection)
159 if not with_connection:
161 self.close_database_connection(connection)
163 def get_album(self, track, mbid=None,
164 with_connection=None, add_not=False):
166 get album information from the database.
167 if not in database insert new entry.
168 Attention: use Track|Album object!!
169 Use AlbumArtist tag if provided, fallback to Album tag
172 connection = with_connection
174 connection = self.get_database_connection()
175 if track.albumartist:
176 artist = track.albumartist
178 artist = track.artist
179 art_id = self.get_artist(artist, with_connection=connection)[0]
181 rows = connection.execute(
182 "SELECT * FROM albums WHERE name = ? AND artist = ?",
185 if not with_connection:
186 self.close_database_connection(connection)
191 "INSERT INTO albums (name, artist, mbid) VALUES (?, ?, ?)",
192 (album, art_id, mbid))
194 rows = connection.execute(
195 "SELECT * FROM albums WHERE name = ? AND artist = ?",
198 if not with_connection:
199 self.close_database_connection(connection)
201 if not with_connection:
202 self.close_database_connection(connection)
204 def get_artists(self, with_connection=None):
205 """Returns all artists in DB"""
207 connection = with_connection
209 connection = self.get_database_connection()
210 rows = connection.execute("SELECT name FROM artists ORDER BY name")
211 results = [row for row in rows]
212 if not with_connection:
213 self.close_database_connection(connection)
214 for artist in results:
217 def get_bl_artist(self, artist_name,
218 with_connection=None, add_not=None):
219 """get blacklisted artist information from the database."""
221 connection = with_connection
223 connection = self.get_database_connection()
224 art = self.get_artist(artist_name,
225 with_connection=connection, add_not=add_not)
229 rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
232 if not with_connection:
233 self.close_database_connection(connection)
236 if not with_connection:
237 self.close_database_connection(connection)
239 connection.execute("INSERT INTO black_list (artist) VALUES (?)",
241 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
242 " WHERE artist = ?", (art_id,))
244 rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
247 if not with_connection:
248 self.close_database_connection(connection)
250 if not with_connection:
251 self.close_database_connection(connection)
254 def get_bl_album(self, track,
255 with_connection=None, add_not=None):
256 """get blacklisted album information from the database."""
258 connection = with_connection
260 connection = self.get_database_connection()
261 album = self.get_album(track,
262 with_connection=connection, add_not=add_not)
266 rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
269 if not with_connection:
270 self.close_database_connection(connection)
273 if not with_connection:
274 self.close_database_connection(connection)
276 connection.execute("INSERT INTO black_list (album) VALUES (?)",
278 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
279 " WHERE album = ?", (alb_id,))
281 rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
284 if not with_connection:
285 self.close_database_connection(connection)
287 if not with_connection:
288 self.close_database_connection(connection)
291 def get_bl_track(self, track, with_connection=None, add_not=None):
292 """get blacklisted track information from the database."""
294 connection = with_connection
296 connection = self.get_database_connection()
297 track = self.get_track(track,
298 with_connection=connection, add_not=add_not)
302 rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
305 if not with_connection:
306 self.close_database_connection(connection)
309 if not with_connection:
310 self.close_database_connection(connection)
312 connection.execute("INSERT INTO black_list (track) VALUES (?)",
314 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
315 " WHERE track = ?", (track_id,))
317 rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
320 if not with_connection:
321 self.close_database_connection(connection)
323 if not with_connection:
324 self.close_database_connection(connection)
327 def get_artists_history(self, artists, duration=__HIST_DURATION__):
330 date = datetime.utcnow() - timedelta(hours=duration)
331 connection = self.get_database_connection()
332 rows = connection.execute(
333 "SELECT arts.name, albs.name, trs.name, trs.file"
334 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
335 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
336 " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),))
338 if artists and row[0] not in artists:
343 self.close_database_connection(connection)
345 def get_history(self, artist=None, artists=None, duration=__HIST_DURATION__):
346 """Retrieve complete play history, most recent tracks first
347 artist : filter history for specific artist
348 artists : filter history for specific artists list
349 """ # pylint: disable=C0301
350 date = datetime.utcnow() - timedelta(hours=duration)
351 connection = self.get_database_connection()
353 rows = connection.execute(
354 "SELECT arts.name, albs.name, trs.name, trs.file, hist.last_play"
355 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
356 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
357 " AND hist.last_play > ? AND arts.name = ?"
358 " ORDER BY hist.last_play DESC", (date.isoformat(' '), artist,))
360 rows = connection.execute(
361 "SELECT arts.name, albs.name, trs.name, trs.file"
362 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
363 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
364 " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),))
366 if artists and row[0] not in artists:
369 self.close_database_connection(connection)
371 def get_black_list(self):
372 """Retrieve complete black list."""
373 connection = self.get_database_connection()
374 rows = connection.execute('SELECT black_list.rowid, artists.name'
375 ' FROM artists INNER JOIN black_list'
376 ' ON artists.id = black_list.artist')
377 yield ('Row ID', 'Actual black listed element', 'Extra information',)
379 yield ('Row ID', 'Artist',)
382 rows = connection.execute(
383 'SELECT black_list.rowid, albums.name, artists.name'
384 ' FROM artists, albums INNER JOIN black_list'
385 ' ON albums.id = black_list.album'
386 ' WHERE artists.id = albums.artist')
388 yield ('Row ID', 'Album', 'Artist name')
391 rows = connection.execute(
392 'SELECT black_list.rowid, tracks.name, artists.name'
393 ' FROM artists, tracks INNER JOIN black_list'
394 ' ON tracks.id = black_list.track'
395 ' WHERE tracks.artist = artists.id')
397 yield ('Row ID', 'Title', 'Artist name')
400 self.close_database_connection(connection)
402 def _set_mbid(self, artist_id=None, mbid=None, with_connection=None):
405 connection = with_connection
407 connection = self.get_database_connection()
408 connection.execute("UPDATE artists SET mbid = ? WHERE id = ?",
411 if not with_connection:
412 self.close_database_connection(connection)
414 def _remove_bl(self, rowid):
415 """Remove bl row id"""
416 connection = self.get_database_connection()
417 connection.execute('DELETE FROM black_list'
418 ' WHERE black_list.rowid = ?', (rowid,))
420 self.close_database_connection(connection)
422 def add_history(self, track):
424 connection = self.get_database_connection()
425 track_id = self.get_track(track, with_connection=connection)[0]
426 rows = connection.execute("SELECT * FROM history WHERE track = ? ",
428 if not rows.fetchone():
429 connection.execute("INSERT INTO history (track) VALUES (?)",
431 connection.execute("UPDATE history SET last_play = DATETIME('now') "
432 " WHERE track = ?", (track_id,))
434 self.close_database_connection(connection)
436 def _clean_artists_table(self, with_connection=None):
437 """Clean orphan artists"""
439 connection = with_connection
441 connection = self.get_database_connection()
442 artists_ids = set([row[0] for row in connection.execute(
443 "SELECT id FROM artists")])
444 artist_2_artist_ids = set([row[0] for row in connection.execute(
445 "SELECT artist FROM black_list")] +
446 [row[0] for row in connection.execute(
447 "SELECT artist FROM albums")] +
448 [row[0] for row in connection.execute(
449 "SELECT artist FROM tracks")])
450 orphans = [(orphan,) for orphan in artists_ids - artist_2_artist_ids]
451 connection.executemany('DELETE FROM artists WHERE id = (?);', orphans)
452 if not with_connection:
454 self.close_database_connection(connection)
456 def _clean_albums_table(self, with_connection=None):
457 """Clean orphan albums"""
459 connection = with_connection
461 connection = self.get_database_connection()
462 orphan_black_ids = set([row[0] for row in connection.execute(
463 """SELECT albums.id FROM albums
464 LEFT JOIN black_list ON albums.id = black_list.album
465 WHERE ( black_list.album IS NULL )""")])
466 orphan_tracks_ids = set([row[0] for row in connection.execute(
467 """SELECT albums.id FROM albums
468 LEFT JOIN tracks ON albums.id = tracks.album
469 WHERE tracks.album IS NULL""")])
470 orphans = [(orphan,) for orphan in orphan_black_ids & orphan_tracks_ids]
471 connection.executemany('DELETE FROM albums WHERE id = (?);', orphans)
472 if not with_connection:
474 self.close_database_connection(connection)
476 def _clean_tracks_table(self, with_connection=None):
477 """Clean orphan tracks"""
479 connection = with_connection
481 connection = self.get_database_connection()
482 hist_orphan_ids = set([row[0] for row in connection.execute(
483 """SELECT tracks.id FROM tracks
484 LEFT JOIN history ON tracks.id = history.track
485 WHERE history.track IS NULL""")])
486 black_list_orphan_ids = set([row[0] for row in connection.execute(
487 """SELECT tracks.id FROM tracks
488 LEFT JOIN black_list ON tracks.id = black_list.track
489 WHERE black_list.track IS NULL""")])
490 orphans = [(orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids]
491 connection.executemany('DELETE FROM tracks WHERE id = (?);', orphans)
492 if not with_connection:
494 self.close_database_connection(connection)
496 def clean_database(self, with_connection=None):
497 """Wrapper around _clean_* methods"""
499 connection = with_connection
501 connection = self.get_database_connection()
502 self._clean_tracks_table(with_connection=connection)
503 self._clean_albums_table(with_connection=connection)
504 self._clean_artists_table(with_connection=connection)
505 connection.execute("VACUUM")
506 if not with_connection:
508 self.close_database_connection(connection)
510 def purge_history(self, duration=__HIST_DURATION__):
511 """Remove old entries in history"""
512 connection = self.get_database_connection()
513 connection.execute("DELETE FROM history WHERE last_play"
514 " < datetime('now', '-%i hours')" % duration)
516 self.close_database_connection(connection)
518 def _set_dbversion(self):
520 connection = self.get_database_connection()
521 connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
522 (__DB_VERSION__, 'Sima DB'))
524 self.close_database_connection(connection)
527 """ Set up a database
529 connection = self.get_database_connection()
531 'CREATE TABLE IF NOT EXISTS db_info'
532 ' (version INTEGER, name CHAR(36))')
534 'CREATE TABLE IF NOT EXISTS artists (id INTEGER PRIMARY KEY, name'
535 ' VARCHAR(100), mbid CHAR(36))')
537 'CREATE TABLE IF NOT EXISTS albums (id INTEGER PRIMARY KEY,'
538 ' artist INTEGER, name VARCHAR(100), mbid CHAR(36))')
540 'CREATE TABLE IF NOT EXISTS tracks (id INTEGER PRIMARY KEY,'
541 ' name VARCHAR(100), artist INTEGER, album INTEGER,'
542 ' file VARCHAR(500), mbid CHAR(36))')
544 'CREATE TABLE IF NOT EXISTS black_list (artist INTEGER,'
545 ' album INTEGER, track INTEGER, updated DATE)')
547 'CREATE TABLE IF NOT EXISTS history (last_play DATE,'
550 self.close_database_connection(connection)
551 self._set_dbversion()
555 # vim: ai ts=4 sw=4 sts=4 expandtab