1 # -*- coding: utf-8 -*-
3 # Copyright (c) 2009-2013, 2019 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)
41 class SimaDBError(Exception):
48 class SimaDBAccessError(SimaDBError):
49 """Error on accessing DB file"""
53 class SimaDBNoFile(SimaDBError):
54 """No DB file present"""
61 def __init__(self, db_path=None):
62 self._db_path = db_path
63 self.db_path_mod_control()
65 def db_path_mod_control(self):
66 """Controls DB path access & write permissions"""
67 db_path = self._db_path
68 # Controls directory access
69 if not isdir(dirname(db_path)):
70 raise SimaDBAccessError('Not a regular directory: "%s"' %
72 if not access(dirname(db_path), W_OK):
73 raise SimaDBAccessError('No write access to "%s"' % dirname(db_path))
74 # Is a file but no write access
75 if access(db_path, F_OK) and not access(db_path, W_OK | F_OK):
76 raise SimaDBAccessError('No write access to "%s"' % db_path)
78 if not access(db_path, F_OK):
79 raise SimaDBNoFile('No DB file in "%s"' % db_path)
81 def close_database_connection(self, connection):
82 """Close the database connection."""
85 def get_database_connection(self):
86 """get database reference"""
87 connection = sqlite3.connect(
88 self._db_path, timeout=5.0, isolation_level=None)
89 #connection.text_factory = str
92 def get_artist(self, artist_name, mbid=None,
93 with_connection=None, add_not=False):
94 """get artist information from the database.
95 if not in database insert new entry."""
97 connection = with_connection
99 connection = self.get_database_connection()
100 rows = connection.execute(
101 "SELECT * FROM artists WHERE name = ?", (artist_name,))
103 if not with_connection:
104 self.close_database_connection(connection)
107 if not with_connection:
108 self.close_database_connection(connection)
111 "INSERT INTO artists (name, mbid) VALUES (?, ?)",
114 rows = connection.execute(
115 "SELECT * FROM artists WHERE name = ?", (artist_name,))
117 if not with_connection:
118 self.close_database_connection(connection)
120 if not with_connection:
121 self.close_database_connection(connection)
123 def get_track(self, track, with_connection=None, add_not=False):
125 Get a track from Tracks table, add if not existing,
126 Attention: use Track() object!!
127 if not in database insert new entry."""
132 connection = with_connection
134 connection = self.get_database_connection()
135 art_id = self.get_artist(art, with_connection=connection)[0]
136 alb_id = self.get_album(track, with_connection=connection)[0]
137 rows = connection.execute(
138 "SELECT * FROM tracks WHERE name = ? AND"
139 " artist = ? AND file = ?", (nam, art_id, fil))
141 if not with_connection:
142 self.close_database_connection(connection)
147 "INSERT INTO tracks (artist, album, name, file) VALUES (?, ?, ?, ?)",
148 (art_id, alb_id, nam, fil))
150 rows = connection.execute(
151 "SELECT * FROM tracks WHERE name = ? AND"
152 " artist = ? AND album = ? AND file = ?",
153 (nam, art_id, alb_id, fil,))
155 if not with_connection:
156 self.close_database_connection(connection)
158 if not with_connection:
160 self.close_database_connection(connection)
162 def get_album(self, track, mbid=None,
163 with_connection=None, add_not=False):
165 get album information from the database.
166 if not in database insert new entry.
167 Attention: use Track|Album object!!
168 Use AlbumArtist tag if provided, fallback to Album tag
171 connection = with_connection
173 connection = self.get_database_connection()
174 if track.albumartist:
175 artist = track.albumartist
177 artist = track.artist
178 art_id = self.get_artist(artist, with_connection=connection)[0]
180 rows = connection.execute(
181 "SELECT * FROM albums WHERE name = ? AND artist = ?",
184 if not with_connection:
185 self.close_database_connection(connection)
190 "INSERT INTO albums (name, artist, mbid) VALUES (?, ?, ?)",
191 (album, art_id, mbid))
193 rows = connection.execute(
194 "SELECT * FROM albums WHERE name = ? AND artist = ?",
197 if not with_connection:
198 self.close_database_connection(connection)
200 if not with_connection:
201 self.close_database_connection(connection)
203 def get_artists(self, with_connection=None):
204 """Returns all artists in DB"""
206 connection = with_connection
208 connection = self.get_database_connection()
209 rows = connection.execute("SELECT name FROM artists ORDER BY name")
210 results = [row for row in rows]
211 if not with_connection:
212 self.close_database_connection(connection)
213 for artist in results:
216 def get_bl_artist(self, artist_name,
217 with_connection=None, add_not=None):
218 """get blacklisted artist information from the database."""
220 connection = with_connection
222 connection = self.get_database_connection()
223 art = self.get_artist(artist_name, with_connection=connection,
228 rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
231 if not with_connection:
232 self.close_database_connection(connection)
235 if not with_connection:
236 self.close_database_connection(connection)
238 connection.execute("INSERT INTO black_list (artist) VALUES (?)",
240 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
241 " WHERE artist = ?", (art_id,))
243 rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
246 if not with_connection:
247 self.close_database_connection(connection)
249 if not with_connection:
250 self.close_database_connection(connection)
253 def get_bl_album(self, track, with_connection=None, add_not=None):
254 """get blacklisted album information from the database."""
256 connection = with_connection
258 connection = self.get_database_connection()
259 album = self.get_album(track, with_connection=connection,
264 rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
267 if not with_connection:
268 self.close_database_connection(connection)
271 if not with_connection:
272 self.close_database_connection(connection)
274 connection.execute("INSERT INTO black_list (album) VALUES (?)",
276 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
277 " WHERE album = ?", (alb_id,))
279 rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
282 if not with_connection:
283 self.close_database_connection(connection)
285 if not with_connection:
286 self.close_database_connection(connection)
289 def get_bl_track(self, track, with_connection=None, add_not=None):
290 """get blacklisted track information from the database."""
292 connection = with_connection
294 connection = self.get_database_connection()
295 track = self.get_track(track, with_connection=connection,
300 rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
303 if not with_connection:
304 self.close_database_connection(connection)
307 if not with_connection:
308 self.close_database_connection(connection)
310 connection.execute("INSERT INTO black_list (track) VALUES (?)",
312 connection.execute("UPDATE black_list SET updated = DATETIME('now')"
313 " WHERE track = ?", (track_id,))
315 rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
318 if not with_connection:
319 self.close_database_connection(connection)
321 if not with_connection:
322 self.close_database_connection(connection)
325 def get_artists_history(self, artists, duration=__HIST_DURATION__):
328 date = datetime.utcnow() - timedelta(hours=duration)
329 connection = self.get_database_connection()
330 rows = connection.execute(
331 "SELECT arts.name, albs.name, trs.name, trs.file"
332 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
333 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
334 " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),))
336 if artists and row[0] not in artists:
341 self.close_database_connection(connection)
343 def get_history(self, artist=None, artists=None, duration=__HIST_DURATION__):
344 """Retrieve complete play history, most recent tracks first
345 artist : filter history for specific artist
346 artists : filter history for specific artists list
347 """ # pylint: disable=C0301
348 date = datetime.utcnow() - timedelta(hours=duration)
349 connection = self.get_database_connection()
351 rows = connection.execute(
352 "SELECT arts.name, albs.name, trs.name, trs.file, hist.last_play"
353 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
354 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
355 " AND hist.last_play > ? AND arts.name = ?"
356 " ORDER BY hist.last_play DESC", (date.isoformat(' '), artist,))
358 rows = connection.execute(
359 "SELECT arts.name, albs.name, trs.name, trs.file"
360 " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs"
361 " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id"
362 " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),))
364 if artists and row[0] not in artists:
367 self.close_database_connection(connection)
369 def get_black_list(self):
370 """Retrieve complete black list."""
371 connection = self.get_database_connection()
372 rows = connection.execute('SELECT black_list.rowid, artists.name'
373 ' FROM artists INNER JOIN black_list'
374 ' ON artists.id = black_list.artist')
375 yield ('Row ID', 'Actual black listed element', 'Extra information',)
377 yield ('Row ID', 'Artist',)
380 rows = connection.execute(
381 'SELECT black_list.rowid, albums.name, artists.name'
382 ' FROM artists, albums INNER JOIN black_list'
383 ' ON albums.id = black_list.album'
384 ' WHERE artists.id = albums.artist')
386 yield ('Row ID', 'Album', 'Artist name')
389 rows = connection.execute(
390 'SELECT black_list.rowid, tracks.name, artists.name'
391 ' FROM artists, tracks INNER JOIN black_list'
392 ' ON tracks.id = black_list.track'
393 ' WHERE tracks.artist = artists.id')
395 yield ('Row ID', 'Title', 'Artist name')
398 self.close_database_connection(connection)
400 def _set_mbid(self, artist_id=None, mbid=None, with_connection=None):
403 connection = with_connection
405 connection = self.get_database_connection()
406 connection.execute("UPDATE artists SET mbid = ? WHERE id = ?",
409 if not with_connection:
410 self.close_database_connection(connection)
412 def _remove_bl(self, rowid):
413 """Remove bl row id"""
414 connection = self.get_database_connection()
415 connection.execute('DELETE FROM black_list'
416 ' WHERE black_list.rowid = ?', (rowid,))
418 self.close_database_connection(connection)
420 def add_history(self, track):
422 connection = self.get_database_connection()
423 track_id = self.get_track(track, with_connection=connection)[0]
424 rows = connection.execute("SELECT * FROM history WHERE track = ? ",
426 if not rows.fetchone():
427 connection.execute("INSERT INTO history (track) VALUES (?)",
429 connection.execute("UPDATE history SET last_play = DATETIME('now') "
430 " WHERE track = ?", (track_id,))
432 self.close_database_connection(connection)
434 def _clean_artists_table(self, with_connection=None):
435 """Clean orphan artists"""
437 connection = with_connection
439 connection = self.get_database_connection()
440 artists_ids = set([row[0] for row in connection.execute(
441 "SELECT id FROM artists")])
442 artist_2_artist_ids = set([row[0] for row in connection.execute(
443 "SELECT artist FROM black_list")] +
444 [row[0] for row in connection.execute(
445 "SELECT artist FROM albums")] +
446 [row[0] for row in connection.execute(
447 "SELECT artist FROM tracks")])
448 orphans = [(orphan,) for orphan in artists_ids - artist_2_artist_ids]
449 connection.executemany('DELETE FROM artists WHERE id = (?);', orphans)
450 if not with_connection:
452 self.close_database_connection(connection)
454 def _clean_albums_table(self, with_connection=None):
455 """Clean orphan albums"""
457 connection = with_connection
459 connection = self.get_database_connection()
460 orphan_black_ids = set([row[0] for row in connection.execute(
461 """SELECT albums.id FROM albums
462 LEFT JOIN black_list ON albums.id = black_list.album
463 WHERE ( black_list.album IS NULL )""")])
464 orphan_tracks_ids = set([row[0] for row in connection.execute(
465 """SELECT albums.id FROM albums
466 LEFT JOIN tracks ON albums.id = tracks.album
467 WHERE tracks.album IS NULL""")])
468 orphans = [(orphan,) for orphan in orphan_black_ids & orphan_tracks_ids]
469 connection.executemany('DELETE FROM albums WHERE id = (?);', orphans)
470 if not with_connection:
472 self.close_database_connection(connection)
474 def _clean_tracks_table(self, with_connection=None):
475 """Clean orphan tracks"""
477 connection = with_connection
479 connection = self.get_database_connection()
480 hist_orphan_ids = set([row[0] for row in connection.execute(
481 """SELECT tracks.id FROM tracks
482 LEFT JOIN history ON tracks.id = history.track
483 WHERE history.track IS NULL""")])
484 black_list_orphan_ids = set([row[0] for row in connection.execute(
485 """SELECT tracks.id FROM tracks
486 LEFT JOIN black_list ON tracks.id = black_list.track
487 WHERE black_list.track IS NULL""")])
488 orphans = [(orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids]
489 connection.executemany('DELETE FROM tracks WHERE id = (?);', orphans)
490 if not with_connection:
492 self.close_database_connection(connection)
494 def clean_database(self, with_connection=None):
495 """Wrapper around _clean_* methods"""
497 connection = with_connection
499 connection = self.get_database_connection()
500 self._clean_tracks_table(with_connection=connection)
501 self._clean_albums_table(with_connection=connection)
502 self._clean_artists_table(with_connection=connection)
503 connection.execute("VACUUM")
504 if not with_connection:
506 self.close_database_connection(connection)
508 def purge_history(self, duration=__HIST_DURATION__):
509 """Remove old entries in history"""
510 connection = self.get_database_connection()
511 connection.execute("DELETE FROM history WHERE last_play"
512 " < datetime('now', '-%i hours')" % duration)
514 self.close_database_connection(connection)
516 def _set_dbversion(self):
518 connection = self.get_database_connection()
519 connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
520 (__DB_VERSION__, 'Sima DB'))
522 self.close_database_connection(connection)
525 """ Set up a database
527 connection = self.get_database_connection()
529 'CREATE TABLE IF NOT EXISTS db_info'
530 ' (version INTEGER, name CHAR(36))')
532 'CREATE TABLE IF NOT EXISTS artists (id INTEGER PRIMARY KEY, name'
533 ' VARCHAR(100), mbid CHAR(36))')
535 'CREATE TABLE IF NOT EXISTS albums (id INTEGER PRIMARY KEY,'
536 ' artist INTEGER, name VARCHAR(100), mbid CHAR(36))')
538 'CREATE TABLE IF NOT EXISTS tracks (id INTEGER PRIMARY KEY,'
539 ' name VARCHAR(100), artist INTEGER, album INTEGER,'
540 ' file VARCHAR(500), mbid CHAR(36))')
542 'CREATE TABLE IF NOT EXISTS black_list (artist INTEGER,'
543 ' album INTEGER, track INTEGER, updated DATE)')
545 'CREATE TABLE IF NOT EXISTS history (last_play DATE,'
548 self.close_database_connection(connection)
549 self._set_dbversion()
553 # vim: ai ts=4 sw=4 sts=4 expandtab