"""Core Object dealing with plugins and player client
"""
-import sys
import time
from collections import deque
return PlayerClient(host, port, pswd)
def add_history(self):
+ """Handle local short history"""
self.short_history.appendleft(self.player.current)
def register_plugin(self, plugin_class):
getattr(plugin, method)(*args, **kwds)
def need_tracks(self):
+ """Is the player in need for tracks"""
if not self.enabled:
self.log.debug('Queueing disabled!')
return False
try:
mod_obj = __import__(module, fromlist=[plugin])
except ImportError as err:
- logger.error('Failed to load plugin\'s module: {0} ({1})'.format(module, err))
+ logger.error('Failed to load plugin\'s module: ' +
+ '{0} ({1})'.format(module, err))
sima.shutdown()
sys.exit(1)
try:
logger.error('Failed to load plugin {0} ({1})'.format(plugin, err))
sima.shutdown()
sys.exit(1)
- logger.info('Loading {0} plugin: {name} ({doc})'.format(source, **plugin_obj.info()))
+ logger.info('Loading {0} plugin: {name} ({doc})'.format(
+ source, **plugin_obj.info()))
sima.register_plugin(plugin_obj)
# pylint: disable=broad-except
try:
start(sopt, restart)
- except SigHup as err: # SigHup inherit from Exception
+ except SigHup: # SigHup inherit from Exception
run(sopt, True)
except Exception: # Unhandled exception
exception_log()
# Script starts here
def main():
+ """Entry point"""
nfo = dict({'version': info.__version__,
'prog': 'sima'})
# StartOpt gathers options from command line call (in StartOpt().options)
# -*- coding: utf-8 -*-
-
# Public Domain
#
# Copyright 2007, 2009 Sander Marechal <s.marechal@jejik.com>
for details (ISBN 0201563177)
Short explanation:
- Unix processes belong to "process group" which in turn lies within a "session".
- A session can have a controlling tty.
+ Unix processes belong to "process group" which in turn lies within a
+ "session". A session can have a controlling tty.
Forking twice allows to detach the session from a possible tty.
The process lives then within the init process.
"""
# -*- coding: utf-8 -*-
-
# Copyright (c) 2009, 2010, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
#
# This file is part of sima
LOG_FORMATS = {
- logging.DEBUG: '{asctime} {filename: >11}:{lineno: <3} {levelname: <7}: {message}',
+ logging.DEBUG: '{asctime} {filename: >11}:{lineno: <3} {levelname: <7}: {message}',
logging.INFO: '{asctime} {levelname: <7}: {message}',
#logging.DEBUG: '{asctime} {filename}:{lineno}({funcName}) '
#'{levelname}: {message}',
# -*- coding: utf-8 -*-
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko@azylum.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+#
+"""
+Defines some object to handle audio file metadata
+"""
from .simastr import SimaStr
from .track import Track
class MetaException(Exception):
+ """Generic Meta Exception"""
pass
class NotSameArtist(MetaException):
class Meta:
+ """Generic Class for Meta object"""
def __init__(self, **kwargs):
self.name = None
class Player(object):
-
"""Player interface to inherit from.
When querying player music library for tracks, Player instance *must* return
# -*- coding: utf-8 -*-
-
-class Plugin():
+# Copyright (c) 2013, 2014 Jack Kaliko <kaliko@azylum.org>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+#
+"""
+Plugin object to derive from
+"""
+
+class Plugin:
"""
First non-empty line of the docstring is used as description
Rest of the docstring at your convenience.
pass
def shutdown(self):
+ """Called on application shutdown"""
pass
# -*- coding: utf-8 -*-
-
+#
# Copyright (c) 2009-2013 Jack Kaliko <jack@azylum.org>
# Copyright (c) 2009, Eric Casteleijn <thisfred@gmail.com>
# Copyright (c) 2008 Rick van Hattem
# along with sima. If not, see <http://www.gnu.org/licenses/>.
#
#
+"""SQlite database library
+"""
# DOC:
# MuscicBrainz ID: <http://musicbrainz.org/doc/MusicBrainzIdentifier>
# <http://musicbrainz.org/doc/Same_Artist_With_Different_Names>
__DB_VERSION__ = 2
-__HIST_DURATION__ = int(7 * 24) # in hours
+__HIST_DURATION__ = int(30 * 24) # in hours
import sqlite3
self.db_path_mod_control()
def db_path_mod_control(self):
+ """Controls DB path access & write permissions"""
db_path = self._db_path
# Controls directory access
if not isdir(dirname(db_path)):
"""Retrieve complete play history, most recent tracks first
artist : filter history for specific artist
artists : filter history for specific artists list
- """
+ """ # pylint: disable=C0301
date = datetime.utcnow() - timedelta(hours=duration)
connection = self.get_database_connection()
if artist:
yield ('Row ID', 'Artist',)
for row in rows:
yield row
- rows = connection.execute('SELECT black_list.rowid, albums.name, artists.name'
+ 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 ('Row ID', 'Album', 'Artist name')
for row in rows:
yield row
- rows = connection.execute('SELECT black_list.rowid, tracks.name, artists.name'
+ 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')
"""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,))
+ 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("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 _set_dbversion(self):
+ """Add db version"""
connection = self.get_database_connection()
connection.execute('INSERT INTO db_info (version, name) VALUES (?, ?)',
(__DB_VERSION__, 'Sima DB'))
'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)")
+ "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)")
+ "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)")
+ "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)")
+ "CREATE INDEX IF NOT EXISTS lfma2aa2x ON lfm_artist_2_artist (artist2)")
connection.commit()
self.close_database_connection(connection)
self._set_dbversion()
__author__ = 'Jack Kaliko'
-import logging
-
from datetime import datetime, timedelta
-from time import sleep
from requests import get, Request, Timeout, ConnectionError
SOCKET_TIMEOUT = 4
-class SimaEch():
- """
+class SimaEch:
+ """EchoNest http client
"""
root_url = 'http://{host}/api/{version}'.format(**ECH)
cache = {}
raise WSError(status.get('message'))
def _forge_payload(self, artist):
- """
+ """Build payload
"""
payload = {'api_key': ECH.get('apikey')}
if not isinstance(artist, Artist):
payload.update(
id='musicbrainz:artist:{0}'.format(artist.mbid))
else:
- payload.update(name=artist.name)
+ payload.update(name=artist.name)
payload.update(bucket='id:musicbrainz')
payload.update(results=100)
return payload
def get_similar(self, artist=None):
- """
+ """Fetch similar artists
"""
payload = self._forge_payload(artist)
# Construct URL
#
"""
-Consume EchoNest web service
+Consume Last.fm web service
"""
__version__ = '0.5.0'
SOCKET_TIMEOUT = 6
-class SimaFM():
- """
+class SimaFM:
+ """Last.fm http client
"""
root_url = 'http://{host}/{version}/'.format(**LFM)
cache = {}
return True
def _forge_payload(self, artist, method='similar', track=None):
- """
+ """Build payload
"""
payloads = dict({'similar': {'method':'artist.getsimilar',},
'top': {'method':'artist.gettoptracks',},
if artist.mbid:
payload.update(mbid='{0}'.format(artist.mbid))
else:
- payload.update(artist=artist.name,
- autocorrect=1)
+ payload.update(artist=artist.name,
+ autocorrect=1)
payload.update(results=100)
if method == 'track':
payload.update(track=track)
return payload
def get_similar(self, artist=None):
- """
+ """Fetch similar artists
"""
payload = self._forge_payload(artist)
# Construct URL
# -*- coding: utf-8 -*-
-
#
# Copyright (c) 2009, 2010, 2013 Jack Kaliko <kaliko@azylum.org>
#
# If not, see <http://www.gnu.org/licenses/>.
#
-"""
+r"""
SimaStr
Special unicode() subclass to perform fuzzy match on specific strings with
# IMPORTS
import unicodedata
-from re import (compile, U, I)
+from re import compile as re_compile, U, I
from ..utils.leven import levenshtein_ratio
# Trailing patterns: ! ? live
# TODO: add "concert" key word
# add "Live at <somewhere>"
- regexp_dict.update({'trail': '([- !?\.]|\(? ?[Ll]ive ?\)?)'})
+ regexp_dict.update({'trail': r'([- !?\.]|\(? ?[Ll]ive ?\)?)'})
- reg_lead = compile('^(?P<lead>%(lead)s )(?P<root0>.*)$' % regexp_dict, I | U)
- reg_midl = compile('^(?P<root0>.*)(?P<mid> %(mid)s )(?P<root1>.*)' % regexp_dict, U)
- reg_trail = compile('^(?P<root0>.*?)(?P<trail>%(trail)s+$)' % regexp_dict, U)
+ reg_lead = re_compile('^(?P<lead>%(lead)s )(?P<root0>.*)$' % regexp_dict, I | U)
+ reg_midl = re_compile('^(?P<root0>.*)(?P<mid> %(mid)s )(?P<root1>.*)' % regexp_dict, U)
+ reg_trail = re_compile('^(?P<root0>.*?)(?P<trail>%(trail)s+$)' % regexp_dict, U)
def __init__(self, fuzzstr):
"""
# fuzzy computation
self._get_root()
if self.__class__.diafilter:
- self.remove_diacritics()
+ self.remove_diacritics()
def __new__(cls, fuzzstr):
return super(SimaStr, cls).__new__(cls, fuzzstr)
self.stripped = sea.group('root0')
def remove_diacritics(self):
+ """converting diacritics"""
self.stripped = ''.join(x for x in
unicodedata.normalize('NFKD', self.stripped)
if unicodedata.category(x) != 'Mn')
import time
-class Track(object):
+class Track:
"""
Track object.
Instanciate with Player replies.
self._file = file
if not kwargs:
self._empty = True
- self.time = time
+ self._time = time
self.__dict__.update(**kwargs)
self.tags_to_collapse = ['artist', 'album', 'title', 'date',
'genre', 'albumartist']
fmt = '%M:%S'
return time.strftime(fmt, temps)
-
-def main():
- pass
-
-# Script starts here
-if __name__ == '__main__':
- main()
-
# VIM MODLINE
# vim: ai ts=4 sw=4 sts=4 expandtab
# -*- coding: utf-8 -*-
-# Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
+# Copyright (c) 2009-2014 Jack Kaliko <kaliko@azylum.org>
#
# This file is part of sima
#
self.log.info('EXTRA ARTS: {}'.format(
'/'.join([trk.artist for trk in extra_arts])))
for artist in extra_arts:
- self.log.debug('Looking for artist similar to "{0.artist}" as well'.format(artist))
+ self.log.debug(
+ 'Looking for artist similar to "{0.artist}" as well'.format(
+ artist))
similar = self.lfm_similar_artists(artist=artist)
if not similar:
return ret_extra
# str conversion while Album type is not propagated
albums = [ str(album) for album in albums]
if albums:
- self.log.debug('Albums candidate: {0:s}'.format(' / '.join(albums)))
+ self.log.debug('Albums candidate: {0:s}'.format(
+ ' / '.join(albums)))
else: continue
# albums yet in history for this artist
albums = set(albums)
# local import
from ...lib.plugin import Plugin
-from ...lib.track import Track
class RandomFallBack(Plugin):
return trks
def get_trk(self):
+ """Get a single track acording to random flavour
+ """
artists = list(self.player.artists)
if self.mode == 'sensitive':
played_art = self.get_played_artist()
self.log.debug('[%s] present in conf file' % section)
for option in self.defaults[section]:
if self.config.has_option(section, option):
- #self.log.debug(u'option "%s" set to "%s" in conf. file' %
- # (option, self.config.get(section, option)))
+ #self.log.debug('option "%s" set to "%s" in conf. file'%
+ # (option, self.config.get(section, option)))
pass
else:
self.log.debug(
# You should have received a copy of the GNU General Public License
# along with sima. If not, see <http://www.gnu.org/licenses/>.
#
-#
+"""Computes levenshtein distance/ratio"""
def levenshtein(a_st, b_st):
"""Computes the Levenshtein distance between two strings."""
#
#
-import sys
from argparse import (ArgumentParser, SUPPRESS)
-from .utils import Obsolete, Wfile, Rfile, Wdir
+from .utils import Wfile, Rfile, Wdir
USAGE = """USAGE: %prog [--help] [options]"""
DESCRIPTION = """
"""
def __init__(self, script_info,):
+ self.parser = None
self.info = dict(script_info)
self.options = dict()
self.main()
#
"""generic tools and utilities for sima
"""
+# pylint: disable=C0111
import traceback
import sys
sys.exit(1)
def purge_cache(obj, age=4):
+ """purge old entries in http client cache
+ """
now = datetime.utcnow()
if now.hour == obj.timestamp.hour:
return
class SigHup(Exception):
+ """SIGHUP raises this Exception"""
pass
# ArgParse Callbacks
if not access(self._file, W_OK):
self.parser.error('no write access to "{0}"'.format(self._file))
-class Throttle():
+class Throttle:
+ """throttle decorator"""
def __init__(self, wait):
self.wait = wait
self.last_called = datetime.now()
return result
return wrapper
-class Cache():
+class Cache:
+ """Plain cache object"""
def __init__(self, elem, last=None):
self.elem = elem
self.requestdate = last