[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