]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simadb.py
Uniform use of MetaContainer
[mpd-sima.git] / sima / lib / simadb.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (c) 2009-2013, 2019-2020 kaliko <kaliko@azylum.org>
4 # Copyright (c) 2009, Eric Casteleijn <thisfred@gmail.com>
5 # Copyright (c) 2008 Rick van Hattem
6 #
7 #  This file is part of sima
8 #
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.
13 #
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.
18 #
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/>.
21 #
22 #
23 """SQlite database library
24 """
25
26 #    DOC:
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>
30
31 __DB_VERSION__ = 3
32 __HIST_DURATION__ = int(30 * 24)  # in hours
33
34 import sqlite3
35
36 from datetime import (datetime, timedelta)
37 from os.path import dirname, isdir
38 from os import (access, W_OK, F_OK)
39
40
41 class SimaDBError(Exception):
42     """
43     Exceptions.
44     """
45     pass
46
47
48 class SimaDBAccessError(SimaDBError):
49     """Error on accessing DB file"""
50     pass
51
52
53 class SimaDBNoFile(SimaDBError):
54     """No DB file present"""
55     pass
56
57
58 class SimaDB(object):
59     "SQLite management"
60
61     def __init__(self, db_path=None):
62         self._db_path = db_path
63         self.db_path_mod_control()
64
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"' %
71                                     dirname(db_path))
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)
77         # No file
78         if not access(db_path, F_OK):
79             raise SimaDBNoFile('No DB file in "%s"' % db_path)
80
81     def close_database_connection(self, connection):
82         """Close the database connection."""
83         connection.close()
84
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
90         return connection
91
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."""
96         if with_connection:
97             connection = with_connection
98         else:
99             connection = self.get_database_connection()
100         rows = connection.execute(
101             "SELECT * FROM artists WHERE name = ?", (artist_name,))
102         for row in rows:
103             if not with_connection:
104                 self.close_database_connection(connection)
105             return row
106         if add_not:
107             if not with_connection:
108                 self.close_database_connection(connection)
109             return False
110         connection.execute(
111             "INSERT INTO artists (name, mbid) VALUES (?, ?)",
112             (artist_name, mbid))
113         connection.commit()
114         rows = connection.execute(
115             "SELECT * FROM artists WHERE name = ?", (artist_name,))
116         for row in rows:
117             if not with_connection:
118                 self.close_database_connection(connection)
119             return row
120         if not with_connection:
121             self.close_database_connection(connection)
122
123     def get_track(self, track, with_connection=None, add_not=False):
124         """
125         Get a track from Tracks table, add if not existing,
126         Attention: use Track() object!!
127         if not in database insert new entry."""
128         art = track.artist
129         nam = track.title
130         fil = track.file
131         if with_connection:
132             connection = with_connection
133         else:
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))
140         for row in rows:
141             if not with_connection:
142                 self.close_database_connection(connection)
143             return row
144         if add_not:
145             return False
146         connection.execute(
147             "INSERT INTO tracks (artist, album, name, file) VALUES (?, ?, ?, ?)",
148             (art_id, alb_id, nam, fil))
149         connection.commit()
150         rows = connection.execute(
151             "SELECT * FROM tracks WHERE name = ? AND"
152             " artist = ? AND album = ? AND file = ?",
153             (nam, art_id, alb_id, fil,))
154         for row in rows:
155             if not with_connection:
156                 self.close_database_connection(connection)
157             return row
158         if not with_connection:
159             connection.commit()
160             self.close_database_connection(connection)
161
162     def get_album(self, track, mbid=None,
163                   with_connection=None, add_not=False):
164         """
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
169         """
170         if with_connection:
171             connection = with_connection
172         else:
173             connection = self.get_database_connection()
174         if track.albumartist:
175             artist = track.albumartist
176         else:
177             artist = track.artist
178         art_id = self.get_artist(artist, with_connection=connection)[0]
179         album = track.album
180         rows = connection.execute(
181             "SELECT * FROM albums WHERE name = ? AND artist = ?",
182             (album, art_id))
183         for row in rows:
184             if not with_connection:
185                 self.close_database_connection(connection)
186             return row
187         if add_not:
188             return False
189         connection.execute(
190             "INSERT INTO albums (name, artist, mbid) VALUES (?, ?, ?)",
191             (album, art_id, mbid))
192         connection.commit()
193         rows = connection.execute(
194             "SELECT * FROM albums WHERE name = ? AND artist = ?",
195             (album, art_id))
196         for row in rows:
197             if not with_connection:
198                 self.close_database_connection(connection)
199             return row
200         if not with_connection:
201             self.close_database_connection(connection)
202
203     def get_artists(self, with_connection=None):
204         """Returns all artists in DB"""
205         if with_connection:
206             connection = with_connection
207         else:
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:
214             yield artist
215
216     def get_bl_artist(self, artist_name,
217                       with_connection=None, add_not=None):
218         """get blacklisted artist information from the database."""
219         if with_connection:
220             connection = with_connection
221         else:
222             connection = self.get_database_connection()
223         art = self.get_artist(artist_name, with_connection=connection,
224                               add_not=add_not)
225         if not art:
226             return False
227         art_id = art[0]
228         rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
229                                   (art_id,))
230         for row in rows:
231             if not with_connection:
232                 self.close_database_connection(connection)
233             return row
234         if add_not:
235             if not with_connection:
236                 self.close_database_connection(connection)
237             return False
238         connection.execute("INSERT INTO black_list (artist) VALUES (?)",
239                            (art_id,))
240         connection.execute("UPDATE black_list SET updated = DATETIME('now')"
241                            " WHERE artist = ?", (art_id,))
242         connection.commit()
243         rows = connection.execute("SELECT * FROM black_list WHERE artist = ?",
244                                   (art_id,))
245         for row in rows:
246             if not with_connection:
247                 self.close_database_connection(connection)
248             return row
249         if not with_connection:
250             self.close_database_connection(connection)
251         return False
252
253     def get_bl_album(self, track, with_connection=None, add_not=None):
254         """get blacklisted album information from the database."""
255         if with_connection:
256             connection = with_connection
257         else:
258             connection = self.get_database_connection()
259         album = self.get_album(track, with_connection=connection,
260                                add_not=add_not)
261         if not album:
262             return False
263         alb_id = album[0]
264         rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
265                                   (alb_id,))
266         for row in rows:
267             if not with_connection:
268                 self.close_database_connection(connection)
269             return row
270         if add_not:
271             if not with_connection:
272                 self.close_database_connection(connection)
273             return False
274         connection.execute("INSERT INTO black_list (album) VALUES (?)",
275                            (alb_id,))
276         connection.execute("UPDATE black_list SET updated = DATETIME('now')"
277                            " WHERE album = ?", (alb_id,))
278         connection.commit()
279         rows = connection.execute("SELECT * FROM black_list WHERE album = ?",
280                                   (alb_id,))
281         for row in rows:
282             if not with_connection:
283                 self.close_database_connection(connection)
284             return row
285         if not with_connection:
286             self.close_database_connection(connection)
287         return False
288
289     def get_bl_track(self, track, with_connection=None, add_not=None):
290         """get blacklisted track information from the database."""
291         if with_connection:
292             connection = with_connection
293         else:
294             connection = self.get_database_connection()
295         track = self.get_track(track, with_connection=connection,
296                                add_not=add_not)
297         if not track:
298             return False
299         track_id = track[0]
300         rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
301                                   (track_id,))
302         for row in rows:
303             if not with_connection:
304                 self.close_database_connection(connection)
305             return row
306         if add_not:
307             if not with_connection:
308                 self.close_database_connection(connection)
309             return False
310         connection.execute("INSERT INTO black_list (track) VALUES (?)",
311                            (track_id,))
312         connection.execute("UPDATE black_list SET updated = DATETIME('now')"
313                            " WHERE track = ?", (track_id,))
314         connection.commit()
315         rows = connection.execute("SELECT * FROM black_list WHERE track = ?",
316                                   (track_id,))
317         for row in rows:
318             if not with_connection:
319                 self.close_database_connection(connection)
320             return row
321         if not with_connection:
322             self.close_database_connection(connection)
323         return False
324
325     def get_artists_history(self, artists, duration=__HIST_DURATION__):
326         """
327         :param list artists: list of object that can evaluate equality with
328                              artist name, iterable of str or Artist object
329         """
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(' '),))
337         for row in rows:
338             if artists and row[0] not in artists:
339                 continue
340             for art in artists:
341                 if row[0] == art:
342                     yield art
343         self.close_database_connection(connection)
344
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()
352         if artist:
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,))
359         else:
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(' '),))
365         for row in rows:
366             if artists and row[0] not in artists:
367                 continue
368             yield row
369         self.close_database_connection(connection)
370
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',)
378         yield ('',)
379         yield ('Row ID', 'Artist',)
380         for row in rows:
381             yield row
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')
387         yield ('',)
388         yield ('Row ID', 'Album', 'Artist name')
389         for row in rows:
390             yield row
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')
396         yield ('',)
397         yield ('Row ID', 'Title', 'Artist name')
398         for row in rows:
399             yield row
400         self.close_database_connection(connection)
401
402     def _set_mbid(self, artist_id=None, mbid=None, with_connection=None):
403         """"""
404         if with_connection:
405             connection = with_connection
406         else:
407             connection = self.get_database_connection()
408         connection.execute("UPDATE artists SET mbid = ? WHERE id = ?",
409                            (mbid, artist_id))
410         connection.commit()
411         if not with_connection:
412             self.close_database_connection(connection)
413
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,))
419         connection.commit()
420         self.close_database_connection(connection)
421
422     def add_history(self, track):
423         """Add to history"""
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 = ? ",
427                                   (track_id,))
428         if not rows.fetchone():
429             connection.execute("INSERT INTO history (track) VALUES (?)",
430                                (track_id,))
431         connection.execute("UPDATE history SET last_play = DATETIME('now') "
432                            " WHERE track = ?", (track_id,))
433         connection.commit()
434         self.close_database_connection(connection)
435
436     def _clean_artists_table(self, with_connection=None):
437         """Clean orphan artists"""
438         if with_connection:
439             connection = with_connection
440         else:
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:
453             connection.commit()
454             self.close_database_connection(connection)
455
456     def _clean_albums_table(self, with_connection=None):
457         """Clean orphan albums"""
458         if with_connection:
459             connection = with_connection
460         else:
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:
473             connection.commit()
474             self.close_database_connection(connection)
475
476     def _clean_tracks_table(self, with_connection=None):
477         """Clean orphan tracks"""
478         if with_connection:
479             connection = with_connection
480         else:
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:
493             connection.commit()
494             self.close_database_connection(connection)
495
496     def clean_database(self, with_connection=None):
497         """Wrapper around _clean_* methods"""
498         if with_connection:
499             connection = with_connection
500         else:
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:
507             connection.commit()
508             self.close_database_connection(connection)
509
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)
515         connection.commit()
516         self.close_database_connection(connection)
517
518     def _set_dbversion(self):
519         """Add db version"""
520         connection = self.get_database_connection()
521         connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
522                            (__DB_VERSION__, 'Sima DB'))
523         connection.commit()
524         self.close_database_connection(connection)
525
526     def create_db(self):
527         """ Set up a database
528         """
529         connection = self.get_database_connection()
530         connection.execute(
531             'CREATE TABLE IF NOT EXISTS db_info'
532             ' (version INTEGER, name CHAR(36))')
533         connection.execute(
534             'CREATE TABLE IF NOT EXISTS artists (id INTEGER PRIMARY KEY, name'
535             ' VARCHAR(100), mbid CHAR(36))')
536         connection.execute(
537             'CREATE TABLE IF NOT EXISTS albums (id INTEGER PRIMARY KEY,'
538             ' artist INTEGER, name VARCHAR(100), mbid CHAR(36))')
539         connection.execute(
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))')
543         connection.execute(
544             'CREATE TABLE IF NOT EXISTS black_list (artist INTEGER,'
545             ' album INTEGER, track INTEGER, updated DATE)')
546         connection.execute(
547             'CREATE TABLE IF NOT EXISTS history (last_play DATE,'
548             ' track integer)')
549         connection.commit()
550         self.close_database_connection(connection)
551         self._set_dbversion()
552
553
554 # VIM MODLINE
555 # vim: ai ts=4 sw=4 sts=4 expandtab