From 5fe20b6caffe162afe5be18e77fe40004d00c95e Mon Sep 17 00:00:00 2001 From: kaliko Date: Mon, 23 Sep 2013 20:35:20 +0200 Subject: [PATCH] Add SimaDB and deal with history --- README | 8 + launch | 27 +- sima/client.py | 14 +- sima/core.py | 90 ++++- sima/lib/logger.py | 3 +- sima/lib/plugin.py | 13 +- sima/lib/simadb.py | 730 ++++++++++++++++++++++++++++++++++++++++ sima/plugins/addhist.py | 34 ++ sima/plugins/crop.py | 1 + sima/plugins/lastfm.py | 18 + sima/utils/config.py | 14 +- 11 files changed, 916 insertions(+), 36 deletions(-) create mode 100644 sima/lib/simadb.py create mode 100644 sima/plugins/addhist.py create mode 100644 sima/plugins/lastfm.py diff --git a/README b/README index ec5133d..f6eb1d0 100644 --- a/README +++ b/README @@ -3,3 +3,11 @@ Design for python >= 3.3 Requires python-musicpd: http://media.kaliko.me/src/musicpd/ + + +Changes with MPD_sima + + * project renamed sima + → defaults to + config : ~/.config/sima/sima.cfg + cache : ~/.local/share/sima/sima.db diff --git a/launch b/launch index f3603cb..5c50ef1 100755 --- a/launch +++ b/launch @@ -1,12 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +"""Sima +""" import logging +import sys + +from os.path import isfile ## from sima import core from sima.plugins.crop import Crop +from sima.plugins.addhist import History from sima.lib.logger import set_logger +from sima.lib.simadb import SimaDB from sima.utils.config import ConfMan from sima.utils.startopt import StartOpt from sima.utils.utils import exception_log @@ -20,10 +27,10 @@ def main(): # StartOpt gathers options from command line call (in StartOpt().options) sopt = StartOpt(info) # set logger - set_logger(level='debug') + verbosity = sopt.options.get('verbosity', 'warning') + cli_loglevel = getattr(logging, verbosity.upper()) + set_logger(level=verbosity) logger = logging.getLogger('sima') - cli_loglevel = getattr(logging, - sopt.options.get('verbosity', 'warning').upper()) logger.setLevel(cli_loglevel) # loads configuration conf_manager = ConfMan(logger, sopt.options) @@ -32,9 +39,21 @@ def main(): config.get('log', 'verbosity').upper())) # pylint: disable=E1103 logger.debug('Command line say: {0}'.format(sopt.options)) + + # Create Database + if (sopt.options.get('create_db', None) + or not isfile(conf_manager.db_file)): + logger.info('Creating database in "{}"'.format(conf_manager.db_file)) + open(conf_manager.db_file, 'a').close() + SimaDB(db_path=conf_manager.db_file).create_db() + if sopt.options.get('create_db', None): + logger.info('Done, bye...') + sys.exit(0) + logger.info('Starting...') - sima = core.Sima() + sima = core.Sima(config, conf_manager.db_file) sima.register_plugin(Crop) + sima.register_plugin(History) try: sima.run() except KeyboardInterrupt: diff --git a/sima/client.py b/sima/client.py index 331c02c..641fa22 100644 --- a/sima/client.py +++ b/sima/client.py @@ -26,6 +26,7 @@ class PlayerError(Exception): class PlayerCommandError(PlayerError): """Command error""" +PlayerUnHandledError = MPDError class PlayerClient(Player): """MPC Client @@ -91,6 +92,13 @@ class PlayerClient(Player): return Track(**ans) return ans + def __skipped_track(self, old_curr): + if (self.state == 'stop' + or not hasattr(old_curr, 'id') + or not hasattr(self.current, 'id')): + return False + return (self.current.id != old_curr.id) # pylint: disable=no-member + def find_track(self, artist, title=None): #return getattr(self, 'find')('artist', artist, 'title', title) if title: @@ -108,10 +116,14 @@ class PlayerClient(Player): return self.find('artist', artist, 'album', album) def monitor(self): + curr = self.current try: self._client.send_idle('database', 'playlist', 'player', 'options') select([self._client], [], [], 60) - return self._client.fetch_idle() + ret = self._client.fetch_idle() + if self.__skipped_track(curr): + ret.append('skipped') + return ret except (MPDError, IOError) as err: raise PlayerError("Couldn't init idle: %s" % err) diff --git a/sima/core.py b/sima/core.py index c751e5b..3710d92 100644 --- a/sima/core.py +++ b/sima/core.py @@ -7,52 +7,108 @@ __version__ = '0.12.0.b' __author__ = 'kaliko jack' __url__ = 'git://git.kaliko.me/sima.git' +import sys +import time + from logging import getLogger -from .client import PlayerClient +from .client import PlayerClient, Track +from .client import PlayerError, PlayerUnHandledError +from .lib.simadb import SimaDB class Sima(object): """Main class, plugin and player management """ - def __init__(self): + def __init__(self, conf, dbfile): + self.config = conf + self.sdb = SimaDB(db_path=dbfile) self.log = getLogger('sima') self.plugins = list() - self.player = None - self.connect_player() + self.player = PlayerClient() # Player client + try: + self.player.connect() + except (PlayerError, PlayerUnHandledError) as err: + self.log.error('Fails to connect player: {}'.format(err)) + self.shutdown() self.current_track = None def register_plugin(self, plugin_class): + """Registers plubin in Sima instance...""" self.plugins.append(plugin_class(self)) def foreach_plugin(self, method, *args, **kwds): + """Plugin's callbacks dispatcher""" for plugin in self.plugins: getattr(plugin, method)(*args, **kwds) - def connect_player(self): - """Instanciate player client and connect + def reconnect_player(self): + """Trying to reconnect cycling through longer timeout + cycle : 5s 10s 1m 5m 20m 1h """ - self.player = PlayerClient() # Player client - self.player.connect() + sleepfor = [5, 10, 60, 300, 1200, 3600] + while True: + tmp = sleepfor.pop(0) + sleepfor.append(tmp) + self.log.info('Trying to reconnect in {:>4d} seconds'.format(tmp)) + time.sleep(tmp) + try: + self.player.connect() + except PlayerError: + continue + except PlayerUnHandledError as err: + #TODO: unhandled Player exceptions + self.log.warning('Unhandled player exception: %s' % err) + self.log.info('Got reconnected') + break def shutdown(self): """General shutdown method """ + self.log.warning('Starting shutdown.') self.player.disconnect() self.foreach_plugin('shutdown') + self.log.info('The way is shut, it was made by those who are dead. ' + 'And the dead keep it…') + self.log.info('bye...') + sys.exit(0) + def run(self): - """Dispatching callbacks to plugins """ - self.log.debug(self.player.status()) - self.log.info(self.player.current) + """ + self.current_track = Track() while 42: - # hanging here untill a monitored event is raised in the player - changed = self.player.monitor() - if 'playlist' in changed: - self.foreach_plugin('callback_playlist') - if 'player' in changed: - self.log.info(self.player.current) + try: + self.loop() + except PlayerUnHandledError as err: + #TODO: unhandled Player exceptions + self.log.warning('Unhandled player exception: {}'.format(err)) + del(self.player) + self.player = PlayerClient() + time.sleep(10) + except PlayerError as err: + self.log.warning('Player error: %s' % err) + self.reconnect_player() + + def loop(self): + """Dispatching callbacks to plugins + """ + # hanging here untill a monitored event is raised in the player + if getattr(self, 'changed', False): # first loop detection + self.changed = self.player.monitor() + else: + self.changed = ['playlist', 'player', 'skipped'] + self.log.debug('changed: {}'.format(', '.join(self.changed))) + if 'playlist' in self.changed: + self.foreach_plugin('callback_playlist') + if 'player' in self.changed: + self.foreach_plugin('callback_player') + if 'skipped' in self.changed: + if self.player.state == 'play': + self.log.info('Playing: {}'.format(self.player.current)) + self.foreach_plugin('callback_next_song') + self.current_track = self.player.current # VIM MODLINE diff --git a/sima/lib/logger.py b/sima/lib/logger.py index 7521188..8c6cd2f 100644 --- a/sima/lib/logger.py +++ b/sima/lib/logger.py @@ -27,7 +27,8 @@ import logging import sys LOG_FORMATS = { - logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) {levelname}: {message}', + logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) ' + '{levelname}: {message}', logging.INFO: '{asctime} {levelname}: {message}' } DATE_FMT = "%Y-%m-%d %H:%M:%S" diff --git a/sima/lib/plugin.py b/sima/lib/plugin.py index ac21b2f..f846e2e 100644 --- a/sima/lib/plugin.py +++ b/sima/lib/plugin.py @@ -2,6 +2,7 @@ class Plugin(): def __init__(self, daemon): + self.log = daemon.log self.__daemon = daemon #self.history = daemon.player.history @@ -9,6 +10,12 @@ class Plugin(): def name(self): return self.__class__.__name__.lower() + def callback_player(self): + """ + Called on player changes + """ + pass + def callback_playlist(self): """ Called on playlist changes @@ -28,12 +35,6 @@ class Plugin(): """ pass - def callback_player_stop(self): - """Not returning data, - Could be use to ensure player never stops - """ - pass - def shutdown(self): pass diff --git a/sima/lib/simadb.py b/sima/lib/simadb.py new file mode 100644 index 0000000..4db498d --- /dev/null +++ b/sima/lib/simadb.py @@ -0,0 +1,730 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2009-2013 Jack Kaliko +# Copyright (c) 2009, Eric Casteleijn +# Copyright (c) 2008 Rick van Hattem +# +# This file is part of sima +# +# sima is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# sima is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with sima. If not, see . +# +# + +# DOC: +# MuscicBrainz ID: +# Artists: +# + +__DB_VERSION__ = 2 +__HIST_DURATION__ = int(7 * 24) # in hours + +import sqlite3 + +from datetime import (datetime, timedelta) +from os.path import dirname, isdir +from os import (access, W_OK, F_OK) +from shutil import copyfile + + +class SimaDBError(Exception): + """ + Exceptions. + """ + pass + + +class SimaDBAccessError(SimaDBError): + """Error on accessing DB file""" + pass + + +class SimaDBNoFile(SimaDBError): + """No DB file present""" + pass + + +class SimaDBUpgradeError(SimaDBError): + """Error on upgrade""" + pass + + +class SimaDB(object): + "SQLite management" + + def __init__(self, db_path=None): + self._db_path = db_path + self.db_path_mod_control() + + def db_path_mod_control(self): + db_path = self._db_path + # Controls directory access + if not isdir(dirname(db_path)): + raise SimaDBAccessError('Not a regular directory: "%s"' % + dirname(db_path)) + if not access(dirname(db_path), W_OK): + raise SimaDBAccessError('No write access to "%s"' % dirname(db_path)) + # Is a file but no write access + if access(db_path, F_OK) and not access(db_path, W_OK | F_OK): + raise SimaDBAccessError('No write access to "%s"' % db_path) + # No file + if not access(db_path, F_OK): + raise SimaDBNoFile('No DB file in "%s"' % db_path) + + def close_database_connection(self, connection): + """Close the database connection.""" + connection.close() + + def get_database_connection(self): + """get database reference""" + connection = sqlite3.connect( + self._db_path, timeout=5.0, isolation_level="immediate") + #connection.text_factory = str + return connection + + def upgrade(self): + """upgrade DB from previous versions""" + connection = self.get_database_connection() + try: + connection.execute('SELECT version FROM db_info') + except Exception as err: + if err.__str__() == "no such table: db_info": + # db version < 2 (MPD_sima 0.6) + copyfile(self._db_path, self._db_path + '.0.6') + connection.execute('DROP TABLE tracks') + connection.commit() + self.create_db() + else: + raise SimaDBUpgradeError('Could not upgrade database: "%s"' % + err) + self.close_database_connection(connection) + + def get_artist(self, artist_name, mbid=None, + with_connection=None, add_not=False): + """get artist information from the database. + if not in database insert new entry.""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + rows = connection.execute( + "SELECT * FROM artists WHERE name = ?", (artist_name,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + if not with_connection: + self.close_database_connection(connection) + return False + connection.execute( + "INSERT INTO artists (name, mbid) VALUES (?, ?)", + (artist_name, mbid)) + connection.commit() + rows = connection.execute( + "SELECT * FROM artists WHERE name = ?", (artist_name,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + self.close_database_connection(connection) + + def get_track(self, track, with_connection=None, add_not=False): + """ + Get a track from Tracks table, add if not existing, + Attention: use Track() object!! + if not in database insert new entry.""" + art = track.artist + nam = track.title + fil = track.get_filename() + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + art_id = self.get_artist(art, with_connection=connection)[0] + alb_id = self.get_album(track, with_connection=connection)[0] + rows = connection.execute( + "SELECT * FROM tracks WHERE name = ? AND" + " artist = ? AND file = ?", (nam, art_id, fil)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + return False + connection.execute( + "INSERT INTO tracks (artist, album, name, file) VALUES (?, ?, ?, ?)", + (art_id, alb_id, nam, fil)) + connection.commit() + rows = connection.execute( + "SELECT * FROM tracks WHERE name = ? AND" + " artist = ? AND album = ? AND file = ?", + (nam, art_id, alb_id, fil,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def get_album(self, track, mbid=None, + with_connection=None, add_not=False): + """ + get album information from the database. + if not in database insert new entry. + Attention: use Track() object!! + Use AlbumArtist tag is provided, fallback to Album tag + """ + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + if track.albumartist: + artist = track.albumartist + else: + artist = track.artist + art_id = self.get_artist(artist, with_connection=connection)[0] + album = track.album + rows = connection.execute( + "SELECT * FROM albums WHERE name = ? AND artist = ?", + (album, art_id)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + return False + connection.execute( + "INSERT INTO albums (name, artist, mbid) VALUES (?, ?, ?)", + (album, art_id, mbid)) + connection.commit() + rows = connection.execute( + "SELECT * FROM albums WHERE name = ? AND artist = ?", + (album, art_id)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + self.close_database_connection(connection) + + def get_artists(self, with_connection=None): + """Returns all artists in DB""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + rows = connection.execute("SELECT name FROM artists ORDER BY name") + results = [row for row in rows] + if not with_connection: + self.close_database_connection(connection) + for artist in results: + yield artist + + def get_bl_artist(self, artist_name, + with_connection=None, add_not=None): + """get blacklisted artist information from the database.""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + art = self.get_artist(artist_name, + with_connection=connection, add_not=add_not) + if not art: + return False + art_id = art[0] + rows = connection.execute("SELECT * FROM black_list WHERE artist = ?", + (art_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + if not with_connection: + self.close_database_connection(connection) + return False + connection.execute("INSERT INTO black_list (artist) VALUES (?)", + (art_id,)) + connection.execute("UPDATE black_list SET updated = DATETIME('now')" + " WHERE artist = ?", (art_id,)) + connection.commit() + rows = connection.execute("SELECT * FROM black_list WHERE artist = ?", + (art_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + self.close_database_connection(connection) + return False + + def get_bl_album(self, track, + with_connection=None, add_not=None): + """get blacklisted track information from the database.""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + album = self.get_album(track, + with_connection=connection, add_not=add_not) + if not album: + return False + alb_id = album[0] + rows = connection.execute("SELECT * FROM black_list WHERE album = ?", + (alb_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + if not with_connection: + self.close_database_connection(connection) + return False + connection.execute("INSERT INTO black_list (album) VALUES (?)", + (alb_id,)) + connection.execute("UPDATE black_list SET updated = DATETIME('now')" + " WHERE album = ?", (alb_id,)) + connection.commit() + rows = connection.execute("SELECT * FROM black_list WHERE album = ?", + (alb_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + self.close_database_connection(connection) + return False + + def get_bl_track(self, track, with_connection=None, add_not=None): + """get blacklisted track information from the database.""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + track = self.get_track(track, + with_connection=connection, add_not=add_not) + if not track: + return False + track_id = track[0] + rows = connection.execute("SELECT * FROM black_list WHERE track = ?", + (track_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if add_not: + if not with_connection: + self.close_database_connection(connection) + return False + connection.execute("INSERT INTO black_list (track) VALUES (?)", + (track_id,)) + connection.execute("UPDATE black_list SET updated = DATETIME('now')" + " WHERE track = ?", (track_id,)) + connection.commit() + rows = connection.execute("SELECT * FROM black_list WHERE track = ?", + (track_id,)) + for row in rows: + if not with_connection: + self.close_database_connection(connection) + return row + if not with_connection: + self.close_database_connection(connection) + return False + + def get_history(self, artist=None, artists=None, duration=__HIST_DURATION__): + """Retrieve complete play history, most recent tracks first + artist : filter history for specific artist + artists : filter history for specific artists list + """ + date = datetime.utcnow() - timedelta(hours=duration) + connection = self.get_database_connection() + if artist: + rows = connection.execute( + "SELECT arts.name, albs.name, trs.name, trs.file, hist.last_play" + " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs" + " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id" + " AND hist.last_play > ? AND arts.name = ?" + " ORDER BY hist.last_play DESC", (date.isoformat(' '), artist,)) + else: + rows = connection.execute( + "SELECT arts.name, albs.name, trs.name, trs.file" + " FROM artists AS arts, tracks AS trs, history AS hist, albums AS albs" + " WHERE trs.id = hist.track AND trs.artist = arts.id AND trs.album = albs.id" + " AND hist.last_play > ? ORDER BY hist.last_play DESC", (date.isoformat(' '),)) + for row in rows: + if artists and row[0] not in artists: + continue + yield row + self.close_database_connection(connection) + + def get_black_list(self): + """Retrieve complete black list.""" + connection = self.get_database_connection() + rows = connection.execute('SELECT black_list.rowid, artists.name' + ' FROM artists INNER JOIN black_list' + ' ON artists.id = black_list.artist') + yield ('Row ID', 'Actual black listed element', 'Extra information',) + yield ('',) + yield ('Row ID', 'Artist',) + for row in rows: + yield row + rows = connection.execute('SELECT black_list.rowid, albums.name, artists.name' + ' FROM artists, albums INNER JOIN black_list' + ' ON albums.id = black_list.album' + ' WHERE artists.id = albums.artist') + yield ('',) + yield ('Row ID', 'Album', 'Artist name') + for row in rows: + yield row + rows = connection.execute('SELECT black_list.rowid, tracks.name, artists.name' + ' FROM artists, tracks INNER JOIN black_list' + ' ON tracks.id = black_list.track' + ' WHERE tracks.artist = artists.id') + yield ('',) + yield ('Row ID', 'Title', 'Artist name') + for row in rows: + yield row + self.close_database_connection(connection) + + def _set_mbid(self, artist_id=None, mbid=None, with_connection=None): + """get artist information from the database. + if not in database insert new entry.""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + connection.execute("UPDATE artists SET mbid = ? WHERE id = ?", + (mbid, artist_id)) + connection.commit() + if not with_connection: + self.close_database_connection(connection) + + def _get_similar_artists_from_db(self, artist_id): + connection = self.get_database_connection() + results = [row for row in connection.execute( + "SELECT match, name FROM usr_artist_2_artist INNER JOIN" + " artists ON usr_artist_2_artist.artist2 = artists.id WHERE" + " usr_artist_2_artist.artist1 = ? ORDER BY match DESC;", + (artist_id,))] + self.close_database_connection(connection) + for score, artist in results: + yield {'score': score, 'artist': artist} + + def _get_reverse_similar_artists_from_db(self, artist_id): + connection = self.get_database_connection() + results = [row for row in connection.execute( + "SELECT name FROM usr_artist_2_artist INNER JOIN" + " artists ON usr_artist_2_artist.artist1 = artists.id WHERE" + " usr_artist_2_artist.artist2 = ?;", + (artist_id,))] + self.close_database_connection(connection) + for artist in results: + yield artist[0] + + def get_similar_artists(self, artist_name): + """get similar artists from the database sorted by descending + match score""" + artist_id = self.get_artist(artist_name)[0] + for result in self._get_similar_artists_from_db(artist_id): + yield result + + def _get_artist_match(self, artist1, artist2, with_connection=None): + """get artist match score from database""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + rows = connection.execute( + "SELECT match FROM usr_artist_2_artist WHERE artist1 = ?" + " AND artist2 = ?", + (artist1, artist2)) + result = 0 + for row in rows: + result = row[0] + break + if not with_connection: + self.close_database_connection(connection) + return result + + def _remove_relation_between_2_artist(self, artist1, artist2): + """Remove a similarity relation""" + connection = self.get_database_connection() + connection.execute( + 'DELETE FROM usr_artist_2_artist' + ' WHERE artist1 = ? AND artist2 = ?;', + (artist1, artist2)) + self.clean_database(with_connection=connection) + self._update_artist(artist_id=artist1, with_connection=connection) + connection.commit() + self.close_database_connection(connection) + + def _remove_artist(self, artist_id, deep=False, with_connection=None): + """Remove all artist1 reference""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + if deep: + connection.execute( + 'DELETE FROM usr_artist_2_artist' + ' WHERE artist1 = ? OR artist2 = ?;', + (artist_id, artist_id)) + connection.execute( + 'DELETE FROM artists WHERE id = ?;', + (artist_id,)) + else: + connection.execute( + 'DELETE FROM usr_artist_2_artist WHERE artist1 = ?;', + (artist_id,)) + self.clean_database(with_connection=connection) + self._update_artist(artist_id=artist_id, with_connection=connection) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def _remove_bl(self, rowid): + """Remove bl row id""" + connection = self.get_database_connection() + connection.execute('DELETE FROM black_list' + ' WHERE black_list.rowid = ?', (rowid,)) + connection.commit() + self.close_database_connection(connection) + + def _insert_artist_match( + self, artist1, artist2, match, with_connection=None): + """write match score to the database. + Does not update time stamp in table artist/*_updated""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + connection.execute( + "INSERT INTO usr_artist_2_artist (artist1, artist2, match) VALUES" + " (?, ?, ?)", + (artist1, artist2, match)) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def add_history(self, track): + """Add to history""" + connection = self.get_database_connection() + track_id = self.get_track(track, with_connection=connection)[0] + rows = connection.execute("SELECT * FROM history WHERE track = ? ", (track_id,)) + if not rows.fetchone(): + connection.execute("INSERT INTO history (track) VALUES (?)", (track_id,)) + connection.execute("UPDATE history SET last_play = DATETIME('now') " + " WHERE track = ?", (track_id,)) + connection.commit() + self.close_database_connection(connection) + + def _update_artist(self, artist_id, with_connection=None): + """write artist information to the database""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + connection.execute( + "UPDATE artists SET usr_updated = DATETIME('now') WHERE id = ?", + (artist_id,)) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def _update_artist_match( + self, artist1, artist2, match, with_connection=None): + """write match score to the database""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + connection.execute( + "UPDATE usr_artist_2_artist SET match = ? WHERE artist1 = ? AND" + " artist2 = ?", + (match, artist1, artist2)) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def _update_similar_artists(self, artist, similar_artists): + """write user similar artist information to the database + """ + # DOC: similar_artists = list([{'score': match, 'artist': name}]) + # + connection = self.get_database_connection() + artist_id = self.get_artist(artist, with_connection=connection)[0] + for artist in similar_artists: + id2 = self.get_artist( + artist['artist'], with_connection=connection)[0] + if self._get_artist_match( + artist_id, id2, with_connection=connection): + self._update_artist_match( + artist_id, id2, artist['score'], + with_connection=connection) + continue + self._insert_artist_match( + artist_id, id2, artist['score'], + with_connection=connection) + self._update_artist(artist_id, with_connection=connection) + connection.commit() + self.close_database_connection(connection) + + def _clean_artists_table(self, with_connection=None): + """Clean orphan artists""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + artists_ids = set([row[0] for row in connection.execute( + "SELECT id FROM artists")]) + artist_2_artist_ids = set([row[0] for row in connection.execute( + "SELECT artist1 FROM usr_artist_2_artist")] + + [row[0] for row in connection.execute( + "SELECT artist2 FROM usr_artist_2_artist")] + + [row[0] for row in connection.execute( + "SELECT artist FROM black_list")] + + [row[0] for row in connection.execute( + "SELECT artist FROM albums")] + + [row[0] for row in connection.execute( + "SELECT artist FROM tracks")]) + orphans = [ (orphan,) for orphan in artists_ids - artist_2_artist_ids ] + connection.executemany('DELETE FROM artists WHERE id = (?);', orphans) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def _clean_albums_table(self, with_connection=None): + """Clean orphan albums""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + orphan_black_ids = set([row[0] for row in connection.execute( + """SELECT albums.id FROM albums + LEFT JOIN black_list ON albums.id = black_list.album + WHERE ( black_list.album IS NULL )""")]) + orphan_tracks_ids = set([row[0] for row in connection.execute( + """SELECT albums.id FROM albums + LEFT JOIN tracks ON albums.id = tracks.album + WHERE tracks.album IS NULL""")]) + orphans = [ (orphan,) for orphan in orphan_black_ids & orphan_tracks_ids ] + connection.executemany('DELETE FROM albums WHERE id = (?);', orphans) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def _clean_tracks_table(self, with_connection=None): + """Clean orphan tracks""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + hist_orphan_ids = set([row[0] for row in connection.execute( + """SELECT tracks.id FROM tracks + LEFT JOIN history ON tracks.id = history.track + WHERE history.track IS NULL""")]) + black_list_orphan_ids = set([row[0] for row in connection.execute( + """SELECT tracks.id FROM tracks + LEFT JOIN black_list ON tracks.id = black_list.track + WHERE black_list.track IS NULL""")]) + orphans = [ (orphan,) for orphan in hist_orphan_ids & black_list_orphan_ids ] + connection.executemany('DELETE FROM tracks WHERE id = (?);', orphans) + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def clean_database(self, with_connection=None): + """Wrapper around _clean_* methods""" + if with_connection: + connection = with_connection + else: + connection = self.get_database_connection() + self._clean_tracks_table(with_connection=connection) + self._clean_albums_table(with_connection=connection) + self._clean_artists_table(with_connection=connection) + connection.execute("VACUUM") + if not with_connection: + connection.commit() + self.close_database_connection(connection) + + def purge_history(self, duration=__HIST_DURATION__): + """Remove old entries in history""" + connection = self.get_database_connection() + connection.execute("DELETE FROM history WHERE last_play" + " < datetime('now', '-%i hours')" % duration) + connection.commit() + self.close_database_connection(connection) + + def _set_dbversion(self): + connection = self.get_database_connection() + connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)', + (__DB_VERSION__, 'Sima DB')) + connection.commit() + self.close_database_connection(connection) + + def create_db(self): + """ Set up a database for the artist similarity scores + """ + connection = self.get_database_connection() + connection.execute( + 'CREATE TABLE IF NOT EXISTS db_info' + ' (version INTEGER, name CHAR(36))') + connection.execute( + 'CREATE TABLE IF NOT EXISTS artists (id INTEGER PRIMARY KEY, name' + ' VARCHAR(100), mbid CHAR(36), lfm_updated DATE, usr_updated DATE)') + connection.execute( + 'CREATE TABLE IF NOT EXISTS usr_artist_2_artist (artist1 INTEGER,' + ' artist2 INTEGER, match INTEGER)') + connection.execute( + 'CREATE TABLE IF NOT EXISTS lfm_artist_2_artist (artist1 INTEGER,' + ' artist2 INTEGER, match INTEGER)') + connection.execute( + 'CREATE TABLE IF NOT EXISTS albums (id INTEGER PRIMARY KEY,' + ' artist INTEGER, name VARCHAR(100), mbid CHAR(36))') + connection.execute( + 'CREATE TABLE IF NOT EXISTS tracks (id INTEGER PRIMARY KEY,' + ' name VARCHAR(100), artist INTEGER, album INTEGER,' + ' file VARCHAR(500), mbid CHAR(36))') + connection.execute( + 'CREATE TABLE IF NOT EXISTS black_list (artist INTEGER,' + ' album INTEGER, track INTEGER, updated DATE)') + connection.execute( + 'CREATE TABLE IF NOT EXISTS history (last_play DATE,' + ' track integer)') + connection.execute( + "CREATE INDEX IF NOT EXISTS a2aa1x ON usr_artist_2_artist (artist1)") + connection.execute( + "CREATE INDEX IF NOT EXISTS a2aa2x ON usr_artist_2_artist (artist2)") + connection.execute( + "CREATE INDEX IF NOT EXISTS lfma2aa1x ON lfm_artist_2_artist (artist1)") + connection.execute( + "CREATE INDEX IF NOT EXISTS lfma2aa2x ON lfm_artist_2_artist (artist2)") + connection.commit() + self.close_database_connection(connection) + self._set_dbversion() + + +def main(): + db = SimaDB(db_path='/tmp/sima.db') + db.purge_history(int(4)) + db.clean_database() + + +# Script starts here +if __name__ == '__main__': + main() + +# VIM MODLINE +# vim: ai ts=4 sw=4 sts=4 expandtab diff --git a/sima/plugins/addhist.py b/sima/plugins/addhist.py new file mode 100644 index 0000000..8b8da7a --- /dev/null +++ b/sima/plugins/addhist.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""Add playing tracks to history +""" + +# standart library import +#from select import select + +# third parties componants + +# local import +from ..lib.plugin import Plugin + +class History(Plugin): + """ + History + """ + def __init__(self, daemon): + Plugin.__init__(self, daemon) + self.sdb = daemon.sdb + self.player = daemon.player + + def shutdown(self): + self.log.info('Cleaning database') + self.sdb.purge_history() + self.sdb.clean_database() + + def callback_next_song(self): + current = self.player.current + self.log.debug('add history: "{}"'.format(current)) + self.sdb.add_history(current) + + +# VIM MODLINE +# vim: ai ts=4 sw=4 sts=4 expandtab diff --git a/sima/plugins/crop.py b/sima/plugins/crop.py index b75f0d9..8467ba4 100644 --- a/sima/plugins/crop.py +++ b/sima/plugins/crop.py @@ -19,6 +19,7 @@ class Crop(Plugin): player = self._Plugin__daemon.player target_lengh = 10 while player.currentsong().pos > target_lengh: + self.log.debug('cropping playlist') player.remove() diff --git a/sima/plugins/lastfm.py b/sima/plugins/lastfm.py new file mode 100644 index 0000000..43a1ae5 --- /dev/null +++ b/sima/plugins/lastfm.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +# standart library import +#from select import select + +# third parties componants + +# local import +from ..lib.plugin import Plugin + +class Last(Plugin): + """last.fm similar artists + """ + pass + + +# VIM MODLINE +# vim: ai ts=4 sw=4 sts=4 expandtab diff --git a/sima/utils/config.py b/sima/utils/config.py index 84367d6..1dd9567 100644 --- a/sima/utils/config.py +++ b/sima/utils/config.py @@ -36,8 +36,8 @@ from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG) from . import utils # DEFAULTS -DIRNAME = 'mpd_sima' -CONF_FILE = 'mpd_sima.cfg' +DIRNAME = 'sima' +CONF_FILE = 'sima.cfg' DEFAULT_CONF = { 'MPD': { @@ -64,7 +64,7 @@ DEFAULT_CONF = { # -class ConfMan(object):#CONFIG MANAGER CLASS +class ConfMan(object): # CONFIG MANAGER CLASS """ Configuration manager. Default configuration is stored in DEFAULT_CONF dictionnary. @@ -89,7 +89,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS self.defaults = dict(DEFAULT_CONF) self.startopt = options ## Sima sqlite DB - self.userdb_file = None + self.db_file = None self.log = logger ## INIT CALLS @@ -117,7 +117,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS Controls conf file permissions. """ mode = S_IMODE(stat(self.conf_file)[ST_MODE]) - self.log.debug('file permision is: %o' % mode) + self.log.debug('file permission is: %o' % mode) if mode & S_IRWXO or mode & S_IRWXG: self.log.warning('File is readable by "other" and/or' + ' "group" (actual permission %o octal).' % @@ -184,7 +184,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS def init_config(self): """ Use XDG directory standard if exists - else use "HOME/(.config|.local/share)/mpd_sima/" + else use "HOME/(.config|.local/share)/sima/" http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html """ @@ -223,7 +223,7 @@ class ConfMan(object):#CONFIG MANAGER CLASS self.log.error('Please use "--config" to locate the conf file') sys.exit(1) - self.userdb_file = join(data_dir, 'sima.db') + self.db_file = join(data_dir, 'sima.db') config = configparser.SafeConfigParser() -- 2.39.5