[SCM] mpd-sima/upstream: Imported Upstream version 0.12.2
kaliko-guest at users.alioth.debian.org
kaliko-guest at users.alioth.debian.org
Sun Sep 28 11:34:29 UTC 2014
The following commit has been merged in the upstream branch:
commit 10ae44d50e8e701468e495cd32efec5eaf73c03f
Author: Geoffroy Youri Berret <kaliko at azylum.org>
Date: Thu Jul 3 18:22:24 2014 +0200
Imported Upstream version 0.12.2
diff --git a/data/man/info.xml b/data/man/info.xml
index 96bffe2..437a753 100644
--- a/data/man/info.xml
+++ b/data/man/info.xml
@@ -5,7 +5,7 @@
<!ENTITY dhemail "kaliko at azylum.org">
<!ENTITY dhusername "&dhfirstname; &dhsurname;">
<!ENTITY dhpackage "mpd-sima">
- <!ENTITY release "0.12.1">
+ <!ENTITY release "0.12.2">
<!-- TITLE should be something like "User commands" or similar (see
http://www.tldp.org/HOWTO/Man-Page/q2.html). -->
<!ENTITY dhtitle "&dhpackage; &release; User Manual">
diff --git a/doc/Changelog b/doc/Changelog
index 199d707..02dee06 100644
--- a/doc/Changelog
+++ b/doc/Changelog
@@ -1,3 +1,12 @@
+MPD_sima v0.12.2
+
+ * Add some randomness to track selection
+ * Do not queue artists already queued (regression)
+ * Some refactoring in http client
+
+-- kaliko jack <kaliko at azylum.org> Tue, 01 Jul 2014 21:47:56 +0200
+
+
MPD_sima v0.12.1
* Fixed SIGHUP error, need python-musicpd >= 0.4.1
@@ -7,6 +16,7 @@ MPD_sima v0.12.1
-- kaliko jack <kaliko at azylum.org> Sat, 21 Jun 2014 14:02:17 +0200
+
MPD_sima v0.12.0
* Major refactoring
diff --git a/setup.py b/setup.py
index 036f85e..acae874 100755
--- a/setup.py
+++ b/setup.py
@@ -47,6 +47,7 @@ setup(name='MPD_sima',
entry_points={
'console_scripts': ['mpd-sima = sima.launch:main',]
},
+ test_suite="tests",
)
# VIM MODLINE
diff --git a/sima/__init__.py b/sima/__init__.py
index 267899c..8d12a6f 100644
--- a/sima/__init__.py
+++ b/sima/__init__.py
@@ -13,7 +13,7 @@ ECH = {'apikey': 'WlRKQkhTS0JHWFVDUEZZRFA',
'version': 'v4',
}
-WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
+WAIT_BETWEEN_REQUESTS = timedelta(days=0, seconds=2)
SOCKET_TIMEOUT = 6
# vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/info.py b/sima/info.py
index c416735..3fae78c 100644
--- a/sima/info.py
+++ b/sima/info.py
@@ -11,7 +11,7 @@ queue is getting short.
"""
-__version__ = '0.12.1'
+__version__ = '0.12.2'
__author__ = 'kaliko jack'
__email__ = 'kaliko at azylum.org'
__url__ = 'git://git.kaliko.me/sima.git'
diff --git a/sima/lib/http.py b/sima/lib/http.py
index c07ad38..0c1b396 100644
--- a/sima/lib/http.py
+++ b/sima/lib/http.py
@@ -26,6 +26,10 @@ import time
import email.utils
+from requests import Session, Request, Timeout, ConnectionError
+
+from sima import SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
+from sima.utils.utils import WSError, WSTimeout, WSHTTPError, Throttle
from .cache import DictCache
@@ -88,11 +92,11 @@ class CacheController(object):
retval = dict(parts_with_args + parts_wo_args)
return retval
- def cached_request(self, url, headers):
+ def cached_request(self, request):
"""Return the cached resquest if available and fresh
"""
- cache_url = self.cache_url(url)
- cc = self.parse_cache_control(headers)
+ cache_url = self.cache_url(request.url)
+ cc = self.parse_cache_control(request.headers)
# non-caching states
no_cache = True if 'no-cache' in cc else False
@@ -125,7 +129,7 @@ class CacheController(object):
for header in varied_headers:
# If our headers don't match for the headers listed in
# the vary header, then don't use the cached response
- if headers.get(header, None) != original_headers.get(header):
+ if request.headers.get(header, None) != original_headers.get(header):
return False
now = time.time()
@@ -178,10 +182,10 @@ class CacheController(object):
self.cache.delete(cache_url)
if 'etag' in resp.headers:
- headers['If-None-Match'] = resp.headers['ETag']
+ request.headers['If-None-Match'] = resp.headers['ETag']
if 'last-modified' in resp.headers:
- headers['If-Modified-Since'] = resp.headers['Last-Modified']
+ request.headers['If-Modified-Since'] = resp.headers['Last-Modified']
# return the original handler
return False
@@ -265,3 +269,50 @@ class CacheController(object):
resp.from_cache = True
return resp
+
+
+class HttpClient:
+ def __init__(self, cache=None, stats=None):
+ """
+ Prepare http request
+ Use cached elements or proceed http request
+ """
+ self.stats = stats
+ self.controller = CacheController(cache)
+
+ def __call__(self, ress, payload):
+ req = Request('GET', ress, params=payload,).prepare()
+ if self.stats:
+ self.stats.update(total=self.stats.get('total')+1)
+ cached_response = self.controller.cached_request(req)
+ if cached_response:
+ if self.stats:
+ self.stats.update(ccontrol=self.stats.get('ccontrol')+1)
+ return cached_response
+ try:
+ return self.fetch_ws(req)
+ except Timeout:
+ raise WSTimeout('Failed to reach server within {0}s'.format(
+ SOCKET_TIMEOUT))
+ except ConnectionError as err:
+ raise WSError(err)
+
+ @Throttle(WAIT_BETWEEN_REQUESTS)
+ def fetch_ws(self, prepreq):
+ """fetch from web service"""
+ sess = Session()
+ resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
+ if resp.status_code == 304:
+ self.stats.update(etag=self.stats.get('etag')+1)
+ resp = self.controller.update_cached_response(prepreq, resp)
+ elif resp.status_code != 200:
+ raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
+ ratelimit = resp.headers.get('x-ratelimit-remaining', None)
+ if ratelimit and self.stats:
+ minrl = min(int(ratelimit), self.stats.get('minrl'))
+ self.stats.update(minrl=minrl)
+ self.controller.cache_response(resp.request, resp)
+ return resp
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
diff --git a/sima/lib/meta.py b/sima/lib/meta.py
index a3efbf5..949b556 100644
--- a/sima/lib/meta.py
+++ b/sima/lib/meta.py
@@ -64,6 +64,8 @@ class Meta:
else:
return id(self)
+ def __bool__(self): # empty name not possible for a valid obj
+ return bool(self.name)
class Album(Meta):
__hash__ = Meta.__hash__
@@ -89,15 +91,15 @@ class Album(Meta):
class Artist(Meta):
def __init__(self, **kwargs):
- self._aliases = []
+ self._aliases = set()
super().__init__(**kwargs)
def append(self, name):
- self._aliases.append(name)
+ self._aliases.update({name,})
@property
def names(self):
- return self._aliases + [self.name]
+ return self._aliases | {self.name,}
def __add__(self, other):
if isinstance(other, Artist):
@@ -109,4 +111,3 @@ class Artist(Meta):
raise NotSameArtist('different mbids: {0} and {1}'.format(self, other))
# vim: ai ts=4 sw=4 sts=4 expandtab
-
diff --git a/sima/lib/simaecho.py b/sima/lib/simaecho.py
index e5fe485..3f649a6 100644
--- a/sima/lib/simaecho.py
+++ b/sima/lib/simaecho.py
@@ -21,18 +21,17 @@
Consume EchoNest web service
"""
-__version__ = '0.0.2'
+__version__ = '0.0.3'
__author__ = 'Jack Kaliko'
-from requests import Session, Request, Timeout, ConnectionError
-from sima import ECH, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
+from sima import ECH
from sima.lib.meta import Artist
from sima.lib.track import Track
-from sima.lib.http import CacheController
-from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
-from sima.utils.utils import getws, Throttle
+from sima.lib.http import HttpClient
+from sima.utils.utils import WSError, WSNotFound
+from sima.utils.utils import getws
if len(ECH.get('apikey')) == 23: # simple hack allowing imp.reload
getws(ECH)
@@ -45,52 +44,12 @@ class SimaEch:
name = 'EchoNest'
cache = False
stats = {'etag':0,
- 'ccontrol':0,
- 'minrl':120,
- 'total':0}
+ 'ccontrol':0,
+ 'minrl':120,
+ 'total':0}
def __init__(self):
- self.controller = CacheController(self.cache)
-
- def _fetch(self, ressource, payload):
- """
- Prepare http request
- Use cached elements or proceed http request
- """
- req = Request('GET', ressource, params=payload,
- ).prepare()
- SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
- if self.cache:
- cached_response = self.controller.cached_request(req.url, req.headers)
- if cached_response:
- SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
- return cached_response.json()
- try:
- return self._fetch_ws(req)
- except Timeout:
- raise WSTimeout('Failed to reach server within {0}s'.format(
- SOCKET_TIMEOUT))
- except ConnectionError as err:
- raise WSError(err)
-
- @Throttle(WAIT_BETWEEN_REQUESTS)
- def _fetch_ws(self, prepreq):
- """fetch from web service"""
- sess = Session()
- resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
- if resp.status_code == 304:
- SimaEch.stats.update(etag=SimaEch.stats.get('etag')+1)
- resp = self.controller.update_cached_response(prepreq, resp)
- elif resp.status_code != 200:
- raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
- ans = resp.json()
- self._controls_answer(ans)
- SimaEch.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
- minrl = min(int(SimaEch.ratelimit), SimaEch.stats.get('minrl'))
- SimaEch.stats.update(minrl=minrl)
- if self.cache:
- self.controller.cache_response(resp.request, resp)
- return ans
+ self.http = HttpClient(cache=self.cache, stats=self.stats)
def _controls_answer(self, ans):
"""Controls answer.
@@ -135,8 +94,9 @@ class SimaEch:
payload = self._forge_payload(artist)
# Construct URL
ressource = '{0}/artist/similar'.format(SimaEch.root_url)
- ans = self._fetch(ressource, payload)
- for art in ans.get('response').get('artists'):
+ ans = self.http(ressource, payload)
+ self._controls_answer(ans.json())
+ for art in ans.json().get('response').get('artists'):
mbid = None
if 'foreign_ids' in art:
for frgnid in art.get('foreign_ids'):
@@ -151,13 +111,14 @@ class SimaEch:
payload = self._forge_payload(artist, top=True)
# Construct URL
ressource = '{0}/song/search'.format(SimaEch.root_url)
- ans = self._fetch(ressource, payload)
+ ans = self.http(ressource, payload)
+ self._controls_answer(ans.json())
titles = list()
art = {
'artist': artist.name,
'musicbrainz_artistid': artist.mbid,
}
- for song in ans.get('response').get('songs'):
+ for song in ans.json().get('response').get('songs'):
title = song.get('title')
if title not in titles:
titles.append(title)
diff --git a/sima/lib/simafm.py b/sima/lib/simafm.py
index 0988266..236efe7 100644
--- a/sima/lib/simafm.py
+++ b/sima/lib/simafm.py
@@ -26,15 +26,13 @@ __author__ = 'Jack Kaliko'
-from requests import Session, Request, Timeout, ConnectionError
-
-from sima import LFM, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
+from sima import LFM
from sima.lib.meta import Artist
from sima.lib.track import Track
-from sima.lib.http import CacheController
-from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
-from sima.utils.utils import getws, Throttle
+from sima.lib.http import HttpClient
+from sima.utils.utils import WSError, WSNotFound
+from sima.utils.utils import getws
if len(LFM.get('apikey')) == 43: # simple hack allowing imp.reload
getws(LFM)
@@ -51,46 +49,9 @@ class SimaFM:
'total':0}
def __init__(self):
- self.controller = CacheController(self.cache)
+ self.http = HttpClient(cache=self.cache, stats=self.stats)
self.artist = None
- def _fetch(self, payload):
- """
- Prepare http request
- Use cached elements or proceed http request
- """
- req = Request('GET', SimaFM.root_url, params=payload,
- ).prepare()
- SimaFM.stats.update(total=SimaFM.stats.get('total')+1)
- if self.cache:
- cached_response = self.controller.cached_request(req.url, req.headers)
- if cached_response:
- SimaFM.stats.update(ccontrol=SimaFM.stats.get('ccontrol')+1)
- return cached_response.json()
- try:
- return self._fetch_ws(req)
- except Timeout:
- raise WSTimeout('Failed to reach server within {0}s'.format(
- SOCKET_TIMEOUT))
- except ConnectionError as err:
- raise WSError(err)
-
- @Throttle(WAIT_BETWEEN_REQUESTS)
- def _fetch_ws(self, prepreq):
- """fetch from web service"""
- sess = Session()
- resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
- if resp.status_code == 304:
- SimaFM.stats.update(etag=SimaFM.stats.get('etag')+1)
- resp = self.controller.update_cached_response(prepreq, resp)
- elif resp.status_code != 200:
- raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
- ans = resp.json()
- self._controls_answer(ans)
- if self.cache:
- self.controller.cache_response(resp.request, resp)
- return ans
-
def _controls_answer(self, ans):
"""Controls answer.
"""
@@ -132,25 +93,27 @@ class SimaFM:
"""
payload = self._forge_payload(artist)
# Construct URL
- ans = self._fetch(payload)
+ ans = self.http(self.root_url, payload)
+ self._controls_answer(ans.json())
# Artist might be found be return no 'artist' list…
# cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
# json format is broken IMHO, xml is more consistent IIRC
# Here what we got:
# >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
# autocorrect=1 should fix it, checking anyway.
- simarts = ans.get('similarartists').get('artist')
+ simarts = ans.json().get('similarartists').get('artist')
if not isinstance(simarts, list):
raise WSError('Artist found but no similarities returned')
- for art in ans.get('similarartists').get('artist'):
+ for art in ans.json().get('similarartists').get('artist'):
yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
def get_toptrack(self, artist=None):
"""Fetch artist top tracks
"""
payload = self._forge_payload(artist, method='top')
- ans = self._fetch(payload)
- tops = ans.get('toptracks').get('track')
+ ans = self.http(self.root_url, payload)
+ self._controls_answer(ans.json())
+ tops = ans.json().get('toptracks').get('track')
art = {
'artist': artist.name,
'musicbrainz_artistid': artist.mbid,
diff --git a/sima/lib/webserv.py b/sima/lib/webserv.py
index 5e78785..4a048e9 100644
--- a/sima/lib/webserv.py
+++ b/sima/lib/webserv.py
@@ -236,18 +236,22 @@ class WebService(Plugin):
self.log.info('First five similar artist(s): {}...'.format(
' / '.join([a for a in list(similar)[0:5]])))
self.log.info('Looking availability in music library')
- ret = self.get_artists_from_player(similar)
+ ret = set(self.get_artists_from_player(similar))
ret_extra = None
if len(self.history) >= 2:
if self.plugin_conf.getint('depth') > 1:
ret_extra = self.get_recursive_similar_artist()
if ret_extra:
- ret = list(set(ret) | set(ret_extra))
+ ret = set(ret) | set(ret_extra)
if not ret:
self.log.warning('Got nothing from music library.')
self.log.warning('Try running in debug mode to guess why...')
return []
self.log.info('Got {} artists in library'.format(len(ret)))
+ queued_artists = { trk.artist for trk in self.player.queue }
+ if ret & queued_artists:
+ self.log.debug('Removing already queued artist: {0}'.format(ret & queued_artists))
+ ret = list(ret - queued_artists)
# Move around similars items to get in unplayed|not recently played
# artist first.
return self._get_artists_list_reorg(ret)
@@ -326,6 +330,7 @@ class WebService(Plugin):
self.log.info('{0.name} ratelimit: {0.ratelimit}'.format(self.ws))
for trk in titles:
found = self.player.fuzzy_find_track(artist.name, trk.title)
+ random.shuffle(found)
if found:
self.log.debug('{0}'.format(found[0]))
if self.filter_track(found):
@@ -340,6 +345,7 @@ class WebService(Plugin):
self.log.debug('Trying to find titles to add for "{}"'.format(
artist))
found = self.player.find_track(artist)
+ random.shuffle(found)
if not found:
self.log.debug('Found nothing to queue for {0}'.format(artist))
continue
diff --git a/sima/utils/config.py b/sima/utils/config.py
index b3a8d46..786208a 100644
--- a/sima/utils/config.py
+++ b/sima/utils/config.py
@@ -29,8 +29,8 @@ import logging
import sys
from configparser import Error
-from os import (makedirs, environ, stat, chmod)
-from os.path import (join, isdir, isfile)
+from os import (access, makedirs, environ, stat, chmod, W_OK, R_OK)
+from os.path import (join, isdir, isfile, dirname, exists)
from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
from . import utils
@@ -120,9 +120,32 @@ class ConfMan(object): # CONFIG MANAGER CLASS
## INIT CALLS
self.init_config()
self.supersedes_config_with_cmd_line_options()
+ # Controls files access
+ self.control_facc()
# generate dbfile
self.config['sima']['db_file'] = join(self.config['sima']['var_dir'], 'sima.db')
+ def control_facc(self):
+ """TODO: redundant with startopt cli args controls
+ """
+ ok = True
+ for op, ftochk in [('log', self.config['log']['logfile']),
+ ('pidfile', self.config['daemon']['pidfile']),]:
+ if not ftochk:
+ continue
+ if not exists(ftochk):
+ # Is parent directory writable then
+ filedir = dirname(ftochk)
+ if not access(filedir, W_OK):
+ self.log.critical('no write access to "{0}" ({1})'.format(filedir, op))
+ ok = False
+ else:
+ if not access(ftochk, W_OK):
+ self.log.critical('no write access to "{0}" ({1}))'.format(ftochk, op))
+ ok = False
+ if not ok:
+ sys.exit(2)
+
def control_mod(self):
"""
Controls conf file permissions.
@@ -191,8 +214,8 @@ class ConfMan(object): # CONFIG MANAGER CLASS
chmod(conf_dir, 0o700)
self.conf_file = join(conf_dir, CONF_FILE)
else:
- self.log.error('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
- self.log.error('Please use "--config" to locate the conf file')
+ self.log.critical('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
+ self.log.critical('Please use "--config" to locate the conf file')
sys.exit(1)
## Sima sqlite DB
diff --git a/tests/test_http.py b/tests/test_http.py
new file mode 100644
index 0000000..d6c4388
--- /dev/null
+++ b/tests/test_http.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+import unittest
+import time
+
+from unittest.mock import Mock
+
+from sima.lib.cache import DictCache
+from sima.lib.http import CacheController
+
+TIME_FMT = "%a, %d %b %Y %H:%M:%S GMT"
+
+
+class TestCacheControlRequest(unittest.TestCase):
+ url = 'http://foo.com/bar'
+
+ def setUp(self):
+ self.c = CacheController(
+ DictCache(),
+ )
+
+ def req(self, headers):
+ return self.c.cached_request(Mock(url=self.url, headers=headers))
+
+ def test_cache_request_no_cache(self):
+ resp = self.req({'cache-control': 'no-cache'})
+ assert not resp
+
+ def test_cache_request_pragma_no_cache(self):
+ resp = self.req({'pragma': 'no-cache'})
+ assert not resp
+
+ def test_cache_request_no_store(self):
+ resp = self.req({'cache-control': 'no-store'})
+ assert not resp
+
+ def test_cache_request_max_age_0(self):
+ resp = self.req({'cache-control': 'max-age=0'})
+ assert not resp
+
+ def test_cache_request_not_in_cache(self):
+ resp = self.req({})
+ assert not resp
+
+ def test_cache_request_fresh_max_age(self):
+ now = time.strftime(TIME_FMT, time.gmtime())
+ resp = Mock(headers={'cache-control': 'max-age=3600',
+ 'date': now})
+
+ cache = DictCache({self.url: resp})
+ self.c.cache = cache
+ r = self.req({})
+ assert r == resp
+
+ def test_cache_request_unfresh_max_age(self):
+ earlier = time.time() - 3700 # epoch - 1h01m40s
+ now = time.strftime(TIME_FMT, time.gmtime(earlier))
+ resp = Mock(headers={'cache-control': 'max-age=3600',
+ 'date': now})
+ self.c.cache = DictCache({self.url: resp})
+ r = self.req({})
+ assert not r
+
+ def test_cache_request_fresh_expires(self):
+ later = time.time() + 86400 # GMT + 1 day
+ expires = time.strftime(TIME_FMT, time.gmtime(later))
+ now = time.strftime(TIME_FMT, time.gmtime())
+ resp = Mock(headers={'expires': expires,
+ 'date': now})
+ cache = DictCache({self.url: resp})
+ self.c.cache = cache
+ r = self.req({})
+ assert r == resp
+
+ def test_cache_request_unfresh_expires(self):
+ sooner = time.time() - 86400 # GMT - 1 day
+ expires = time.strftime(TIME_FMT, time.gmtime(sooner))
+ now = time.strftime(TIME_FMT, time.gmtime())
+ resp = Mock(headers={'expires': expires,
+ 'date': now})
+ cache = DictCache({self.url: resp})
+ self.c.cache = cache
+ r = self.req({})
+ assert not r
+
+# VIM MODLINE
+# vim: ai ts=4 sw=4 sts=4 expandtab
+
diff --git a/tests/test_simastr.py b/tests/test_simastr.py
index ebd44f0..88b8b97 100644
--- a/tests/test_simastr.py
+++ b/tests/test_simastr.py
@@ -25,6 +25,10 @@ class TestSequenceFunctions(unittest.TestCase):
'Desert Sessions And PJ Harvey',
self.assertTrue
),
+ ( 'Smells like teen spirit',
+ 'Smells Like Teen Spirits (live)',
+ self.assertTrue
+ ),
]
sima.lib.simastr.SimaStr.diafilter = True
for sta, stb, assertfunc in tests:
--
mpd-sima packaging
More information about the pkg-multimedia-commits
mailing list