--- upmpdcli-1.9.17.orig/debian/control
+++ upmpdcli-1.9.17/debian/control
@@ -0,0 +1,141 @@
+Source: upmpdcli
+Section: sound
+Priority: optional
+Maintainer: Jean-Francois Dockes <jf@dockes.org>
+# Also may need: qt5-default (if qmake says: could not find a Qt installation of '')
+# Note that we should not have to add libnpupnp-dev and dependants cause we don't use them directly
+# but this seems to be necessary anyway. Looks like a meson bug
+Build-Depends: debhelper-compat (=12),
+               pkg-config,
+               meson,
+               python3,
+               libmpdclient-dev,
+               libmicrohttpd-dev,
+               libjsoncpp-dev,
+               libexpat1-dev,
+               libcurl4-gnutls-dev,
+               qt5-qmake,
+               qtbase5-dev,
+               libupnpp-dev (>= 0.26.0),
+               libnpupnp-dev
+Standards-Version: 4.5.1
+Homepage: http://www.lesbonscomptes.com/upmpdcli
+Vcs-Browser: https://framagit.org/medoc92/upmpdcli
+Vcs-Git: https://framagit.org/medoc92/upmpdcli.git
+
+Package: upmpdcli
+Architecture: any
+Pre-Depends: ${misc:Pre-Depends}
+# Note: python3 and python3-requests are used by the radio scripts
+Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, python3, python3-requests
+Section: sound
+Description: UPnP Media Renderer front-end to MPD, the Music Player Daemon
+ upmpdcli primarily acts as an UPnP Media Renderer on the front-end so that
+ you can control it with any UPnP controller like, e.g. BubbleUPnP on an
+ Android tablet. It uses an MPD instance to actually play the tracks. 
+ A typical configuration might have for example, MPD running on a Raspberry
+ PI, with upmpdcli on the same host or any other Linux PC on the network.
+ The program also has a Media Server function, which can be configured by
+ various plugins to act as a Proxy for external services (streaming or radios)
+ or to serve local audio files.
+ 
+Package: upmpdcli-config
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Section: sound
+Description: GUI configuration editor for upmpdcli.
+ upmpdcli-config is a Qt-based GUI for the guided editing an upmpdcli configuration
+ file. It presents the different parameters in sections, with appropriate descriptions.
+
+Package: scweb
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, ${misc:Depends}
+Description: WEB interface for controlling multiple Songcast Receivers
+ scweb is a WEB front-end to the scctl songcast receiver control command. It
+ allows grouping and ungrouping songcast clients from a WEB browser.
+
+Package: upmpdcli-bbc
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, python3-bs4, python3-feedparser,
+         python3-dateutil, ${misc:Depends}
+Description: upmpdcli BBC radio plugin
+ upmpdcli-bbc allows upmpdcli to retrieve the BBC radio programmes from the
+ web site and listen to them.
+
+Package: upmpdcli-hra
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli highresaudio plugin
+ upmpdcli-hra allows upmpdcli to proxy access to the highresaudio
+ https://www.highresaudio.com/en music streaming service.
+
+Package: upmpdcli-mother-earth-radio
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli Mother Earth Radio plugin
+ upmpdcli-mother-earth-radio allows upmpdcli to proxy access to the
+ Mother Earth Radio streams.
+
+Package: upmpdcli-qobuz
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli Qobuz plugin
+ upmpdcli-qobuz allows upmpdcli to proxy access to the Qobuz music
+ streaming service.
+
+Package: upmpdcli-radio-browser
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli https://radio-browser.info plugin
+ upmpdcli-radio-browser allows you to browse the radio catalog through the
+ Media Server interface and play streams. You will need to install the
+ pyradios Python3 module with pip3, it is not packaged at the moment.
+
+Package: upmpdcli-radio-paradise
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli Radio Paradise plugin
+ upmpdcli-mother-earth-radio allows upmpdcli to proxy access to the
+ Radio Paradise streams.
+
+Package: upmpdcli-subsonic
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, ${misc:Depends}
+Description: upmpdcli Subsonic plugin
+ upmpdcli-subsonic allows upmpdcli to proxy access to a Subsonic or
+ Navidrome server. It needs the installation of the subsonic-connector
+ (version 0.3.12) Python module (not packaged
+ by Debian, use PyPI).
+
+Package: upmpdcli-tidal
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, python3-requests, ${misc:Depends}
+Description: upmpdcli Tidal plugin. You will need to install the tidalapi
+ Python3 module with pip3, it is not packaged at the moment. 
+
+Package: upmpdcli-radios
+Architecture: all
+Section: sound
+Depends: upmpdcli, python3, ${misc:Depends}
+Description: upmpdcli radio list export plugin
+ upmpdcli-radios exports the upmpdcli OpenHome service radio list through
+ the upmpdcli Media Server.
+
+Package: upmpdcli-uprcl
+Architecture: all
+Section: sound
+Depends: upmpdcli, recollcmd (>= 1.43.11), python3 (>= 3.5), python3-requests,
+ python3-recoll, python3-waitress, python3-mutagen, ${misc:Depends}
+Description: upmpdcli local Media Server plugin
+ upmpdcli-uprcl implement a Media Server for local audio files. It indexes
+ the metadata from local audio files using Recoll, and makes it available and
+ browseable to UPnP Control Points on the local network.
--- upmpdcli-1.9.17.orig/debian/copyright
+++ upmpdcli-1.9.17/debian/copyright
@@ -0,0 +1,96 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: upmpdcli
+Source: https://framagit.org/medoc92/upmpdcli/
+
+Files: *
+Copyright: 2005-2019, Jean-Francois Dockes <jf@dockes.org>
+License: LGPL-2.1+
+
+Files: src/mediaserver/cdplugins/tidal/*
+       src/mediaserver/cdplugins/attic/spotify/spotify-app.py
+       src/mediaserver/cdplugins/pycommon/routing.py
+       src/mediaserver/cdplugins/pycommon/upmplgmodels.py
+Copyright: 2014 Thomas Amland
+License: LGPL-3+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This program 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 Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the full text of the GNU Lesser General Public
+ License version 3 can be found in the file
+ '/usr/share/common-licenses/LGPL-3'.
+ 
+
+Files: src/mediaserver/cdplugins/uprcl/bottle.py
+Copyright: 2009-2018, Marcel Hellkamp.
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+
+Files: src/mediaserver/cdplugins/qobuz/*
+Copyright: 2012 by Joachim Basmaison, Cyril Leclerc
+License: GPL-3+
+ This program 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.
+ .
+ This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the full text of the GNU General Public
+ License version 3 can be found in the file
+ '/usr/share/common-licenses/GPL-3'.
+ 
+
+Files: src/pathut.cpp
+Copyright: 2008-2019 Free Software Foundation, Inc.
+License: LGPL-2.1+
+
+License: LGPL-2.1+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+ .
+ This program 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 Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program; if not, see <http://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the full text of the GNU Lesser General Public
+ License version 2 can be found in the file
+ '/usr/share/common-licenses/LGPL-2.1'.
--- upmpdcli-1.9.17.orig/debian/patches/cfgui-cpp14.diff
+++ upmpdcli-1.9.17/debian/patches/cfgui-cpp14.diff
@@ -0,0 +1,13 @@
+diff --git a/cfgui/upmpdcli-config.pro.in b/cfgui/upmpdcli-config.pro.in
+index ff7b485..f8b4d66 100644
+--- a/cfgui/upmpdcli-config.pro.in
++++ b/cfgui/upmpdcli-config.pro.in
+@@ -2,7 +2,7 @@ TEMPLATE	= app
+ LANGUAGE	= C++
+ 
+ VPATH = @srcdir@
+-QMAKE_CXXFLAGS += -std=c++11 -I../ -DENABLE_XMLCONF
++QMAKE_CXXFLAGS += -std=c++14 -I../ -DENABLE_XMLCONF
+ 	       
+ 
+ DEFINES += MDU_INCLUDE_LOG=\'<libupnpp/log.h>\'
--- upmpdcli-1.9.17.orig/debian/patches/debian-fixes.diff
+++ upmpdcli-1.9.17/debian/patches/debian-fixes.diff
@@ -0,0 +1,29 @@
+diff --git a/Makefile.am b/Makefile.am
+index 08f5f58..4481622 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -264,6 +264,7 @@ install-data-hook: web.tar.gz rdpl2stream.tar.gz uprclbottle.tar.gz
+ 	(cd $(DESTDIR)/$(cdpluginsspotifydir); chmod a+x spotify-app.py) 
+ 	(cd $(DESTDIR)/$(cdpluginsuprcldir); chmod a+x uprcl-app.py) 
+ 	(cd $(DESTDIR)/$(radioscriptsdir);chmod a+rx . *)
++	(cd $(DESTDIR)/$(radioscriptsdir);chmod a-x radiolist.conf)
+         
+ dist_pkgdata_SCRIPTS = samplescripts/Analog-Input
+ 
+diff --git a/samplescripts/Analog-Input b/samplescripts/Analog-Input
+old mode 100644
+new mode 100755
+index ac956fd..87cff68
+--- a/samplescripts/Analog-Input
++++ b/samplescripts/Analog-Input
+@@ -1,4 +1,4 @@
+-#!/usr/bin//python
++#!/usr/bin/python3
+ 
+ from __future__ import print_function
+ 
+@@ -190,4 +190,3 @@ while True:
+ 
+ time.sleep(1)
+ cleanup(0)
+-
--- upmpdcli-1.9.17.orig/debian/patches/gcc-49-initlist-fix.diff
+++ upmpdcli-1.9.17/debian/patches/gcc-49-initlist-fix.diff
@@ -0,0 +1,15 @@
+Index: upmpdcli-1.5.15/src/ohcredentials.cxx
+===================================================================
+--- upmpdcli-1.5.15.orig/src/ohcredentials.cxx
++++ upmpdcli-1.5.15/src/ohcredentials.cxx
+@@ -82,8 +82,9 @@ struct ServiceCreds {
+                 return;
+             }
+             std::unordered_map<std::string, std::string> res;
++            std::unordered_map<std::string, std::string> empty;
+             auto toknm = "appid";
+-            cmd->callproc("getappid", {}, res);
++            cmd->callproc("getappid", empty, res);
+             auto it = res.find(toknm);
+             if (it == res.end()) {
+                 LOGERR("ServiceCreds::init: no " << toknm << " value in getappid call result\n");
--- upmpdcli-1.9.17.orig/debian/patches/import-collections-abc.patch
+++ upmpdcli-1.9.17/debian/patches/import-collections-abc.patch
@@ -0,0 +1,2135 @@
+commit 1c9c363fd0d8aa0f29a6c382e72ceea6fdf81939
+Author: Jean-Francois Dockes <jf@dockes.org>
+Date:   Sun Aug 14 08:40:43 2022 +0200
+
+    bottle.py modules: collections->collections.abc
+
+diff --git a/src/mediaserver/cdplugins/uprcl/bottle.py b/src/mediaserver/cdplugins/uprcl/bottle.py
+index a884013..a4c4727 100755
+--- a/src/mediaserver/cdplugins/uprcl/bottle.py
++++ b/src/mediaserver/cdplugins/uprcl/bottle.py
+@@ -129,7 +129,7 @@ if py3k:
+     from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
+     urlunquote = functools.partial(urlunquote, encoding='latin1')
+     from http.cookies import SimpleCookie, Morsel, CookieError
+-    from collections import MutableMapping as DictMixin
++    from collections.abc import MutableMapping as DictMixin
+     import pickle
+     from io import BytesIO
+     import configparser
+@@ -152,7 +152,7 @@ else:  # 2.x
+     import cPickle as pickle
+     from StringIO import StringIO as BytesIO
+     import ConfigParser as configparser
+-    from collections import MutableMapping as DictMixin
++    from collections.abc import MutableMapping as DictMixin
+     unicode = unicode
+     json_loads = json_lds
+     exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
+diff --git a/web/bottle.py b/web/bottle.py
+index a027b7d..a4c4727 100644
+--- a/web/bottle.py
++++ b/web/bottle.py
+@@ -1,86 +1,117 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python
+ # -*- coding: utf-8 -*-
+ """
+ Bottle is a fast and simple micro-framework for small web applications. It
+-offers request dispatching (Routes) with url parameter support, templates,
++offers request dispatching (Routes) with URL parameter support, templates,
+ a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
+ template engines - all in a single file and with no dependencies other than the
+ Python Standard Library.
+ 
+ Homepage and documentation: http://bottlepy.org/
+ 
+-Copyright (c) 2014, Marcel Hellkamp.
++Copyright (c) 2009-2018, Marcel Hellkamp.
+ License: MIT (see LICENSE for details)
+ """
+ 
+-from __future__ import with_statement
++import sys
+ 
+ __author__ = 'Marcel Hellkamp'
+ __version__ = '0.13-dev'
+ __license__ = 'MIT'
+ 
+-# The gevent and eventlet server adapters need to patch some modules before
+-# they are imported. This is why we parse the commandline parameters here but
+-# handle them later
+-if __name__ == '__main__':
+-    from optparse import OptionParser
+-    _cmd_parser = OptionParser(
+-        usage="usage: %prog [options] package.module:app")
+-    _opt = _cmd_parser.add_option
+-    _opt("--version", action="store_true", help="show version number.")
+-    _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
+-    _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
+-    _opt("-p", "--plugin",
+-         action="append",
+-         help="install additional plugin/s.")
+-    _opt("--debug", action="store_true", help="start server in debug mode.")
+-    _opt("--reload", action="store_true", help="auto-reload on file changes.")
+-    _cmd_options, _cmd_args = _cmd_parser.parse_args()
+-    if _cmd_options.server:
+-        if _cmd_options.server.startswith('gevent'):
++###############################################################################
++# Command-line interface ######################################################
++###############################################################################
++# INFO: Some server adapters need to monkey-patch std-lib modules before they
++# are imported. This is why some of the command-line handling is done here, but
++# the actual call to _main() is at the end of the file.
++
++
++def _cli_parse(args):  # pragma: no coverage
++    from argparse import ArgumentParser
++
++    parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app")
++    opt = parser.add_argument
++    opt("--version", action="store_true", help="show version number.")
++    opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
++    opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
++    opt("-p", "--plugin", action="append", help="install additional plugin/s.")
++    opt("-c", "--conf", action="append", metavar="FILE",
++        help="load config values from FILE.")
++    opt("-C", "--param", action="append", metavar="NAME=VALUE",
++        help="override config values.")
++    opt("--debug", action="store_true", help="start server in debug mode.")
++    opt("--reload", action="store_true", help="auto-reload on file changes.")
++    opt('app', help='WSGI app entry point.', nargs='?')
++
++    cli_args = parser.parse_args(args[1:])
++
++    return cli_args, parser
++
++
++def _cli_patch(cli_args):  # pragma: no coverage
++    parsed_args, _ = _cli_parse(cli_args)
++    opts = parsed_args
++    if opts.server:
++        if opts.server.startswith('gevent'):
+             import gevent.monkey
+             gevent.monkey.patch_all()
+-        elif _cmd_options.server.startswith('eventlet'):
++        elif opts.server.startswith('eventlet'):
+             import eventlet
+             eventlet.monkey_patch()
+ 
++
++if __name__ == '__main__':
++    _cli_patch(sys.argv)
++
++###############################################################################
++# Imports and Python 2/3 unification ##########################################
++###############################################################################
++
++
+ import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\
+-        os, re, sys, tempfile, threading, time, warnings
++        os, re, tempfile, threading, time, warnings, weakref, hashlib
+ 
+ from types import FunctionType
+ from datetime import date as datedate, datetime, timedelta
+ from tempfile import TemporaryFile
+ from traceback import format_exc, print_exc
+-from inspect import getargspec
+ from unicodedata import normalize
+ 
+ try:
+-    from simplejson import dumps as json_dumps, loads as json_lds
+-except ImportError:  # pragma: no cover
++    from ujson import dumps as json_dumps, loads as json_lds
++except ImportError:
++    from json import dumps as json_dumps, loads as json_lds
++
++# inspect.getargspec was removed in Python 3.6, use
++# Signature-based version where we can (Python 3.3+)
++try:
++    from inspect import signature
++    def getargspec(func):
++        params = signature(func).parameters
++        args, varargs, keywords, defaults = [], None, None, []
++        for name, param in params.items():
++            if param.kind == param.VAR_POSITIONAL:
++                varargs = name
++            elif param.kind == param.VAR_KEYWORD:
++                keywords = name
++            else:
++                args.append(name)
++                if param.default is not param.empty:
++                    defaults.append(param.default)
++        return (args, varargs, keywords, tuple(defaults) or None)
++except ImportError:
+     try:
+-        from json import dumps as json_dumps, loads as json_lds
++        from inspect import getfullargspec
++        def getargspec(func):
++            spec = getfullargspec(func)
++            kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs)
++            return kwargs, spec[1], spec[2], spec[3]
+     except ImportError:
+-        try:
+-            from django.utils.simplejson import dumps as json_dumps, loads as json_lds
+-        except ImportError:
+-
+-            def json_dumps(data):
+-                raise ImportError(
+-                    "JSON support requires Python 2.6 or simplejson.")
+-
+-            json_lds = json_dumps
+-
+-# We now try to fix 2.5/2.6/3.1/3.2 incompatibilities.
+-# It ain't pretty but it works... Sorry for the mess.
++        from inspect import getargspec
+ 
+-py = sys.version_info
+-py3k = py >= (3, 0, 0)
+-py25 = py <  (2, 6, 0)
+-py31 = (3, 1, 0) <= py < (3, 2, 0)
++py3k = sys.version_info.major > 2
+ 
+-# Workaround for the missing "as" keyword in py3k.
+-def _e():
+-    return sys.exc_info()[1]
+ 
+ # Workaround for the "print is a keyword/function" Python 2/3 dilemma
+ # and a fallback for mod_wsgi (resticts stdout/err attribute access)
+@@ -97,11 +128,12 @@ if py3k:
+     from urllib.parse import urljoin, SplitResult as UrlSplitResult
+     from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
+     urlunquote = functools.partial(urlunquote, encoding='latin1')
+-    from http.cookies import SimpleCookie
+-    from collections import MutableMapping as DictMixin
++    from http.cookies import SimpleCookie, Morsel, CookieError
++    from collections.abc import MutableMapping as DictMixin
+     import pickle
+     from io import BytesIO
+-    from configparser import ConfigParser
++    import configparser
++
+     basestring = str
+     unicode = str
+     json_loads = lambda s: json_lds(touni(s))
+@@ -115,49 +147,31 @@ else:  # 2.x
+     import thread
+     from urlparse import urljoin, SplitResult as UrlSplitResult
+     from urllib import urlencode, quote as urlquote, unquote as urlunquote
+-    from Cookie import SimpleCookie
++    from Cookie import SimpleCookie, Morsel, CookieError
+     from itertools import imap
+     import cPickle as pickle
+     from StringIO import StringIO as BytesIO
+-    from ConfigParser import SafeConfigParser as ConfigParser
+-    if py25:
+-        msg = "Python 2.5 support may be dropped in future versions of Bottle."
+-        warnings.warn(msg, DeprecationWarning)
+-        from UserDict import DictMixin
+-
+-        def next(it):
+-            return it.next()
+-
+-        bytes = str
+-    else:  # 2.6, 2.7
+-        from collections import MutableMapping as DictMixin
++    import ConfigParser as configparser
++    from collections.abc import MutableMapping as DictMixin
+     unicode = unicode
+     json_loads = json_lds
+-    eval(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
+-
++    exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '<py3fix>', 'exec'))
+ 
+ # Some helpers for string/byte handling
+ def tob(s, enc='utf8'):
+-    return s.encode(enc) if isinstance(s, unicode) else bytes(s)
++    if isinstance(s, unicode):
++        return s.encode(enc)
++    return b'' if s is None else bytes(s)
+ 
+ 
+ def touni(s, enc='utf8', err='strict'):
+     if isinstance(s, bytes):
+         return s.decode(enc, err)
+-    else:
+-        return unicode(s or ("" if s is None else s))
++    return unicode("" if s is None else s)
+ 
+ 
+ tonat = touni if py3k else tob
+ 
+-# 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense).
+-# 3.1 needs a workaround.
+-if py31:
+-    from io import TextIOWrapper
+-
+-    class NCTextIOWrapper(TextIOWrapper):
+-        def close(self):
+-            pass  # Keep wrapped buffer open.
+ 
+ 
+ # A bug in functools causes it to break if the wrapper is an instance method
+@@ -171,8 +185,14 @@ def update_wrapper(wrapper, wrapped, *a, **ka):
+ # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
+ 
+ 
+-def depr(message, strict=False):
+-    warnings.warn(message, DeprecationWarning, stacklevel=3)
++def depr(major, minor, cause, fix):
++    text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\
++           "Cause: %s\n"\
++           "Fix: %s\n" % (major, minor, cause, fix)
++    if DEBUG == 'strict':
++        raise DeprecationWarning(text)
++    warnings.warn(text, DeprecationWarning, stacklevel=3)
++    return DeprecationWarning(text)
+ 
+ 
+ def makelist(data):  # This is just too handy
+@@ -216,7 +236,7 @@ class cached_property(object):
+         property. """
+ 
+     def __init__(self, func):
+-        self.__doc__ = getattr(func, '__doc__')
++        update_wrapper(self, func)
+         self.func = func
+ 
+     def __get__(self, obj, cls):
+@@ -238,7 +258,7 @@ class lazy_attribute(object):
+         return value
+ 
+ ###############################################################################
+-# Exceptions and Events ########################################################
++# Exceptions and Events #######################################################
+ ###############################################################################
+ 
+ 
+@@ -334,6 +354,9 @@ class Router(object):
+         for match in self.rule_syntax.finditer(rule):
+             prefix += rule[offset:match.start()]
+             g = match.groups()
++            if g[2] is not None:
++                depr(0, 13, "Use of old route syntax.",
++                            "Use <name> instead of :name in routes.")
+             if len(g[0]) % 2:  # Escaped wildcard
+                 prefix += match.group(0)[len(g[0]):]
+                 offset = match.end()
+@@ -384,9 +407,8 @@ class Router(object):
+         try:
+             re_pattern = re.compile('^(%s)$' % pattern)
+             re_match = re_pattern.match
+-        except re.error:
+-            raise RouteSyntaxError("Could not add Route: %s (%s)" %
+-                                   (rule, _e()))
++        except re.error as e:
++            raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e))
+ 
+         if filters:
+ 
+@@ -442,8 +464,8 @@ class Router(object):
+                 query['anon%d' % i] = value
+             url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder])
+             return url if not query else url + '?' + urlencode(query)
+-        except KeyError:
+-            raise RouteBuildError('Missing URL argument: %r' % _e().args[0])
++        except KeyError as E:
++            raise RouteBuildError('Missing URL argument: %r' % E.args[0])
+ 
+     def match(self, environ):
+         """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """
+@@ -471,7 +493,7 @@ class Router(object):
+         nocheck = set(methods)
+         for method in set(self.static) - nocheck:
+             if path in self.static[method]:
+-                allowed.add(verb)
++                allowed.add(method)
+         for method in set(self.dyna_regexes) - allowed - nocheck:
+             for combined, rules in self.dyna_regexes[method]:
+                 match = combined(path)
+@@ -512,7 +534,8 @@ class Route(object):
+         #: Additional keyword arguments passed to the :meth:`Bottle.route`
+         #: decorator are stored in this dictionary. Used for route-specific
+         #: plugin configuration and meta-data.
+-        self.config = ConfigDict().load_dict(config)
++        self.config = app.config._make_overlay()
++        self.config.load_dict(config)
+ 
+     @cached_property
+     def call(self):
+@@ -581,9 +604,10 @@ class Route(object):
+     def get_config(self, key, default=None):
+         """ Lookup a config field and return its value, first checking the
+             route.config, then route.app.config."""
+-        for conf in (self.config, self.app.config):
+-            if key in conf: return conf[key]
+-        return default
++        depr(0, 13, "Route.get_config() is deprectated.",
++                    "The Route.config property already includes values from the"
++                    " application config for missing keys. Access it directly.")
++        return self.config.get(key, default)
+ 
+     def __repr__(self):
+         cb = self.get_undecorated_callback()
+@@ -603,15 +627,34 @@ class Bottle(object):
+                          let debugging middleware handle exceptions.
+     """
+ 
+-    def __init__(self, catchall=True, autojson=True):
++    @lazy_attribute
++    def _global_config(cls):
++        cfg = ConfigDict()
++        cfg.meta_set('catchall', 'validate', bool)
++        return cfg
+ 
++    def __init__(self, **kwargs):
+         #: A :class:`ConfigDict` for app specific configuration.
+-        self.config = ConfigDict()
+-        self.config._on_change = functools.partial(self.trigger_hook, 'config')
+-        self.config.meta_set('autojson', 'validate', bool)
+-        self.config.meta_set('catchall', 'validate', bool)
+-        self.config['catchall'] = catchall
+-        self.config['autojson'] = autojson
++        self.config = self._global_config._make_overlay()
++        self.config._add_change_listener(
++            functools.partial(self.trigger_hook, 'config'))
++
++        self.config.update({
++            "catchall": True
++        })
++
++        if kwargs.get('catchall') is False:
++            depr(0, 13, "Bottle(catchall) keyword argument.",
++                        "The 'catchall' setting is now part of the app "
++                        "configuration. Fix: `app.config['catchall'] = False`")
++            self.config['catchall'] = False
++        if kwargs.get('autojson') is False:
++            depr(0, 13, "Bottle(autojson) keyword argument.",
++                 "The 'autojson' setting is now part of the app "
++                 "configuration. Fix: `app.config['json.enable'] = False`")
++            self.config['json.disable'] = True
++
++        self._mounts = []
+ 
+         #: A :class:`ResourceManager` for application files
+         self.resources = ResourceManager()
+@@ -622,15 +665,14 @@ class Bottle(object):
+ 
+         # Core plugins
+         self.plugins = []  # List of installed plugins.
+-        if self.config['autojson']:
+-            self.install(JSONPlugin())
++        self.install(JSONPlugin())
+         self.install(TemplatePlugin())
+ 
+     #: If true, most exceptions are caught and returned as :exc:`HTTPError`
+     catchall = DictProperty('config', 'catchall')
+ 
+     __hook_names = 'before_request', 'after_request', 'app_reset', 'config'
+-    __hook_reversed = 'after_request'
++    __hook_reversed = {'after_request'}
+ 
+     @cached_property
+     def _hooks(self):
+@@ -672,21 +714,10 @@ class Bottle(object):
+ 
+         return decorator
+ 
+-    def mount(self, prefix, app, **options):
+-        """ Mount an application (:class:`Bottle` or plain WSGI) to a specific
+-            URL prefix. Example::
+-
+-                root_app.mount('/admin/', admin_app)
+-
+-            :param prefix: path prefix or `mount-point`. If it ends in a slash,
+-                that slash is mandatory.
+-            :param app: an instance of :class:`Bottle` or a WSGI application.
+-
+-            All other parameters are passed to the underlying :meth:`route` call.
+-        """
+-
++    def _mount_wsgi(self, prefix, app, **options):
+         segments = [p for p in prefix.split('/') if p]
+-        if not segments: raise ValueError('Empty path prefix.')
++        if not segments:
++            raise ValueError('WSGI applications cannot be mounted to "/".')
+         path_depth = len(segments)
+ 
+         def mountpoint_wrapper():
+@@ -703,8 +734,7 @@ class Bottle(object):
+                     return rs.body.append
+ 
+                 body = app(request.environ, start_response)
+-                if body and rs.body: body = itertools.chain(rs.body, body)
+-                rs.body = body or rs.body
++                rs.body = itertools.chain(rs.body, body) if rs.body else body
+                 return rs
+             finally:
+                 request.path_shift(-path_depth)
+@@ -718,6 +748,59 @@ class Bottle(object):
+         if not prefix.endswith('/'):
+             self.route('/' + '/'.join(segments), **options)
+ 
++    def _mount_app(self, prefix, app, **options):
++        if app in self._mounts or '_mount.app' in app.config:
++            depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.",
++                 "Clone application before mounting to a different location.")
++            return self._mount_wsgi(prefix, app, **options)
++
++        if options:
++            depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.",
++                 "Do not specify any route options when mounting bottle application.")
++            return self._mount_wsgi(prefix, app, **options)
++
++        if not prefix.endswith("/"):
++            depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.",
++                 "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.")
++            return self._mount_wsgi(prefix, app, **options)
++
++        self._mounts.append(app)
++        app.config['_mount.prefix'] = prefix
++        app.config['_mount.app'] = self
++        for route in app.routes:
++            route.rule = prefix + route.rule.lstrip('/')
++            self.add_route(route)
++
++    def mount(self, prefix, app, **options):
++        """ Mount an application (:class:`Bottle` or plain WSGI) to a specific
++            URL prefix. Example::
++
++                parent_app.mount('/prefix/', child_app)
++
++            :param prefix: path prefix or `mount-point`.
++            :param app: an instance of :class:`Bottle` or a WSGI application.
++
++            Plugins from the parent application are not applied to the routes
++            of the mounted child application. If you need plugins in the child
++            application, install them separately.
++
++            While it is possible to use path wildcards within the prefix path
++            (:class:`Bottle` childs only), it is highly discouraged.
++
++            The prefix path must end with a slash. If you want to access the
++            root of the child application via `/prefix` in addition to
++            `/prefix/`, consider adding a route with a 307 redirect to the
++            parent application.
++        """
++
++        if not prefix.startswith('/'):
++            raise ValueError("Prefix must start with '/'")
++
++        if isinstance(app, Bottle):
++            return self._mount_app(prefix, app, **options)
++        else:
++            return self._mount_wsgi(prefix, app, **options)
++
+     def merge(self, routes):
+         """ Merge the routes of another :class:`Bottle` application or a list of
+             :class:`Route` objects into this application. The routes keep their
+@@ -810,7 +893,7 @@ class Bottle(object):
+                 def hello(name):
+                     return 'Hello %s' % name
+ 
+-            The ``:name`` part is a wildcard. See :class:`Router` for syntax
++            The ``<name>`` part is a wildcard. See :class:`Router` for syntax
+             details.
+ 
+             :param path: Request path or a list of paths to listen to. If no
+@@ -867,51 +950,79 @@ class Bottle(object):
+         """ Equals :meth:`route` with a ``PATCH`` method parameter. """
+         return self.route(path, method, **options)
+ 
+-    def error(self, code=500):
+-        """ Decorator: Register an output handler for a HTTP error code"""
++    def error(self, code=500, callback=None):
++        """ Register an output handler for a HTTP error code. Can
++            be used as a decorator or called directly ::
+ 
+-        def wrapper(handler):
+-            self.error_handler[int(code)] = handler
+-            return handler
++                def error_handler_500(error):
++                    return 'error_handler_500'
+ 
+-        return wrapper
++                app.error(code=500, callback=error_handler_500)
++
++                @app.error(404)
++                def error_handler_404(error):
++                    return 'error_handler_404'
++
++        """
++
++        def decorator(callback):
++            if isinstance(callback, basestring): callback = load(callback)
++            self.error_handler[int(code)] = callback
++            return callback
++
++        return decorator(callback) if callback else decorator
+ 
+     def default_error_handler(self, res):
+-        return tob(template(ERROR_PAGE_TEMPLATE, e=res))
++        return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE')))
+ 
+     def _handle(self, environ):
+         path = environ['bottle.raw_path'] = environ['PATH_INFO']
+         if py3k:
+-            try:
+-                environ['PATH_INFO'] = path.encode('latin1').decode('utf8')
+-            except UnicodeError:
+-                return HTTPError(400, 'Invalid path string. Expected UTF-8')
++            environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore')
++
++        environ['bottle.app'] = self
++        request.bind(environ)
++        response.bind()
+ 
+         try:
+-            environ['bottle.app'] = self
+-            request.bind(environ)
+-            response.bind()
+-            try:
+-                self.trigger_hook('before_request')
+-                route, args = self.router.match(environ)
+-                environ['route.handle'] = route
+-                environ['bottle.route'] = route
+-                environ['route.url_args'] = args
+-                return route.call(**args)
+-            finally:
+-                self.trigger_hook('after_request')
+-        except HTTPResponse:
+-            return _e()
+-        except RouteReset:
+-            route.reset()
+-            return self._handle(environ)
++            while True: # Remove in 0.14 together with RouteReset
++                out = None
++                try:
++                    self.trigger_hook('before_request')
++                    route, args = self.router.match(environ)
++                    environ['route.handle'] = route
++                    environ['bottle.route'] = route
++                    environ['route.url_args'] = args
++                    out = route.call(**args)
++                    break
++                except HTTPResponse as E:
++                    out = E
++                    break
++                except RouteReset:
++                    depr(0, 13, "RouteReset exception deprecated",
++                                "Call route.call() after route.reset() and "
++                                "return the result.")
++                    route.reset()
++                    continue
++                finally:
++                    if isinstance(out, HTTPResponse):
++                        out.apply(response)
++                    try:
++                        self.trigger_hook('after_request')
++                    except HTTPResponse as E:
++                        out = E
++                        out.apply(response)
+         except (KeyboardInterrupt, SystemExit, MemoryError):
+             raise
+-        except Exception:
++        except Exception as E:
+             if not self.catchall: raise
+             stacktrace = format_exc()
+             environ['wsgi.errors'].write(stacktrace)
+-            return HTTPError(500, "Internal Server Error", _e(), stacktrace)
++            environ['wsgi.errors'].flush()
++            out = HTTPError(500, "Internal Server Error", E, stacktrace)
++            out.apply(response)
++
++        return out
+ 
+     def _cast(self, out, peek=None):
+         """ Try to convert the parameter into something WSGI compatible and set
+@@ -963,13 +1074,13 @@ class Bottle(object):
+                 first = next(iout)
+         except StopIteration:
+             return self._cast('')
+-        except HTTPResponse:
+-            first = _e()
++        except HTTPResponse as E:
++            first = E
+         except (KeyboardInterrupt, SystemExit, MemoryError):
+             raise
+-        except:
++        except Exception as error:
+             if not self.catchall: raise
+-            first = HTTPError(500, 'Unhandled exception', _e(), format_exc())
++            first = HTTPError(500, 'Unhandled exception', error, format_exc())
+ 
+         # These are the inner types allowed in iterator or generator objects.
+         if isinstance(first, HTTPResponse):
+@@ -999,15 +1110,16 @@ class Bottle(object):
+             return out
+         except (KeyboardInterrupt, SystemExit, MemoryError):
+             raise
+-        except:
++        except Exception as E:
+             if not self.catchall: raise
+             err = '<h1>Critical error while processing request: %s</h1>' \
+                   % html_escape(environ.get('PATH_INFO', '/'))
+             if DEBUG:
+                 err += '<h2>Error:</h2>\n<pre>\n%s\n</pre>\n' \
+                        '<h2>Traceback:</h2>\n<pre>\n%s\n</pre>\n' \
+-                       % (html_escape(repr(_e())), html_escape(format_exc()))
++                       % (html_escape(repr(E)), html_escape(format_exc()))
+             environ['wsgi.errors'].write(err)
++            environ['wsgi.errors'].flush()
+             headers = [('Content-Type', 'text/html; charset=UTF-8')]
+             start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info())
+             return [tob(err)]
+@@ -1024,6 +1136,12 @@ class Bottle(object):
+     def __exit__(self, exc_type, exc_value, traceback):
+         default_app.pop()
+ 
++    def __setattr__(self, name, value):
++        if name in self.__dict__:
++            raise AttributeError("Attribute %s already defined. Plugin conflict?" % name)
++        self.__dict__[name] = value
++
++
+ ###############################################################################
+ # HTTP and WSGI Tools ##########################################################
+ ###############################################################################
+@@ -1093,15 +1211,22 @@ class BaseRequest(object):
+         cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values()
+         return FormsDict((c.key, c.value) for c in cookies)
+ 
+-    def get_cookie(self, key, default=None, secret=None):
++    def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
+         """ Return the content of a cookie. To read a `Signed Cookie`, the
+             `secret` must match the one used to create the cookie (see
+             :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
+             cookie or wrong signature), return a default value. """
+         value = self.cookies.get(key)
+-        if secret and value:
+-            dec = cookie_decode(value, secret)  # (key, value) tuple or None
+-            return dec[1] if dec and dec[0] == key else default
++        if secret:
++            # See BaseResponse.set_cookie for details on signed cookies.
++            if value and value.startswith('!') and '?' in value:
++                sig, msg = map(tob, value[1:].split('?', 1))
++                hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
++                if _lscmp(sig, base64.b64encode(hash)):
++                    dst = pickle.loads(base64.b64decode(msg))
++                    if dst and dst[0] == key:
++                        return dst[1]
++            return default
+         return value or default
+ 
+     @DictProperty('environ', 'bottle.request.query', read_only=True)
+@@ -1123,6 +1248,7 @@ class BaseRequest(object):
+             :class:`FormsDict`. All keys and values are strings. File uploads
+             are stored separately in :attr:`files`. """
+         forms = FormsDict()
++        forms.recode_unicode = self.POST.recode_unicode
+         for name, item in self.POST.allitems():
+             if not isinstance(item, FileUpload):
+                 forms[name] = item
+@@ -1146,6 +1272,7 @@ class BaseRequest(object):
+ 
+         """
+         files = FormsDict()
++        files.recode_unicode = self.POST.recode_unicode
+         for name, item in self.POST.allitems():
+             if isinstance(item, FileUpload):
+                 files[name] = item
+@@ -1153,16 +1280,21 @@ class BaseRequest(object):
+ 
+     @DictProperty('environ', 'bottle.request.json', read_only=True)
+     def json(self):
+-        """ If the ``Content-Type`` header is ``application/json``, this
+-            property holds the parsed content of the request body. Only requests
+-            smaller than :attr:`MEMFILE_MAX` are processed to avoid memory
+-            exhaustion. """
++        """ If the ``Content-Type`` header is ``application/json`` or
++            ``application/json-rpc``, this property holds the parsed content
++            of the request body. Only requests smaller than :attr:`MEMFILE_MAX`
++            are processed to avoid memory exhaustion.
++            Invalid JSON raises a 400 error response.
++        """
+         ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0]
+-        if ctype == 'application/json':
++        if ctype in ('application/json', 'application/json-rpc'):
+             b = self._get_body_string()
+             if not b:
+                 return None
+-            return json_loads(b)
++            try:
++                return json_loads(b)
++            except (ValueError, TypeError):
++                raise HTTPError(400, 'Invalid JSON')
+         return None
+ 
+     def _iter_body(self, read, bufsize):
+@@ -1272,12 +1404,10 @@ class BaseRequest(object):
+         for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'):
+             if key in self.environ: safe_env[key] = self.environ[key]
+         args = dict(fp=self.body, environ=safe_env, keep_blank_values=True)
+-        if py31:
+-            args['fp'] = NCTextIOWrapper(args['fp'],
+-                                         encoding='utf8',
+-                                         newline='\n')
+-        elif py3k:
++
++        if py3k:
+             args['encoding'] = 'utf8'
++            post.recode_unicode = False
+         data = cgi.FieldStorage(**args)
+         self['_cgi.FieldStorage'] = data  #http://bugs.python.org/issue18394
+         data = data.list or []
+@@ -1458,29 +1588,47 @@ class BaseRequest(object):
+ 
+     def __setattr__(self, name, value):
+         if name == 'environ': return object.__setattr__(self, name, value)
+-        self.environ['bottle.request.ext.%s' % name] = value
++        key = 'bottle.request.ext.%s' % name
++        if key in self.environ:
++            raise AttributeError("Attribute already defined: %s" % name)
++        self.environ[key] = value
++
++    def __delattr__(self, name):
++        try:
++            del self.environ['bottle.request.ext.%s' % name]
++        except KeyError:
++            raise AttributeError("Attribute not defined: %s" % name)
+ 
+ 
+-def _hkey(s):
+-    return s.title().replace('_', '-')
++def _hkey(key):
++    if '\n' in key or '\r' in key or '\0' in key:
++        raise ValueError("Header names must not contain control characters: %r" % key)
++    return key.title().replace('_', '-')
++
++
++def _hval(value):
++    value = tonat(value)
++    if '\n' in value or '\r' in value or '\0' in value:
++        raise ValueError("Header value must not contain control characters: %r" % value)
++    return value
+ 
+ 
+ class HeaderProperty(object):
+-    def __init__(self, name, reader=None, writer=str, default=''):
++    def __init__(self, name, reader=None, writer=None, default=''):
+         self.name, self.default = name, default
+         self.reader, self.writer = reader, writer
+         self.__doc__ = 'Current value of the %r header.' % name.title()
+ 
+     def __get__(self, obj, _):
+         if obj is None: return self
+-        value = obj.headers.get(self.name, self.default)
++        value = obj.get_header(self.name, self.default)
+         return self.reader(value) if self.reader else value
+ 
+     def __set__(self, obj, value):
+-        obj.headers[self.name] = self.writer(value)
++        obj[self.name] = self.writer(value) if self.writer else value
+ 
+     def __delete__(self, obj):
+-        del obj.headers[self.name]
++        del obj[self.name]
+ 
+ 
+ class BaseResponse(object):
+@@ -1505,8 +1653,8 @@ class BaseResponse(object):
+     # Header blacklist for specific response codes
+     # (rfc2616 section 10.2.3 and 10.3.5)
+     bad_headers = {
+-        204: set(('Content-Type', )),
+-        304: set(('Allow', 'Content-Encoding', 'Content-Language',
++        204: frozenset(('Content-Type', 'Content-Length')),
++        304: frozenset(('Allow', 'Content-Encoding', 'Content-Language',
+                   'Content-Length', 'Content-Range', 'Content-Type',
+                   'Content-Md5', 'Last-Modified'))
+     }
+@@ -1533,8 +1681,10 @@ class BaseResponse(object):
+         copy.status = self.status
+         copy._headers = dict((k, v[:]) for (k, v) in self._headers.items())
+         if self._cookies:
+-            copy._cookies = SimpleCookie()
+-            copy._cookies.load(self._cookies.output(header=''))
++            cookies = copy._cookies = SimpleCookie()
++            for k,v in self._cookies.items():
++                cookies[k] = v.value
++                cookies[k].update(v) # also copy cookie attributes
+         return copy
+ 
+     def __iter__(self):
+@@ -1597,8 +1747,7 @@ class BaseResponse(object):
+         return self._headers[_hkey(name)][-1]
+ 
+     def __setitem__(self, name, value):
+-        self._headers[_hkey(name)] = [value if isinstance(value, unicode) else
+-                                      str(value)]
++        self._headers[_hkey(name)] = [_hval(value)]
+ 
+     def get_header(self, name, default=None):
+         """ Return the value of a previously defined header. If there is no
+@@ -1608,13 +1757,11 @@ class BaseResponse(object):
+     def set_header(self, name, value):
+         """ Create a new response header, replacing any previously defined
+             headers with the same name. """
+-        self._headers[_hkey(name)] = [value if isinstance(value, unicode)
+-                                            else str(value)]
++        self._headers[_hkey(name)] = [_hval(value)]
+ 
+     def add_header(self, name, value):
+         """ Add an additional response header, not removing duplicates. """
+-        self._headers.setdefault(_hkey(name), []).append(
+-            value if isinstance(value, unicode) else str(value))
++        self._headers.setdefault(_hkey(name), []).append(_hval(value))
+ 
+     def iter_headers(self):
+         """ Yield (header, value) tuples, skipping headers that are not
+@@ -1634,12 +1781,10 @@ class BaseResponse(object):
+         out += [(name, val) for (name, vals) in headers for val in vals]
+         if self._cookies:
+             for c in self._cookies.values():
+-                out.append(('Set-Cookie', c.OutputString()))
++                out.append(('Set-Cookie', _hval(c.OutputString())))
+         if py3k:
+-            return [(k, v.encode('utf8').decode('latin1')) for (k, v) in out]
+-        else:
+-            return [(k, v.encode('utf8') if isinstance(v, unicode) else v)
+-                    for (k, v) in out]
++            out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out]
++        return out
+ 
+     content_type = HeaderProperty('Content-Type')
+     content_length = HeaderProperty('Content-Length', reader=int)
+@@ -1655,7 +1800,7 @@ class BaseResponse(object):
+             return self.content_type.split('charset=')[-1].split(';')[0].strip()
+         return default
+ 
+-    def set_cookie(self, name, value, secret=None, **options):
++    def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
+         """ Create a new cookie or replace an old one. If the `secret` parameter is
+             set, create a `Signed Cookie` (described below).
+ 
+@@ -1666,7 +1811,7 @@ class BaseResponse(object):
+             Additionally, this method accepts all RFC 2109 attributes that are
+             supported by :class:`cookie.Morsel`, including:
+ 
+-            :param max_age: maximum age in seconds. (default: None)
++            :param maxage: maximum age in seconds. (default: None)
+             :param expires: a datetime object or UNIX timestamp. (default: None)
+             :param domain: the domain that is allowed to read the cookie.
+               (default: current domain)
+@@ -1674,8 +1819,12 @@ class BaseResponse(object):
+             :param secure: limit the cookie to HTTPS connections (default: off).
+             :param httponly: prevents client-side javascript to read this cookie
+               (default: off, requires Python 2.6 or newer).
++            :param samesite: disables third-party use for a cookie.
++              Allowed attributes: `lax` and `strict`.
++              In strict mode the cookie will never be sent.
++              In lax mode the cookie is only sent with a top-level GET request.
+ 
+-            If neither `expires` nor `max_age` is set (default), the cookie will
++            If neither `expires` nor `maxage` is set (default), the cookie will
+             expire at the end of the browser session (as soon as the browser
+             window is closed).
+ 
+@@ -1683,6 +1832,11 @@ class BaseResponse(object):
+             cryptographically signed to prevent manipulation. Keep in mind that
+             cookies are limited to 4kb in most browsers.
+ 
++            Warning: Pickle is a potentially dangerous format. If an attacker
++            gains access to the secret key, he could forge cookies that execute
++            code on server side if unpickeld. Using pickle is discouraged and
++            support for it will be removed in later versions of bottle.
++
+             Warning: Signed cookies are not encrypted (the client can still see
+             the content) and not copy-protected (the client can restore an old
+             cookie). The main intention is to make pickling and unpickling
+@@ -1691,16 +1845,31 @@ class BaseResponse(object):
+         if not self._cookies:
+             self._cookies = SimpleCookie()
+ 
++        # Monkey-patch Cookie lib to support 'SameSite' parameter
++        # https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
++        Morsel._reserved.setdefault('samesite', 'SameSite')
++
+         if secret:
+-            value = touni(cookie_encode((name, value), secret))
++            if not isinstance(value, basestring):
++                depr(0, 13, "Pickling of arbitrary objects into cookies is "
++                            "deprecated.", "Only store strings in cookies. "
++                            "JSON strings are fine, too.")
++            encoded = base64.b64encode(pickle.dumps([name, value], -1))
++            sig = base64.b64encode(hmac.new(tob(secret), encoded,
++                                            digestmod=digestmod).digest())
++            value = touni(tob('!') + sig + tob('?') + encoded)
+         elif not isinstance(value, basestring):
+-            raise TypeError('Secret key missing for non-string Cookie.')
++            raise TypeError('Secret key required for non-string cookies.')
++
++        # Cookie size plus options must not exceed 4kb.
++        if len(name) + len(value) > 3800:
++            raise ValueError('Content does not fit into a cookie.')
+ 
+-        if len(value) > 4096: raise ValueError('Cookie value to long.')
+         self._cookies[name] = value
+ 
+         for key, value in options.items():
+-            if key == 'max_age':
++            if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13
++                key = 'max-age'
+                 if isinstance(value, timedelta):
+                     value = value.seconds + value.days * 24 * 3600
+             if key == 'expires':
+@@ -1709,7 +1878,13 @@ class BaseResponse(object):
+                 elif isinstance(value, (int, float)):
+                     value = time.gmtime(value)
+                 value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value)
+-            self._cookies[name][key.replace('_', '-')] = value
++            if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13
++                key = 'samesite'
++                if value.lower() not in ('lax', 'strict'):
++                    raise CookieError("Invalid value samesite=%r (expected 'lax' or 'strict')" % (key,))
++            if key in ('secure', 'httponly') and not value:
++                continue
++            self._cookies[name][key] = value
+ 
+     def delete_cookie(self, key, **kwargs):
+         """ Delete a cookie. Be sure to use the same `domain` and `path`
+@@ -1790,10 +1965,10 @@ class HTTPError(HTTPResponse):
+                  status=None,
+                  body=None,
+                  exception=None,
+-                 traceback=None, **options):
++                 traceback=None, **more_headers):
+         self.exception = exception
+         self.traceback = traceback
+-        super(HTTPError, self).__init__(body, status, **options)
++        super(HTTPError, self).__init__(body, status, **more_headers)
+ 
+ ###############################################################################
+ # Plugins ######################################################################
+@@ -1811,15 +1986,27 @@ class JSONPlugin(object):
+     def __init__(self, json_dumps=json_dumps):
+         self.json_dumps = json_dumps
+ 
+-    def apply(self, callback, _):
++    def setup(self, app):
++        app.config._define('json.enable', default=True, validate=bool,
++                          help="Enable or disable automatic dict->json filter.")
++        app.config._define('json.ascii', default=False, validate=bool,
++                          help="Use only 7-bit ASCII characters in output.")
++        app.config._define('json.indent', default=True, validate=bool,
++                          help="Add whitespace to make json more readable.")
++        app.config._define('json.dump_func', default=None,
++                          help="If defined, use this function to transform"
++                               " dict into json. The other options no longer"
++                               " apply.")
++
++    def apply(self, callback, route):
+         dumps = self.json_dumps
+-        if not dumps: return callback
++        if not self.json_dumps: return callback
+ 
+         def wrapper(*a, **ka):
+             try:
+                 rv = callback(*a, **ka)
+-            except HTTPError:
+-                rv = _e()
++            except HTTPResponse as resp:
++                rv = resp
+ 
+             if isinstance(rv, dict):
+                 #Attempt to serialize, raises exception on failure
+@@ -1843,6 +2030,9 @@ class TemplatePlugin(object):
+     name = 'template'
+     api = 2
+ 
++    def setup(self, app):
++        app.tpl = self
++
+     def apply(self, callback, route):
+         conf = route.config.get('template')
+         if isinstance(conf, (tuple, list)) and len(conf) == 2:
+@@ -2038,7 +2228,6 @@ class FormsDict(MultiDict):
+             return super(FormsDict, self).__getattr__(name)
+         return self.getunicode(name, default=default)
+ 
+-
+ class HeaderDict(MultiDict):
+     """ A case-insensitive version of :class:`MultiDict` that defaults to
+         replace the old value instead of appending it. """
+@@ -2057,16 +2246,13 @@ class HeaderDict(MultiDict):
+         return self.dict[_hkey(key)][-1]
+ 
+     def __setitem__(self, key, value):
+-        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
+-                                 str(value)]
++        self.dict[_hkey(key)] = [_hval(value)]
+ 
+     def append(self, key, value):
+-        self.dict.setdefault(_hkey(key), []).append(
+-            value if isinstance(value, unicode) else str(value))
++        self.dict.setdefault(_hkey(key), []).append(_hval(value))
+ 
+     def replace(self, key, value):
+-        self.dict[_hkey(key)] = [value if isinstance(value, unicode) else
+-                                 str(value)]
++        self.dict[_hkey(key)] = [_hval(value)]
+ 
+     def getall(self, key):
+         return self.dict.get(_hkey(key)) or []
+@@ -2075,7 +2261,7 @@ class HeaderDict(MultiDict):
+         return MultiDict.get(self, _hkey(key), default, index)
+ 
+     def filter(self, names):
+-        for name in [_hkey(n) for n in names]:
++        for name in (_hkey(n) for n in names):
+             if name in self.dict:
+                 del self.dict[name]
+ 
+@@ -2139,32 +2325,99 @@ class WSGIHeaderDict(DictMixin):
+     def __contains__(self, key):
+         return self._ekey(key) in self.environ
+ 
++_UNSET = object()
+ 
+ class ConfigDict(dict):
+     """ A dict-like configuration storage with additional support for
+-        namespaces, validators, meta-data, on_change listeners and more.
++        namespaces, validators, meta-data, overlays and more.
++
++        This dict-like class is heavily optimized for read access. All read-only
++        methods as well as item access should be as fast as the built-in dict.
+     """
+ 
+-    __slots__ = ('_meta', '_on_change')
++    __slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__')
+ 
+     def __init__(self):
+         self._meta = {}
+-        self._on_change = lambda name, value: None
++        self._change_listener = []
++        #: Weak references of overlays that need to be kept in sync.
++        self._overlays = []
++        #: Config that is the source for this overlay.
++        self._source = None
++        #: Keys of values copied from the source (values we do not own)
++        self._virtual_keys = set()
++
++    def load_module(self, path, squash=True):
++        """Load values from a Python module.
++
++           Example modue ``config.py``::
++
++                DEBUG = True
++                SQLITE = {
++                    "db": ":memory:"
++                }
+ 
+-    def load_config(self, filename):
++
++           >>> c = ConfigDict()
++           >>> c.load_module('config')
++           {DEBUG: True, 'SQLITE.DB': 'memory'}
++           >>> c.load_module("config", False)
++           {'DEBUG': True, 'SQLITE': {'DB': 'memory'}}
++
++           :param squash: If true (default), dictionary values are assumed to
++                          represent namespaces (see :meth:`load_dict`).
++        """
++        config_obj = load(path)
++        obj = {key: getattr(config_obj, key) for key in dir(config_obj)
++               if key.isupper()}
++
++        if squash:
++            self.load_dict(obj)
++        else:
++            self.update(obj)
++        return self
++
++    def load_config(self, filename, **options):
+         """ Load values from an ``*.ini`` style config file.
+ 
+-            If the config file contains sections, their names are used as
+-            namespaces for the values within. The two special sections
+-            ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix).
++            A configuration file consists of sections, each led by a
++            ``[section]`` header, followed by key/value entries separated by
++            either ``=`` or ``:``. Section names and keys are case-insensitive.
++            Leading and trailing whitespace is removed from keys and values.
++            Values can be omitted, in which case the key/value delimiter may
++            also be left out. Values can also span multiple lines, as long as
++            they are indented deeper than the first line of the value. Commands
++            are prefixed by ``#`` or ``;`` and may only appear on their own on
++            an otherwise empty line.
++
++            Both section and key names may contain dots (``.``) as namespace
++            separators. The actual configuration parameter name is constructed
++            by joining section name and key name together and converting to
++            lower case.
++
++            The special sections ``bottle`` and ``ROOT`` refer to the root
++            namespace and the ``DEFAULT`` section defines default values for all
++            other sections.
++
++            With Python 3, extended string interpolation is enabled.
++
++            :param filename: The path of a config file, or a list of paths.
++            :param options: All keyword parameters are passed to the underlying
++                :class:`python:configparser.ConfigParser` constructor call.
++
+         """
+-        conf = ConfigParser()
++        options.setdefault('allow_no_value', True)
++        if py3k:
++            options.setdefault('interpolation',
++                               configparser.ExtendedInterpolation())
++        conf = configparser.ConfigParser(**options)
+         conf.read(filename)
+         for section in conf.sections():
+-            for key, value in conf.items(section):
+-                if section not in ('DEFAULT', 'bottle'):
++            for key in conf.options(section):
++                value = conf.get(section, key)
++                if section not in ['bottle', 'ROOT']:
+                     key = section + '.' + key
+-                self[key] = value
++                self[key.lower()] = value
+         return self
+ 
+     def load_dict(self, source, namespace=''):
+@@ -2176,7 +2429,7 @@ class ConfigDict(dict):
+             {'some.namespace.key': 'value'}
+         """
+         for key, value in source.items():
+-            if isinstance(key, str):
++            if isinstance(key, basestring):
+                 nskey = (namespace + '.' + key).strip('.')
+                 if isinstance(value, dict):
+                     self.load_dict(value, namespace=nskey)
+@@ -2189,9 +2442,12 @@ class ConfigDict(dict):
+     def update(self, *a, **ka):
+         """ If the first parameter is a string, all keys are prefixed with this
+             namespace. Apart from that it works just as the usual dict.update().
+-            Example: ``update('some.namespace', key='value')`` """
++
++            >>> c = ConfigDict()
++            >>> c.update('some.namespace', key='value')
++        """
+         prefix = ''
+-        if a and isinstance(a[0], str):
++        if a and isinstance(a[0], basestring):
+             prefix = a[0].strip('.') + '.'
+             a = a[1:]
+         for key, value in dict(*a, **ka).items():
+@@ -2203,40 +2459,143 @@ class ConfigDict(dict):
+         return self[key]
+ 
+     def __setitem__(self, key, value):
+-        if not isinstance(key, str):
++        if not isinstance(key, basestring):
+             raise TypeError('Key has type %r (not a string)' % type(key))
++
++        self._virtual_keys.discard(key)
++
+         value = self.meta_get(key, 'filter', lambda x: x)(value)
+         if key in self and self[key] is value:
+             return
++
+         self._on_change(key, value)
+         dict.__setitem__(self, key, value)
+ 
++        for overlay in self._iter_overlays():
++            overlay._set_virtual(key, value)
++
+     def __delitem__(self, key):
+-        self._on_change(key, None)
++        if key not in self:
++            raise KeyError(key)
++        if key in self._virtual_keys:
++            raise KeyError("Virtual keys cannot be deleted: %s" % key)
++
++        if self._source and key in self._source:
++            # Not virtual, but present in source -> Restore virtual value
++            dict.__delitem__(self, key)
++            self._set_virtual(key, self._source[key])
++        else:  # not virtual, not present in source. This is OUR value
++            self._on_change(key, None)
++            dict.__delitem__(self, key)
++            for overlay in self._iter_overlays():
++                overlay._delete_virtual(key)
++
++    def _set_virtual(self, key, value):
++        """ Recursively set or update virtual keys. Do nothing if non-virtual
++            value is present. """
++        if key in self and key not in self._virtual_keys:
++            return  # Do nothing for non-virtual keys.
++
++        self._virtual_keys.add(key)
++        if key in self and self[key] is not value:
++            self._on_change(key, value)
++        dict.__setitem__(self, key, value)
++        for overlay in self._iter_overlays():
++            overlay._set_virtual(key, value)
++
++    def _delete_virtual(self, key):
++        """ Recursively delete virtual entry. Do nothing if key is not virtual.
++        """
++        if key not in self._virtual_keys:
++            return  # Do nothing for non-virtual keys.
++
++        if key in self:
++            self._on_change(key, None)
+         dict.__delitem__(self, key)
++        self._virtual_keys.discard(key)
++        for overlay in self._iter_overlays():
++            overlay._delete_virtual(key)
++
++    def _on_change(self, key, value):
++        for cb in self._change_listener:
++            if cb(self, key, value):
++                return True
++
++    def _add_change_listener(self, func):
++        self._change_listener.append(func)
++        return func
+ 
+     def meta_get(self, key, metafield, default=None):
+         """ Return the value of a meta field for a key. """
+         return self._meta.get(key, {}).get(metafield, default)
+ 
+     def meta_set(self, key, metafield, value):
+-        """ Set the meta field for a key to a new value. This triggers the
+-            on-change handler for existing keys. """
++        """ Set the meta field for a key to a new value. """
+         self._meta.setdefault(key, {})[metafield] = value
+-        if key in self:
+-            self[key] = self[key]
+ 
+     def meta_list(self, key):
+         """ Return an iterable of meta field names defined for a key. """
+         return self._meta.get(key, {}).keys()
+ 
++    def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET):
++        """ (Unstable) Shortcut for plugins to define own config parameters. """
++        if default is not _UNSET:
++            self.setdefault(key, default)
++        if help is not _UNSET:
++            self.meta_set(key, 'help', help)
++        if validate is not _UNSET:
++            self.meta_set(key, 'validate', validate)
++
++    def _iter_overlays(self):
++        for ref in self._overlays:
++            overlay = ref()
++            if overlay is not None:
++                yield overlay
++
++    def _make_overlay(self):
++        """ (Unstable) Create a new overlay that acts like a chained map: Values
++            missing in the overlay are copied from the source map. Both maps
++            share the same meta entries.
++
++            Entries that were copied from the source are called 'virtual'. You
++            can not delete virtual keys, but overwrite them, which turns them
++            into non-virtual entries. Setting keys on an overlay never affects
++            its source, but may affect any number of child overlays.
++
++            Other than collections.ChainMap or most other implementations, this
++            approach does not resolve missing keys on demand, but instead
++            actively copies all values from the source to the overlay and keeps
++            track of virtual and non-virtual keys internally. This removes any
++            lookup-overhead. Read-access is as fast as a build-in dict for both
++            virtual and non-virtual keys.
++
++            Changes are propagated recursively and depth-first. A failing
++            on-change handler in an overlay stops the propagation of virtual
++            values and may result in an partly updated tree. Take extra care
++            here and make sure that on-change handlers never fail.
++
++            Used by Route.config
++        """
++        # Cleanup dead references
++        self._overlays[:] = [ref for ref in self._overlays if ref() is not None]
++
++        overlay = ConfigDict()
++        overlay._meta = self._meta
++        overlay._source = self
++        self._overlays.append(weakref.ref(overlay))
++        for key in self:
++            overlay._set_virtual(key, self[key])
++        return overlay
++
++
++
+ 
+ class AppStack(list):
+     """ A stack-like list. Calling it returns the head of the stack. """
+ 
+     def __call__(self):
+         """ Return the current default application. """
+-        return self[-1]
++        return self.default
+ 
+     def push(self, value=None):
+         """ Add a new :class:`Bottle` instance to the stack """
+@@ -2244,6 +2603,14 @@ class AppStack(list):
+             value = Bottle()
+         self.append(value)
+         return value
++    new_app = push
++
++    @property
++    def default(self):
++        try:
++            return self[-1]
++        except IndexError:
++            return self.push()
+ 
+ 
+ class WSGIFileWrapper(object):
+@@ -2377,6 +2744,10 @@ class FileUpload(object):
+     content_type = HeaderProperty('Content-Type')
+     content_length = HeaderProperty('Content-Length', reader=int, default=-1)
+ 
++    def get_header(self, name, default=None):
++        """ Return the value of a header within the mulripart part. """
++        return self.headers.get(name, default)
++
+     @cached_property
+     def filename(self):
+         """ Name of the file on the client file system, but normalized to ensure
+@@ -2446,41 +2817,61 @@ def redirect(url, code=None):
+     raise res
+ 
+ 
+-def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024):
+-    """ Yield chunks from a range in a file. No chunk is bigger than maxread."""
++def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024, close=False):
++    """ Yield chunks from a range in a file, optionally closing it at the end.
++        No chunk is bigger than maxread. """
+     fp.seek(offset)
+     while bytes > 0:
+         part = fp.read(min(bytes, maxread))
+-        if not part: break
++        if not part:
++            break
+         bytes -= len(part)
+         yield part
++    if close:
++        fp.close()
+ 
+ 
+ def static_file(filename, root,
+-                mimetype='auto',
++                mimetype=True,
+                 download=False,
+-                charset='UTF-8'):
+-    """ Open a file in a safe way and return :exc:`HTTPResponse` with status
+-        code 200, 305, 403 or 404. The ``Content-Type``, ``Content-Encoding``,
+-        ``Content-Length`` and ``Last-Modified`` headers are set if possible.
+-        Special support for ``If-Modified-Since``, ``Range`` and ``HEAD``
+-        requests.
+-
+-        :param filename: Name or path of the file to send.
++                charset='UTF-8',
++                etag=None):
++    """ Open a file in a safe way and return an instance of :exc:`HTTPResponse`
++        that can be sent back to the client.
++
++        :param filename: Name or path of the file to send, relative to ``root``.
+         :param root: Root path for file lookups. Should be an absolute directory
+             path.
+-        :param mimetype: Defines the content-type header (default: guess from
++        :param mimetype: Provide the content-type header (default: guess from
+             file extension)
+         :param download: If True, ask the browser to open a `Save as...` dialog
+             instead of opening the file with the associated program. You can
+             specify a custom filename as a string. If not specified, the
+             original filename is used (default: False).
+-        :param charset: The charset to use for files with a ``text/*``
+-            mime-type. (default: UTF-8)
+-    """
++        :param charset: The charset for files with a ``text/*`` mime-type.
++            (default: UTF-8)
++        :param etag: Provide a pre-computed ETag header. If set to ``False``,
++            ETag handling is disabled. (default: auto-generate ETag header)
++
++        While checking user input is always a good idea, this function provides
++        additional protection against malicious ``filename`` parameters from
++        breaking out of the ``root`` directory and leaking sensitive information
++        to an attacker.
++
++        Read-protected files or files outside of the ``root`` directory are
++        answered with ``403 Access Denied``. Missing files result in a
++        ``404 Not Found`` response. Conditional requests (``If-Modified-Since``,
++        ``If-None-Match``) are answered with ``304 Not Modified`` whenever
++        possible. ``HEAD`` and ``Range`` requests (used by download managers to
++        check or continue partial downloads) are also handled automatically.
+ 
+-    root = os.path.abspath(root) + os.sep
+-    filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
++    """
++    if type(filename) == type(''):
++        root = os.path.join(os.path.abspath(root), '')
++        filename = os.path.abspath(os.path.join(root, filename.strip('/\\')))
++    else:
++        root = os.path.join(os.path.abspath(root), b'')
++        filename = os.path.abspath(os.path.join(root, filename.strip(b'/\\')))
+     headers = dict()
+ 
+     if not filename.startswith(root):
+@@ -2490,47 +2881,60 @@ def static_file(filename, root,
+     if not os.access(filename, os.R_OK):
+         return HTTPError(403, "You do not have permission to access this file.")
+ 
+-    if mimetype == 'auto':
+-        if download and download != True:
++    if mimetype is True:
++        if download and download is not True:
+             mimetype, encoding = mimetypes.guess_type(download)
+         else:
+             mimetype, encoding = mimetypes.guess_type(filename)
+         if encoding: headers['Content-Encoding'] = encoding
+ 
+     if mimetype:
+-        if mimetype[:5] == 'text/' and charset and 'charset' not in mimetype:
++        if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\
++        and charset and 'charset' not in mimetype:
+             mimetype += '; charset=%s' % charset
+         headers['Content-Type'] = mimetype
+ 
+     if download:
+-        download = os.path.basename(filename if download == True else download)
++        download = os.path.basename(filename if download is True else download)
+         headers['Content-Disposition'] = 'attachment; filename="%s"' % download
+ 
+     stats = os.stat(filename)
+     headers['Content-Length'] = clen = stats.st_size
+-    lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
+-    headers['Last-Modified'] = lm
++    headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime,
++                                                      usegmt=True)
++    headers['Date'] = email.utils.formatdate(time.time(), usegmt=True)
++
++    getenv = request.environ.get
++
++    if etag is None:
++        etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime,
++                                   clen, filename)
++        etag = hashlib.sha1(tob(etag)).hexdigest()
+ 
+-    ims = request.environ.get('HTTP_IF_MODIFIED_SINCE')
++    if etag:
++        headers['ETag'] = etag
++        check = getenv('HTTP_IF_NONE_MATCH')
++        if check and check == etag:
++            return HTTPResponse(status=304, **headers)
++
++    ims = getenv('HTTP_IF_MODIFIED_SINCE')
+     if ims:
+         ims = parse_date(ims.split(";")[0].strip())
+     if ims is not None and ims >= int(stats.st_mtime):
+-        headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT",
+-                                        time.gmtime())
+         return HTTPResponse(status=304, **headers)
+ 
+     body = '' if request.method == 'HEAD' else open(filename, 'rb')
+ 
+     headers["Accept-Ranges"] = "bytes"
+-    ranges = request.environ.get('HTTP_RANGE')
+-    if 'HTTP_RANGE' in request.environ:
+-        ranges = list(parse_range_header(request.environ['HTTP_RANGE'], clen))
++    range_header = getenv('HTTP_RANGE')
++    if range_header:
++        ranges = list(parse_range_header(range_header, clen))
+         if not ranges:
+             return HTTPError(416, "Requested Range Not Satisfiable")
+         offset, end = ranges[0]
+         headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen)
+         headers["Content-Length"] = str(end - offset)
+-        if body: body = _file_iter_range(body, offset, end - offset)
++        if body: body = _file_iter_range(body, offset, end - offset, close=True)
+         return HTTPResponse(body, status=206, **headers)
+     return HTTPResponse(body, **headers)
+ 
+@@ -2596,6 +3000,42 @@ def parse_range_header(header, maxlen=0):
+             pass
+ 
+ 
++#: Header tokenizer used by _parse_http_header()
++_hsplit = re.compile('(?:(?:"((?:[^"\\\\]+|\\\\.)*)")|([^;,=]+))([;,=]?)').findall
++
++def _parse_http_header(h):
++    """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values
++        and parameters. For non-standard or broken input, this implementation may return partial results.
++    :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``)
++    :return: List of (value, params) tuples. The second element is a (possibly empty) dict.
++    """
++    values = []
++    if '"' not in h:  # INFO: Fast path without regexp (~2x faster)
++        for value in h.split(','):
++            parts = value.split(';')
++            values.append((parts[0].strip(), {}))
++            for attr in parts[1:]:
++                name, value = attr.split('=', 1)
++                values[-1][1][name.strip()] = value.strip()
++    else:
++        lop, key, attrs = ',', None, {}
++        for quoted, plain, tok in _hsplit(h):
++            value = plain.strip() if plain else quoted.replace('\\"', '"')
++            if lop == ',':
++                attrs = {}
++                values.append((value, attrs))
++            elif lop == ';':
++                if tok == '=':
++                    key = value
++                else:
++                    attrs[value] = ''
++            elif lop == '=' and key:
++                attrs[key] = value
++                key = None
++            lop = tok
++    return values
++
++
+ def _parse_qsl(qs):
+     r = []
+     for pair in qs.replace(';', '&').split('&'):
+@@ -2615,25 +3055,34 @@ def _lscmp(a, b):
+                    for x, y in zip(a, b)) and len(a) == len(b)
+ 
+ 
+-def cookie_encode(data, key):
++def cookie_encode(data, key, digestmod=None):
+     """ Encode and sign a pickle-able object. Return a (byte) string """
++    depr(0, 13, "cookie_encode() will be removed soon.",
++                "Do not use this API directly.")
++    digestmod = digestmod or hashlib.sha256
+     msg = base64.b64encode(pickle.dumps(data, -1))
+-    sig = base64.b64encode(hmac.new(tob(key), msg).digest())
++    sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
+     return tob('!') + sig + tob('?') + msg
+ 
+ 
+-def cookie_decode(data, key):
++def cookie_decode(data, key, digestmod=None):
+     """ Verify and decode an encoded string. Return an object or None."""
++    depr(0, 13, "cookie_decode() will be removed soon.",
++                "Do not use this API directly.")
+     data = tob(data)
+     if cookie_is_encoded(data):
+         sig, msg = data.split(tob('?'), 1)
+-        if _lscmp(sig[1:], base64.b64encode(hmac.new(tob(key), msg).digest())):
++        digestmod = digestmod or hashlib.sha256
++        hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest()
++        if _lscmp(sig[1:], base64.b64encode(hashed)):
+             return pickle.loads(base64.b64decode(msg))
+     return None
+ 
+ 
+ def cookie_is_encoded(data):
+     """ Return True if the argument looks like a encoded cookie."""
++    depr(0, 13, "cookie_is_encoded() will be removed soon.",
++                "Do not use this API directly.")
+     return bool(data.startswith(tob('!')) and tob('?') in data)
+ 
+ 
+@@ -2750,6 +3199,9 @@ url       = make_default_app_wrapper('get_url')
+ # Server Adapter ###############################################################
+ ###############################################################################
+ 
++# Before you edit or add a server adapter, please read:
++# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870
++# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341
+ 
+ class ServerAdapter(object):
+     quiet = False
+@@ -2823,7 +3275,11 @@ class WSGIRefServer(ServerAdapter):
+ 
+ class CherryPyServer(ServerAdapter):
+     def run(self, handler):  # pragma: no cover
+-        from cherrypy import wsgiserver
++        depr(0, 13, "The wsgi server part of cherrypy was split into a new "
++                    "project called 'cheroot'.", "Use the 'cheroot' server "
++                    "adapter instead of cherrypy.")
++        from cherrypy import wsgiserver # This will fail for CherryPy >= 9
++
+         self.options['bind_addr'] = (self.host, self.port)
+         self.options['wsgi_app'] = handler
+ 
+@@ -2846,10 +3302,29 @@ class CherryPyServer(ServerAdapter):
+             server.stop()
+ 
+ 
++class CherootServer(ServerAdapter):
++    def run(self, handler): # pragma: no cover
++        from cheroot import wsgi
++        from cheroot.ssl import builtin
++        self.options['bind_addr'] = (self.host, self.port)
++        self.options['wsgi_app'] = handler
++        certfile = self.options.pop('certfile', None)
++        keyfile = self.options.pop('keyfile', None)
++        chainfile = self.options.pop('chainfile', None)
++        server = wsgi.Server(**self.options)
++        if certfile and keyfile:
++            server.ssl_adapter = builtin.BuiltinSSLAdapter(
++                    certfile, keyfile, chainfile)
++        try:
++            server.start()
++        finally:
++            server.stop()
++
++
+ class WaitressServer(ServerAdapter):
+     def run(self, handler):
+         from waitress import serve
+-        serve(handler, host=self.host, port=self.port, _quiet=self.quiet)
++        serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options)
+ 
+ 
+ class PasteServer(ServerAdapter):
+@@ -2910,6 +3385,8 @@ class AppEngineServer(ServerAdapter):
+     quiet = True
+ 
+     def run(self, handler):
++        depr(0, 13, "AppEngineServer no longer required",
++             "Configure your application directly in your app.yaml")
+         from google.appengine.ext.webapp import util
+         # A main() function in the handler script enables 'App Caching'.
+         # Lets makes sure it is there. This _really_ improves performance.
+@@ -2947,33 +3424,24 @@ class DieselServer(ServerAdapter):
+ class GeventServer(ServerAdapter):
+     """ Untested. Options:
+ 
+-        * `fast` (default: False) uses libevent's http server, but has some
+-          issues: No streaming, no pipelining, no SSL.
+         * See gevent.wsgi.WSGIServer() documentation for more options.
+     """
+ 
+     def run(self, handler):
+-        from gevent import wsgi, pywsgi, local
++        from gevent import pywsgi, local
+         if not isinstance(threading.local(), local.local):
+             msg = "Bottle requires gevent.monkey.patch_all() (before import)"
+             raise RuntimeError(msg)
+-        if not self.options.pop('fast', None): wsgi = pywsgi
+-        self.options['log'] = None if self.quiet else 'default'
++        if self.quiet:
++            self.options['log'] = None
+         address = (self.host, self.port)
+-        server = wsgi.WSGIServer(address, handler, **self.options)
++        server = pywsgi.WSGIServer(address, handler, **self.options)
+         if 'BOTTLE_CHILD' in os.environ:
+             import signal
+             signal.signal(signal.SIGINT, lambda s, f: server.stop())
+         server.serve_forever()
+ 
+ 
+-class GeventSocketIOServer(ServerAdapter):
+-    def run(self, handler):
+-        from socketio import server
+-        address = (self.host, self.port)
+-        server.SocketIOServer(address, handler, **self.options).serve_forever()
+-
+-
+ class GunicornServer(ServerAdapter):
+     """ Untested. See http://gunicorn.org/configure.html for options. """
+ 
+@@ -3039,17 +3507,25 @@ class BjoernServer(ServerAdapter):
+         from bjoern import run
+         run(handler, self.host, self.port)
+ 
++class AsyncioServerAdapter(ServerAdapter):
++    """ Extend ServerAdapter for adding custom event loop """
++    def get_event_loop(self):
++        pass
+ 
+-class AiohttpServer(ServerAdapter):
+-    """ Untested. 
++class AiohttpServer(AsyncioServerAdapter):
++    """ Untested.
+         aiohttp
+         https://pypi.python.org/pypi/aiohttp/
+     """
+ 
++    def get_event_loop(self):
++        import asyncio
++        return asyncio.new_event_loop()
++
+     def run(self, handler):
+         import asyncio
+         from aiohttp.wsgi import WSGIServerHttpProtocol
+-        self.loop = asyncio.new_event_loop()
++        self.loop = self.get_event_loop()
+         asyncio.set_event_loop(self.loop)
+ 
+         protocol_factory = lambda: WSGIServerHttpProtocol(
+@@ -3069,11 +3545,18 @@ class AiohttpServer(ServerAdapter):
+         except KeyboardInterrupt:
+             self.loop.stop()
+ 
++class AiohttpUVLoopServer(AiohttpServer):
++    """uvloop
++       https://github.com/MagicStack/uvloop
++    """
++    def get_event_loop(self):
++        import uvloop
++        return uvloop.new_event_loop()
+ 
+ class AutoServer(ServerAdapter):
+     """ Untested. """
+     adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer,
+-                WSGIRefServer]
++                CherootServer, WSGIRefServer]
+ 
+     def run(self, handler):
+         for sa in self.adapters:
+@@ -3089,6 +3572,7 @@ server_names = {
+     'wsgiref': WSGIRefServer,
+     'waitress': WaitressServer,
+     'cherrypy': CherryPyServer,
++    'cheroot': CherootServer,
+     'paste': PasteServer,
+     'fapws3': FapwsServer,
+     'tornado': TornadoServer,
+@@ -3099,10 +3583,10 @@ server_names = {
+     'gunicorn': GunicornServer,
+     'eventlet': EventletServer,
+     'gevent': GeventServer,
+-    'geventSocketIO': GeventSocketIOServer,
+     'rocket': RocketServer,
+     'bjoern': BjoernServer,
+     'aiohttp': AiohttpServer,
++    'uvloop': AiohttpUVLoopServer,
+     'auto': AutoServer,
+ }
+ 
+@@ -3157,7 +3641,8 @@ def run(app=None,
+         reloader=False,
+         quiet=False,
+         plugins=None,
+-        debug=None, **kargs):
++        debug=None,
++        config=None, **kargs):
+     """ Start a server instance. This method blocks until the server terminates.
+ 
+         :param app: WSGI application or target string supported by
+@@ -3213,6 +3698,9 @@ def run(app=None,
+                 plugin = load(plugin)
+             app.install(plugin)
+ 
++        if config:
++            app.config.update(config)
++
+         if server in server_names:
+             server = server_names.get(server)
+         if isinstance(server, basestring):
+@@ -3253,7 +3741,7 @@ def run(app=None,
+ 
+ class FileCheckerThread(threading.Thread):
+     """ Interrupt main-thread as soon as a changed module file is detected,
+-        the lockfile gets deleted or gets to old. """
++        the lockfile gets deleted or gets too old. """
+ 
+     def __init__(self, lockfile, interval):
+         threading.Thread.__init__(self)
+@@ -3268,7 +3756,7 @@ class FileCheckerThread(threading.Thread):
+         files = dict()
+ 
+         for module in list(sys.modules.values()):
+-            path = getattr(module, '__file__', '')
++            path = getattr(module, '__file__', '') or ''
+             if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
+             if path and exists(path): files[path] = mtime(path)
+ 
+@@ -3297,9 +3785,8 @@ class FileCheckerThread(threading.Thread):
+ ###############################################################################
+ 
+ 
+-class TemplateError(HTTPError):
+-    def __init__(self, message):
+-        HTTPError.__init__(self, 500, message)
++class TemplateError(BottleException):
++    pass
+ 
+ 
+ class BaseTemplate(object):
+@@ -3343,13 +3830,11 @@ class BaseTemplate(object):
+         """ Search name in all directories specified in lookup.
+         First without, then with common extensions. Return first hit. """
+         if not lookup:
+-            depr('The template lookup path list should not be empty.',
+-                 True)  #0.12
+-            lookup = ['.']
++            raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.")
+ 
+-        if os.path.isabs(name) and os.path.isfile(name):
+-            depr('Absolute template path names are deprecated.', True)  #0.12
+-            return os.path.abspath(name)
++        if os.path.isabs(name):
++            raise depr(0, 12, "Use of absolute path for template name.",
++                       "Refer to templates with names or paths relative to the lookup path.")
+ 
+         for spath in lookup:
+             spath = os.path.abspath(spath) + os.sep
+@@ -3439,7 +3924,7 @@ class Jinja2Template(BaseTemplate):
+         if self.source:
+             self.tpl = self.env.from_string(self.source)
+         else:
+-            self.tpl = self.env.get_template(self.filename)
++            self.tpl = self.env.get_template(self.name)
+ 
+     def render(self, *args, **kwargs):
+         for dictarg in args:
+@@ -3449,10 +3934,13 @@ class Jinja2Template(BaseTemplate):
+         return self.tpl.render(**_defaults)
+ 
+     def loader(self, name):
+-        fname = self.search(name, self.lookup)
++        if name == self.filename:
++            fname = name
++        else:
++            fname = self.search(name, self.lookup)
+         if not fname: return
+         with open(fname, "rb") as f:
+-            return f.read().decode(self.encoding)
++            return (f.read().decode(self.encoding), fname, lambda: False)
+ 
+ 
+ class SimpleTemplate(BaseTemplate):
+@@ -3481,8 +3969,7 @@ class SimpleTemplate(BaseTemplate):
+         try:
+             source, encoding = touni(source), 'utf8'
+         except UnicodeError:
+-            depr('Template encodings other than utf8 are not supported.')  #0.11
+-            source, encoding = touni(source, 'latin1'), 'latin1'
++            raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.')
+         parser = StplParser(source, encoding=encoding, syntax=self.syntax)
+         code = parser.translate()
+         self.encoding = parser.encoding
+@@ -3495,7 +3982,7 @@ class SimpleTemplate(BaseTemplate):
+         env = _env.copy()
+         env.update(kwargs)
+         if _name not in self.cache:
+-            self.cache[_name] = self.__class__(name=_name, lookup=self.lookup)
++            self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax)
+         return self.cache[_name].execute(env['_stdout'], env)
+ 
+     def execute(self, _stdout, kwargs):
+@@ -3513,7 +4000,7 @@ class SimpleTemplate(BaseTemplate):
+             'setdefault': env.setdefault,
+             'defined': env.__contains__
+         })
+-        eval(self.co, env)
++        exec(self.co, env)
+         if env.get('_rebase'):
+             subtpl, rargs = env.pop('_rebase')
+             rargs['base'] = ''.join(_stdout)  #copy stdout
+@@ -3533,7 +4020,6 @@ class SimpleTemplate(BaseTemplate):
+ 
+ 
+ class StplSyntaxError(TemplateError):
+-
+     pass
+ 
+ 
+@@ -3544,7 +4030,7 @@ class StplParser(object):
+     # This huge pile of voodoo magic splits python code into 8 different tokens.
+     # We use the verbose (?x) regex mode to make this more manageable
+ 
+-    _re_tok = _re_inl = r'''((?mx)         # verbose and dot-matches-newline mode
++    _re_tok = _re_inl = r'''(
+         [urbURB]*
+         (?:  ''(?!')
+             |""(?!")
+@@ -3586,6 +4072,12 @@ class StplParser(object):
+     # Match inline statements (may contain python strings)
+     _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl
+ 
++    # add the flag in front of the regexp to avoid Deprecation warning (see Issue #949)
++    # verbose and dot-matches-newline mode
++    _re_tok = '(?mx)' + _re_tok
++    _re_inl = '(?mx)' + _re_inl
++
++
+     default_syntax = '<% %> % {{ }}'
+ 
+     def __init__(self, source, syntax=None, encoding='utf8'):
+@@ -3603,7 +4095,7 @@ class StplParser(object):
+     def set_syntax(self, syntax):
+         self._syntax = syntax
+         self._tokens = syntax.split()
+-        if not syntax in self._re_cache:
++        if syntax not in self._re_cache:
+             names = 'block_start block_close line_start inline_start inline_end'
+             etokens = map(re.escape, self._tokens)
+             pattern_vars = dict(zip(names.split(), etokens))
+@@ -3669,15 +4161,18 @@ class StplParser(object):
+                     self.paren_depth -= 1
+                 code_line += _pc
+             elif _blk1:  # Start-block keyword (if/for/while/def/try/...)
+-                code_line, self.indent_mod = _blk1, -1
++                code_line = _blk1
+                 self.indent += 1
++                self.indent_mod -= 1
+             elif _blk2:  # Continue-block keyword (else/elif/except/...)
+-                code_line, self.indent_mod = _blk2, -1
+-            elif _end:  # The non-standard 'end'-keyword (ends a block)
+-                self.indent -= 1
++                code_line = _blk2
++                self.indent_mod -= 1
+             elif _cend:  # The end-code-block template token (usually '%>')
+                 if multiline: multiline = False
+                 else: code_line += _cend
++            elif _end:
++                self.indent -= 1
++                self.indent_mod += 1
+             else:  # \n
+                 self.write_code(code_line.strip(), comment)
+                 self.lineno += 1
+@@ -3727,6 +4222,8 @@ def template(*args, **kwargs):
+     or directly (as keyword arguments).
+     """
+     tpl = args[0] if args else None
++    for dictarg in args[1:]:
++        kwargs.update(dictarg)
+     adapter = kwargs.pop('template_adapter', SimpleTemplate)
+     lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)
+     tplid = (id(lookup), tpl)
+@@ -3741,8 +4238,6 @@ def template(*args, **kwargs):
+             TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
+     if not TEMPLATES[tplid]:
+         abort(500, 'Template (%s) not found' % tpl)
+-    for dictarg in args[1:]:
+-        kwargs.update(dictarg)
+     return TEMPLATES[tplid].render(kwargs)
+ 
+ 
+@@ -3795,11 +4290,12 @@ DEBUG = False
+ NORUN = False  # If set, run() does nothing. Used by load_app()
+ 
+ #: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found')
+-HTTP_CODES = httplib.responses
++HTTP_CODES = httplib.responses.copy()
+ HTTP_CODES[418] = "I'm a teapot"  # RFC 2324
+ HTTP_CODES[428] = "Precondition Required"
+ HTTP_CODES[429] = "Too Many Requests"
+ HTTP_CODES[431] = "Request Header Fields Too Large"
++HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725
+ HTTP_CODES[511] = "Network Authentication Required"
+ _HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v))
+                           for (k, v) in HTTP_CODES.items())
+@@ -3826,7 +4322,12 @@ ERROR_PAGE_TEMPLATE = """
+             <pre>{{e.body}}</pre>
+             %%if DEBUG and e.exception:
+               <h2>Exception:</h2>
+-              <pre>{{repr(e.exception)}}</pre>
++              %%try:
++                %%exc = repr(e.exception)
++              %%except:
++                %%exc = '<unprintable %%s object>' %% type(e.exception).__name__
++              %%end
++              <pre>{{exc}}</pre>
+             %%end
+             %%if DEBUG and e.traceback:
+               <h2>Traceback:</h2>
+@@ -3842,7 +4343,7 @@ ERROR_PAGE_TEMPLATE = """
+ 
+ #: A thread-safe instance of :class:`LocalRequest`. If accessed from within a
+ #: request callback, this instance always refers to the *current* request
+-#: (even on a multithreaded server).
++#: (even on a multi-threaded server).
+ request = LocalRequest()
+ 
+ #: A thread-safe instance of :class:`LocalResponse`. It is used to change the
+@@ -3852,40 +4353,69 @@ response = LocalResponse()
+ #: A thread-safe namespace. Not used by Bottle.
+ local = threading.local()
+ 
+-# Initialize app stack (create first empty Bottle app)
++# Initialize app stack (create first empty Bottle app now deferred until needed)
+ # BC: 0.6.4 and needed for run()
+-app = default_app = AppStack()
+-app.push()
++apps = app = default_app = AppStack()
+ 
+ #: A virtual package that redirects import statements.
+ #: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`.
+ ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else
+                       __name__ + ".ext", 'bottle_%s').module
+ 
+-if __name__ == '__main__':
+-    opt, args, parser = _cmd_options, _cmd_args, _cmd_parser
+-    if opt.version:
+-        _stdout('Bottle %s\n' % __version__)
+-        sys.exit(0)
+-    if not args:
++
++def _main(argv):  # pragma: no coverage
++    args, parser = _cli_parse(argv)
++
++    def _cli_error(cli_msg):
+         parser.print_help()
+-        _stderr('\nError: No application entry point specified.\n')
++        _stderr('\nError: %s\n' % cli_msg)
+         sys.exit(1)
+ 
++    if args.version:
++        _stdout('Bottle %s\n' % __version__)
++        sys.exit(0)
++    if not args.app:
++        _cli_error("No application entry point specified.")
++
+     sys.path.insert(0, '.')
+     sys.modules.setdefault('bottle', sys.modules['__main__'])
+ 
+-    host, port = (opt.bind or 'localhost'), 8080
++    host, port = (args.bind or 'localhost'), 8080
+     if ':' in host and host.rfind(']') < host.rfind(':'):
+         host, port = host.rsplit(':', 1)
+     host = host.strip('[]')
+ 
+-    run(args[0],
++    config = ConfigDict()
++
++    for cfile in args.conf or []:
++        try:
++            if cfile.endswith('.json'):
++                with open(cfile, 'rb') as fp:
++                    config.load_dict(json_loads(fp.read()))
++            else:
++                config.load_config(cfile)
++        except configparser.Error as parse_error:
++            _cli_error(parse_error)
++        except IOError:
++            _cli_error("Unable to read config file %r" % cfile)
++        except (UnicodeError, TypeError, ValueError) as error:
++            _cli_error("Unable to parse config file %r: %s" % (cfile, error))
++
++    for cval in args.param or []:
++        if '=' in cval:
++            config.update((cval.split('=', 1),))
++        else:
++            config[cval] = True
++
++    run(args.app,
+         host=host,
+         port=int(port),
+-        server=opt.server,
+-        reloader=opt.reload,
+-        plugins=opt.plugin,
+-        debug=opt.debug)
++        server=args.server,
++        reloader=args.reload,
++        plugins=args.plugin,
++        debug=args.debug,
++        config=config)
++
+ 
+-# THE END
++if __name__ == '__main__':  # pragma: no coverage
++    _main(sys.argv)
--- upmpdcli-1.9.17.orig/debian/patches/python-shebang-use-env.diff
+++ upmpdcli-1.9.17/debian/patches/python-shebang-use-env.diff
@@ -0,0 +1,146 @@
+commit d5d9ff64b02dcc4846b5638903870609fd337f88
+Author: Jean-Francois Dockes <jf@dockes.org>
+Date:   Wed Jul 5 16:46:24 2023 +0200
+
+    Change shebang line of python scripts to /usr/bin/env python3
+
+diff --git a/rdpl2stream/fetchStream.py b/rdpl2stream/fetchStream.py
+index d15dcb2..a077be6 100755
+--- a/rdpl2stream/fetchStream.py
++++ b/rdpl2stream/fetchStream.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # Main program for fetching a stream URL
+ 
+diff --git a/src/mediaserver/cdplugins/bbc/bbc-app.py b/src/mediaserver/cdplugins/bbc/bbc-app.py
+index 5e22d09..9f44c5b 100755
+--- a/src/mediaserver/cdplugins/bbc/bbc-app.py
++++ b/src/mediaserver/cdplugins/bbc/bbc-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # Almost completely copied from the Kodi BBC addon, which has an MIT license, retained for the modifications:
+ # https://github.com/jonjomckay/kodi-addon-bbcsounds
+diff --git a/src/mediaserver/cdplugins/deezer/deezer-app.py b/src/mediaserver/cdplugins/deezer/deezer-app.py
+index fbdb43d..74b862e 100755
+--- a/src/mediaserver/cdplugins/deezer/deezer-app.py
++++ b/src/mediaserver/cdplugins/deezer/deezer-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # A lot of code copied from the Kodi Tidal addon which is:
+ # Copyright (C) 2014 Thomas Amland
+diff --git a/src/mediaserver/cdplugins/hra/hra-app.py b/src/mediaserver/cdplugins/hra/hra-app.py
+index 0b2a643..cc7bdba 100755
+--- a/src/mediaserver/cdplugins/hra/hra-app.py
++++ b/src/mediaserver/cdplugins/hra/hra-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # A lot of code copied from the Kodi Tidal addon which is:
+ # Copyright (C) 2014 Thomas Amland
+diff --git a/src/mediaserver/cdplugins/qobuz/qobuz-app.py b/src/mediaserver/cdplugins/qobuz/qobuz-app.py
+index cfc5b93..2611142 100755
+--- a/src/mediaserver/cdplugins/qobuz/qobuz-app.py
++++ b/src/mediaserver/cdplugins/qobuz/qobuz-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # A lot of code copied from the Kodi Tidal addon which is:
+ # Copyright (C) 2014 Thomas Amland
+diff --git a/src/mediaserver/cdplugins/radio-browser/radio-browser-app.py b/src/mediaserver/cdplugins/radio-browser/radio-browser-app.py
+index 21ed7db..0962d46 100755
+--- a/src/mediaserver/cdplugins/radio-browser/radio-browser-app.py
++++ b/src/mediaserver/cdplugins/radio-browser/radio-browser-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # Copyright (C) 2023 J.F.Dockes
+ #
+diff --git a/src/mediaserver/cdplugins/subsonic/subsonic-app.py b/src/mediaserver/cdplugins/subsonic/subsonic-app.py
+index 825de51..184fdd2 100755
+--- a/src/mediaserver/cdplugins/subsonic/subsonic-app.py
++++ b/src/mediaserver/cdplugins/subsonic/subsonic-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ # Copyright (C) 2023 Giovanni Fulco
+ #
+ # This program is free software: you can redistribute it and/or modify
+diff --git a/src/mediaserver/cdplugins/tidal/tidal-app.py b/src/mediaserver/cdplugins/tidal/tidal-app.py
+index a1f9db0..6ef7c6c 100755
+--- a/src/mediaserver/cdplugins/tidal/tidal-app.py
++++ b/src/mediaserver/cdplugins/tidal/tidal-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # A lot of code copied from the Kodi Tidal addon which is:
+ # Copyright (C) 2014 Thomas Amland
+diff --git a/src/mediaserver/cdplugins/upradios/upradios-app.py b/src/mediaserver/cdplugins/upradios/upradios-app.py
+index 2794f3e..a1f7560 100755
+--- a/src/mediaserver/cdplugins/upradios/upradios-app.py
++++ b/src/mediaserver/cdplugins/upradios/upradios-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # Copyright (C) 2021 J.F.Dockes
+ #
+diff --git a/src/mediaserver/cdplugins/uprcl/minimtagfixer.py b/src/mediaserver/cdplugins/uprcl/minimtagfixer.py
+index 0d26277..5f74b7c 100755
+--- a/src/mediaserver/cdplugins/uprcl/minimtagfixer.py
++++ b/src/mediaserver/cdplugins/uprcl/minimtagfixer.py
+@@ -1,5 +1,3 @@
+-#!/usr/bin/python3
+-#
+ # Copyright (C) 2017-2019 J.F.Dockes
+ #   This program is free software; you can redistribute it and/or modify
+ #   it under the terms of the GNU Lesser General Public License as published by
+diff --git a/src/mediaserver/cdplugins/uprcl/uprcl-app.py b/src/mediaserver/cdplugins/uprcl/uprcl-app.py
+index 2488769..3e117fa 100755
+--- a/src/mediaserver/cdplugins/uprcl/uprcl-app.py
++++ b/src/mediaserver/cdplugins/uprcl/uprcl-app.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ #
+ # Copyright (C) 2017 J.F.Dockes
+ #
+diff --git a/src/radio_scripts/radio-france-meta.py b/src/radio_scripts/radio-france-meta.py
+index 09a90e2..1bc4a1f 100644
+--- a/src/radio_scripts/radio-france-meta.py
++++ b/src/radio_scripts/radio-france-meta.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ from __future__ import print_function
+ 
+ '''
+diff --git a/src/radio_scripts/radio-paradise-get-flac.py b/src/radio_scripts/radio-paradise-get-flac.py
+index a8654d5..d13f751 100755
+--- a/src/radio_scripts/radio-paradise-get-flac.py
++++ b/src/radio_scripts/radio-paradise-get-flac.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ # Checked to run with either Python 2 or 3
+ #
+ # Copyright (C) 2017-2018 J.F.Dockes
+diff --git a/src/radio_scripts/radio-paradise-meta.py b/src/radio_scripts/radio-paradise-meta.py
+index 9b82f27..2cbf439 100755
+--- a/src/radio_scripts/radio-paradise-meta.py
++++ b/src/radio_scripts/radio-paradise-meta.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/python3
++#!/usr/bin/env python3
+ from __future__ import print_function
+ 
+ # Metadata getter for Radio Paradiase stations
--- upmpdcli-1.9.17.orig/debian/patches/series-jessie
+++ upmpdcli-1.9.17/debian/patches/series-jessie
@@ -0,0 +1,1 @@
+gcc-49-initlist-fix.diff
--- upmpdcli-1.9.17.orig/debian/postinst
+++ upmpdcli-1.9.17/debian/postinst
@@ -0,0 +1,27 @@
+#! /bin/sh
+# postinst for upmpdcli
+
+set -e
+
+if [ "$1" = configure ]; then
+    if ! getent passwd upmpdcli > /dev/null; then
+	adduser --disabled-password --quiet --system --group \
+	    --home /nonexistent --no-create-home --shell /bin/false upmpdcli
+    fi
+
+    # Check if group upmpdcli exists: old versions did not use
+    # the --group option to adduser so that, when updating, we may
+    # have to create it separately
+    if ! getent group upmpdcli > /dev/null; then
+        addgroup --system upmpdcli
+        usermod -g upmpdcli upmpdcli
+    fi
+
+    if ! groups upmpdcli | cut -d' ' -f 3- | grep -q -w audio ; then
+        usermod -a -G audio upmpdcli
+    fi
+fi
+
+#DEBHELPER#
+
+exit 0
--- upmpdcli-1.9.17.orig/debian/rules
+++ upmpdcli-1.9.17/debian/rules
@@ -0,0 +1,10 @@
+#!/usr/bin/make -f
+# -*- makefile -*-
+
+#export DH_VERBOSE=1
+
+%:
+	dh $@ 
+
+override_dh_auto_configure:
+	dh_auto_configure -- -Dconfgui=true -Dscctl=true
--- upmpdcli-1.9.17.orig/debian/scweb.default
+++ upmpdcli-1.9.17/debian/scweb.default
@@ -0,0 +1,12 @@
+# Values for the upmpdcli Songcast Receiver control web interface
+
+# Actually start ? Not by default
+#SCWEB_DORUN=yes
+
+SCWEB_USER=upmpdcli
+
+# Address can 0.0.0.0 for all interfaces or empty for localhost only (default)
+#SCWEB_ADDRESS=
+
+# Choose port
+SCWEB_PORT=8680
--- upmpdcli-1.9.17.orig/debian/scweb.install
+++ upmpdcli-1.9.17/debian/scweb.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/web
--- upmpdcli-1.9.17.orig/debian/source/format
+++ upmpdcli-1.9.17/debian/source/format
@@ -0,0 +1,1 @@
+3.0 (quilt)
--- upmpdcli-1.9.17.orig/debian/upmpdcli-bbc.install
+++ upmpdcli-1.9.17/debian/upmpdcli-bbc.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/bbc
--- upmpdcli-1.9.17.orig/debian/upmpdcli-config.install
+++ upmpdcli-1.9.17/debian/upmpdcli-config.install
@@ -0,0 +1,1 @@
+usr/bin/upmpdcli-config
--- upmpdcli-1.9.17.orig/debian/upmpdcli-hra.install
+++ upmpdcli-1.9.17/debian/upmpdcli-hra.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/hra
--- upmpdcli-1.9.17.orig/debian/upmpdcli-mother-earth-radio.install
+++ upmpdcli-1.9.17/debian/upmpdcli-mother-earth-radio.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/mother-earth-radio
--- upmpdcli-1.9.17.orig/debian/upmpdcli-qobuz.install
+++ upmpdcli-1.9.17/debian/upmpdcli-qobuz.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/qobuz
--- upmpdcli-1.9.17.orig/debian/upmpdcli-radio-browser.install
+++ upmpdcli-1.9.17/debian/upmpdcli-radio-browser.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/radio-browser
--- upmpdcli-1.9.17.orig/debian/upmpdcli-radio-paradise.install
+++ upmpdcli-1.9.17/debian/upmpdcli-radio-paradise.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/radio-paradise
--- upmpdcli-1.9.17.orig/debian/upmpdcli-radios.install
+++ upmpdcli-1.9.17/debian/upmpdcli-radios.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/upradios
--- upmpdcli-1.9.17.orig/debian/upmpdcli-subsonic.install
+++ upmpdcli-1.9.17/debian/upmpdcli-subsonic.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/subsonic
--- upmpdcli-1.9.17.orig/debian/upmpdcli-tidal.install
+++ upmpdcli-1.9.17/debian/upmpdcli-tidal.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/tidal
--- upmpdcli-1.9.17.orig/debian/upmpdcli-uprcl.install
+++ upmpdcli-1.9.17/debian/upmpdcli-uprcl.install
@@ -0,0 +1,1 @@
+usr/share/upmpdcli/cdplugins/uprcl
--- upmpdcli-1.9.17.orig/debian/upmpdcli.init
+++ upmpdcli-1.9.17/debian/upmpdcli.init
@@ -0,0 +1,149 @@
+#!/bin/sh
+### BEGIN INIT INFO
+# Provides:          upmpdcli
+# Required-Start:    $network $local_fs $remote_fs 
+# Required-Stop:     $remote_fs
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: UPnP Renderer front-end to MPD
+# Description: upmpdcli is as an UPnP Media Renderer front-end 
+#    to MPD, and enables you to control it with any UPnP controller
+#    running, for example, on an Android tablet. It uses the MPD
+#    instance to actually play the tracks. A typical configuration
+#    would have for example, MPD running on a Raspberry PI, with
+#    upmpdcli on the same host or any other Linux PC on the network.
+### END INIT INFO
+
+# Author: Jean-Francois Dockes <jf@dockes.org>
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC=upmpdcli             # Introduce a short description here
+NAME=upmpdcli             # Introduce the short server's name here
+DAEMON=/usr/bin/upmpdcli # Introduce the server's location here
+DAEMON_ARGS="-D -c /etc/upmpdcli.conf"    # Arguments to run the daemon with
+PIDFILE=/var/run/$NAME.pid
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -x $DAEMON ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+	# Return
+	#   0 if daemon has been started
+	#   1 if daemon was already running
+	#   2 if daemon could not be started
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+		|| return 1
+	start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+		$DAEMON_ARGS \
+		|| return 2
+	# Add code here, if necessary, that waits for the process to be ready
+	# to handle requests from services started subsequently which depend
+	# on this one.  As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+	# Return
+	#   0 if daemon has been stopped
+	#   1 if daemon was already stopped
+	#   2 if daemon could not be stopped
+	#   other if a failure occurred
+	start-stop-daemon --stop --quiet --retry=TERM/5/KILL/5 --pidfile $PIDFILE --name $NAME
+	RETVAL="$?"
+	[ "$RETVAL" = 2 ] && return 2
+	# Many daemons don't delete their pidfiles when they exit.
+	rm -f $PIDFILE
+	return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+#do_reload() {
+	#
+	# If the daemon can reload its configuration without
+	# restarting (for example, when it is sent a SIGHUP),
+	# then implement that here.
+	#
+#	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+#	return 0
+#}
+
+case "$1" in
+  start)
+    [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME"
+    do_start
+    case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+  ;;
+  stop)
+	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+	do_stop
+	case "$?" in
+		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+	esac
+	;;
+  status)
+       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
+       ;;
+  #reload|force-reload)
+	#
+	# If do_reload() is not implemented then leave this commented out
+	# and leave 'force-reload' as an alias for 'restart'.
+	#
+	#log_daemon_msg "Reloading $DESC" "$NAME"
+	#do_reload
+	#log_end_msg $?
+	#;;
+  restart|force-reload)
+	#
+	# If the "reload" option is implemented then remove the
+	# 'force-reload' alias
+	#
+	log_daemon_msg "Restarting $DESC" "$NAME"
+	do_stop
+	case "$?" in
+	  0|1)
+		do_start
+		case "$?" in
+			0) log_end_msg 0 ;;
+			1) log_end_msg 1 ;; # Old process is still running
+			*) log_end_msg 1 ;; # Failed to start
+		esac
+		;;
+	  *)
+	  	# Failed to stop
+		log_end_msg 1
+		;;
+	esac
+	;;
+  *)
+	#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+	exit 3
+	;;
+esac
+
+:
--- upmpdcli-1.9.17.orig/debian/upmpdcli.install
+++ upmpdcli-1.9.17/debian/upmpdcli.install
@@ -0,0 +1,29 @@
+etc
+usr/bin/scctl
+usr/bin/upmpdcli
+usr/share/man/man1/upmpdcli.1
+usr/share/man/man5/upmpdcli.conf.5
+usr/share/upmpdcli/AVTransport.xml
+usr/share/upmpdcli/ConnectionManager.xml
+usr/share/upmpdcli/ContentDirectory.xml
+usr/share/upmpdcli/MS-description.xml
+usr/share/upmpdcli/OHCredentials.xml
+usr/share/upmpdcli/OHInfo.xml
+usr/share/upmpdcli/OHPlaylist.xml
+usr/share/upmpdcli/OHProduct.xml
+usr/share/upmpdcli/OHRadio.xml
+usr/share/upmpdcli/OHReceiver.xml
+usr/share/upmpdcli/OHTime.xml
+usr/share/upmpdcli/OHVolume.xml
+usr/share/upmpdcli/RenderingControl.xml
+usr/share/upmpdcli/cdplugins/pycommon
+usr/share/upmpdcli/description.xml
+usr/share/upmpdcli/icon.png
+usr/share/upmpdcli/presentation.html
+usr/share/upmpdcli/protocolinfo.txt
+usr/share/upmpdcli/radio_scripts
+usr/share/upmpdcli/rdpl2stream
+usr/share/upmpdcli/src_scripts
+usr/share/upmpdcli/src_scripts/Analog-Input
+usr/share/upmpdcli/upmpdcli.conf-dist
+usr/share/upmpdcli/upmpdcli.conf-xml
--- upmpdcli-1.9.17.orig/debian/upmpdcli.service
+++ upmpdcli-1.9.17/debian/upmpdcli.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=UPnP Renderer front-end to MPD
+After=network-online.target mpd.service systemd-remount-fs.service
+Wants=network-online.target
+
+[Service]
+Type=simple
+# Note: if start fails check with "systemctl status upmpdcli"
+ExecStart=/usr/bin/upmpdcli -c /etc/upmpdcli.conf
+# For some reason, it happens the first start of libupnp fails. Probably
+# this should be started later in the start sequence, but I don't know
+# how. Retry a bit later.
+Restart=always
+RestartSec=1min
+
+[Install]
+WantedBy=multi-user.target
--- upmpdcli-1.9.17.orig/debian/watch
+++ upmpdcli-1.9.17/debian/watch
@@ -0,0 +1,3 @@
+version=4
+
+https://www.lesbonscomptes.com/upmpdcli/ upmpdcli-(.*)\.tar\.gz
--- upmpdcli-1.9.17.orig/debian/changelog
+++ upmpdcli-1.9.17/debian/changelog
@@ -0,0 +1,928 @@
+upmpdcli (1.9.17-1.1+12.3) unstable; urgency=low
+
+  * version number update by debtransform
+
+ -- debtransform <build@opensuse.org>  Thu, 30 Jul 2026 18:08:45 +0000
+
+upmpdcli (1.9.17-1.1) unstable; urgency=low
+
+  * A 1.9.16 briefly existed, with a typo in a Python file in the Qobuz
+    plugin.
+  * Media server: misc plugin updates and fixes: radio, Subsonic
+  * Media server: restore Qobuz connectivite by supporting the new oauth
+    connection method. See the manual for details.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 15 Apr 2026 17:22:00 +0200
+
+upmpdcli (1.9.15-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: plugins updates: subsonic, mother earth.
+  * OpenHome: Playlist: Improve compat with Linn version.
+  * OpenHome: Radio: use album title if available in radio metadata.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 21 Mar 2026 08:30:00 +0100
+
+upmpdcli (1.9.14-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: let the plugins specify custom HTTP header for fetching
+    from the streaming service. Allows, e.g., for specific authentication
+    using the headers.
+  * Media server: proxy mode: fix rare deadlock when seeking or switching
+    tracks rapidly.
+  * Media server: subsonic and tidal updates
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 23 Feb 2026 18:30:00 +0100
+
+upmpdcli (1.9.13-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Renderer: improve behaviour when switching between Radio and Playlist
+    OpenHome sources.
+  * Media server: subsonic update.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 17 Feb 2026 08:30:00 +0100
+
+upmpdcli (1.9.12-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: uprcl: speed up initialisation by using stored recoll
+    data instead of accessing the file system.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 23 Jan 2026 14:11:00 +0100
+
+upmpdcli (1.9.11-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: uprcl: search: support "=" operator for properties.
+    (mostly motivated by upplay new "click on album title" feature).
+  * Media server: uprcl: add option to not run the indexer on startup.
+  * Media server: uprcl: don't add "album" tag on album entries.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 14 Jan 2026 14:11:00 +0100
+
+upmpdcli (1.9.10-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: Qobuz: improve search, make it usable
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 05 Jan 2026 11:33:00 +0100
+
+upmpdcli (1.9.9-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: uprcl: improve cover detection. Fix incompatibility with recoll.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 02 Jan 2026 19:20:00 +0100
+
+upmpdcli (1.9.8-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: Subsonic plugins updates
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 20 Dec 2025 13:10:00 +0100
+
+upmpdcli (1.9.7-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media server: Tidal and Subsonic plugins updates
+  * Media server: uprcl: fix issue with streaming URL containing
+    colon characters. Update bottle module version.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 02 Nov 2025 09:10:00 +0100
+
+upmpdcli (1.9.6-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Tidal and Subsonic plugins updates
+  * Media server: fix crash occuring when the proxy/redirect is called
+    with a bogus URL (did not happen under normal circumstances).
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 26 May 2025 09:10:00 +0200
+
+upmpdcli (1.9.5-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix qobuz plugin, it had stopped working, probably following a Qobuz API change.
+  * Fix startup crash when built with gcc 15 (initialisation order issue).
+  * Many updates in Media Server plugins, mostly Tidal and Subsonic.
+  * There was a brief 1.9.4, almost immediately updated for the qobuz fix.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 06 May 2025 07:44:00 +0200
+
+upmpdcli (1.9.3-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Misc cleanups in media server plugins
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 11 Mar 2025 16:40:00 +0100
+
+upmpdcli (1.9.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix issue caused by autologin to qobuz when using OH Credentials
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 19 Feb 2025 11:40:00 +0100
+
+upmpdcli (1.9.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Multiple improvements in Tidal and Subsonic Media Server modules.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 07 Feb 2025 10:39:00 +0100
+
+upmpdcli (1.9.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Qobuz: updated the plugin to be closer to the choices on the Web
+    site (reason for the middle version bump).
+  * Qobuz: fixed the search to better return what actually requested.
+  * Tidal: updated plugin.
+  * Misc fixes: restore custom icon support, clear "phantom" track from mpd
+    to avoid its reappearance on startup,
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 19 Nov 2024 12:03:00 +0100
+
+upmpdcli (1.8.18-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix most glaring issues in the Qobuz plugin
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 10 Oct 2024 12:03:00 +0200
+
+upmpdcli (1.8.17-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix qobux plugin issue on bookworm (fails showing genres list).
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 18 Sep 2024 13:26:00 +0200
+
+upmpdcli (1.8.16-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix broken uprcl in 1.8.13-15.
+  * Fix opening log file when started as root.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 19 Aug 2024 13:36:00 +0200
+
+upmpdcli (1.8.14-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix linking  error in 1.8.13 scctl
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 18 Aug 2024 19:20:00 +0200
+
+upmpdcli (1.8.13-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Updates to the Tidal and Subsonic plugins.
+  * Uprcl compatibility with Recoll 1.40
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 17 Aug 2024 10:10:00 +0200
+
+upmpdcli (1.8.12-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Updates to the Tidal plugin
+  * qmake-based MacOS build
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 30 May 2024 17:22:00 +0200
+
+upmpdcli (1.8.11-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Updates to the Tidal and Subsonic Media Server plugins.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 17 May 2024 17:36:00 +0200
+
+upmpdcli (1.8.10-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Moved build system from autotools to meson
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 04 Apr 2024 15:28:00 +0200
+
+upmpdcli (1.8.9-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Add man page for the config file.
+  * Small internal cleanups. Always set id and parentId when generating didl.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 04 Mar 2024 15:28:00 +0100
+
+upmpdcli (1.8.8-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Tidal: avoid SQL error with older sqlite versions.
+  * Misc subsonic and Tidal improvements
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 12 Feb 2024 16:58:00 +0100
+
+upmpdcli (1.8.7-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Misc small plugins fixes.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 08 Jan 2024 10:56:00 +0100
+
+upmpdcli (1.8.6-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix issues in the HTTP streaming proxy. They were preventing its use
+    with Kodi.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 22 Nov 2023 09:34:00 +0100
+
+upmpdcli (1.8.5-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Remove deezer support, it was not working any more.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 19 Nov 2023 08:37:00 +0100
+
+upmpdcli (1.8.4-4~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Rebuilt for new npupnp/upnpp libs version numbering.
+  * Rebuilt for rpi 1 (armv6) compatibility
+  * Misc. improvements to the Tidal, Subsonic,and Radio Paradise Media
+    Server plugins.
+  * New Mother Earth Radio Media Server plugin.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 05 Nov 2023 05:15:00 +0100
+
+upmpdcli (1.8.3-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Completely new Tidal plugin by Giovanni Fulco. Tidal access is
+    working again !
+  * Media Server: change the internal URL format to avoid characters
+    needing quoting in XML: work around misc. device bugs.
+  * Qobuz: add option to obtain actual item resource info when listing
+    a container.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 17 Sep 2023 16:00:00 +0200
+
+upmpdcli (1.8.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Add configuration variable to enable the internal WEB server local
+    file serving function, mostly for use by Media Server plugins for
+    serving icons. Off by default.
+  * Subsonic/Navidrome plugin: misc. updates and improvements.
+  * Uprcl: fix using the 'albumartist' tag in priority when it is set.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 26 Jun 2023 14:00:00 +0100
+
+upmpdcli (1.7.9-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media Server: new Subsonic connector plugin by Giovanni Fulco: allows
+    streaming music from a Subsonic/Navidrome server to an UPnP renderer.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 19 Apr 2023 10:43:00 +0100
+
+upmpdcli (1.7.7-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media Server: fix issues in upnp-to-recoll search translation, and
+    improve search behaviour with some control points (bubble and
+    kazoo).
+  * Fix potential issues with parallel requests.
+  * Uprcl: index artist documents so that they can be searched for
+  * When a search request is relative to objid "0", use the plugin
+    root instead of last browsed container.
+  * Avoid phantom entries by adjusting the result count for entries
+    filtered out by class.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 21 Mar 2023 16:31:00 +0100
+
+upmpdcli (1.7.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix URL comparison issue which could cause false diags in
+    checkContentFormat().
+  * Change the default value for checkcontentformat from true to false.
+  * Qobuz: always use https.
+  * New radio-browser Media Server plugin for https://www.radio-browser.info/
+  * Ensure that we exit when getting a terminating signal while waiting for
+    mpd during init
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 13 Feb 2023 14:12:00 +0100
+
+upmpdcli (1.7.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Uprcl Media Server: report composers as <upnp:artist role="Composer">
+  * Uprcl: fix possible infinite loop while parsing search string.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 15 Jan 2023 10:41:00 +0100
+
+upmpdcli (1.7.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media Server: implement rootalias parameter to point to arbitrary
+    container inside the tree. This is to avoid useless navigation from
+    the actual root for people who, e.g., only use the uprcl '[folders]' tree.
+  * Uprcl: support multiple values for tags (e.g. multiple genres).
+  * Uprcl: fix miscellaneous small issues.
+  * Removed Spotify support as libspotify does not work any more.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 27 Dec 2022 17:14:00 +0100
+
+upmpdcli (1.6.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Upradios: set upnp:artist to "Internet Radio" to avoid "Unknown artist"
+    display in some Control Points.
+  * Uprcl: Add id3 tag support for dsf and wav files
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 18 Dec 2022 10:10:00 +0100
+
+upmpdcli (1.6.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Media Server: uprcl, qobuz, upradios: properly implement UPnP
+    Browse(Metadata), for the sole use of Kodi, probably.
+  * Uprcl container sort: sort by file names if there are no tags (e.g. wav
+    files).
+  * Uprcl: add uprclnotag view configuration variable to disable the "Tag
+    View" entries in the folders tree.
+  * Make configuration parameter names case-insensitive.
+  * Small bugs fixes
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 01 Dec 2022 21:10:00 +0100
+
+upmpdcli (1.6.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * uprcl: process custom tags (not predefined anywhere) as regular ones.
+  * main: change order of media server and renderer initialisations so that
+    the media server does not have to wait for mpd.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 22 Oct 2022 09:10:00 +0200
+
+upmpdcli (1.6.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Allow all configuration variables to also be set through environment
+    variables named UPMPD_xxx where xxx is the upper-cased version of the
+    variable name. This should facilitate running upmpdcli in a container.
+  * Change the names of the low level UPnP library log variables from
+    UPMPDCLI_xxx to UPMPD_xxx and manage them in the same way as the rest
+    (with config entries too).
+  * Allow substituting the host name (%h,%H) and version info (%v) in the
+    configured friendlyname. The default friendlyname is now UpMpd-%h
+  * Uprcl: use same host as upmpdcli by default. Add uprclport configuration
+    variable to only set the port
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 09 Oct 2022 09:44:00 +0200
+
+upmpdcli (1.5.20-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Uprcl: preserve stored playlist order
+  * Uprcl: choose folder image file rather than track-embedded art for folder
+    or album
+  * Uprcl: misc fixes.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 30 Aug 2022 09:17:00 +0200
+
+upmpdcli (1.5.19-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix uprcl web control interface (asking for update did not work).
+  * Fix upradios stations list order.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 28 Jun 2022 09:17:00 +0200
+
+upmpdcli (1.5.18-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Create an upmpdcli group during installation and change the group to
+    upmpdcli when started as root (in addition to the setuid()).
+  * uprcl: try to show more information when the initialisation fails.
+  * Add upmpdcli icon to the Debian package
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 13 Jun 2022 16:11:00 +0200
+
+upmpdcli (1.5.17-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix Qobuz plugin consecutively to small change in Qobuz interface.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 21 May 2022 16:11:00 +0200
+
+upmpdcli (1.5.16-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix radios using Apple Live Streaming, which were broken in 1.5.14
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 22 Apr 2022 10:31:00 +0200
+
+upmpdcli (1.5.15-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix possible deadlocks when switching to Songcast mode
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 13 Apr 2022 18:15:00 +0200
+
+upmpdcli (1.5.14-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Added mediaserver module for listing and listening to  BBC radio stations
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 31 Mar 2022 18:38:00 +0200
+
+upmpdcli (1.5.13-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Enable L16 by default as it is handled correctly by current mpd versions
+  * Add the mpd version to the description version string
+  * Change Qobuz default format to 6
+  * Misc small bug fixes
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 13 Oct 2021 10:38:00 +0200
+
+upmpdcli (1.5.12-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Add small Media Server plugin (upradios) for exporting the radio list.
+  * uprcl: export the radio list as a playlist
+  * uprcl: small improvements
+  * Deezer, Qobuz: set pictures for artists and playlists
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 22 Apr 2021 09:48:00 +0200
+
+upmpdcli (1.5.11-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix possible issue when volume controlled from Kazoo
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 13 Mar 2021 14:15:00 +0100
+
+upmpdcli (1.5.10-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Repair event loop after mpd restart.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 01 Mar 2021 15:34:00 +0100
+
+upmpdcli (1.5.9-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fixed events not working when mpd password was set
+  * Mediaserver support for Deezer
+  * Mediaserver: improved caching, improved uprcl local media module.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 24 Feb 2021 11:19:00 +0100
+
+upmpdcli (1.5.8-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix a number of memory leak and locking issues detected by
+    valgrind/helgrind
+  * Improve performance of big playlist insert by rate-limiting events.
+  * Fix possible crash when started while mpd is not running.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 10 Jan 2021 19:24:00 +0100
+
+upmpdcli (1.5.7-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * OHInfo issue resulted in no album art display in kazoo "now playing"
+    screen
+  * Pause/Unpause caused by other controller was not detected if the pause
+    was long enough for the mpd connection to time out (60S).
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 30 Dec 2020 15:24:00 +0100
+
+upmpdcli (1.5.6-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * OHPlaylist insert: use locking when updating the metadata cache to
+    avoid races with mpd events.
+  * OHRadio/OHinfo: try to ensure that transport events are emitted before
+    metadata ones.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 24 Dec 2020 16:54:00 +0100
+
+upmpdcli (1.5.5-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * More small radio channels metadata adjustments for improving
+    interoperation with Bubble DS.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 07 Dec 2020 18:00:00 +0100
+
+upmpdcli (1.5.4-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix radio channels metadata production.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 05 Dec 2020 09:51:00 +0100
+
+upmpdcli (1.5.3-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix issue with removing tracks from the playlist.
+  * Fix bad locking causing multiple execs of radio scripts.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 01 Dec 2020 18:15:00 +0100
+
+upmpdcli (1.5.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix crash, Lumin, Qobuz
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 24 Nov 2020 18:15:00 +0100
+
+upmpdcli (1.5.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix lacking subscription of OHInfo resulting in missing metadata
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 22 Nov 2020 19:12:00 +0100
+
+upmpdcli (1.5.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Separate the OpenHome and UPnP/AV UPnP devices.
+  * New event management method allows more timely generation
+    of events and no process activity whatsoever when not playing.
+  * Add off by default configuration options for L16 support and IPV6 operation.
+  * Remove the gmusic plugin from the documentation and distribution.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 15 Nov 2020 12:40:00 +0100
+
+upmpdcli (1.4.14-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix build for microhttpd versions 0.9.71 and later (MHD_Result enum)
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 02 Sep 2020 08:28:00 +0100
+
+upmpdcli (1.4.12-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix some AVT events metadata emitted while Playlist is active
+  * New HighResAudio streaming service media server plugin
+  * Misc small fixes
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 12 Jun 2020 13:47:00 +0100
+
+upmpdcli (1.4.10-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Update tidal token and set it in ohcredentials Get data to help bubble
+    discriminate working and non-working versions.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 17 May 2020 10:42:00 +0100
+
+upmpdcli (1.4.9-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Support gmusic oauth login
+  * Fix Radio Paradise meta fetching script
+  * Fix umpdcli-config
+  * Fix error case in Tidal module
+  * Set appid in OHcreds Get response
+  * Other small fixes.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 26 Apr 2020 12:22:00 +0100
+
+upmpdcli (1.4.7-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix previous fix for resetting mpd single mode on playlist init. The
+    previous method confused Kazoo.
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 26 Feb 2020 13:38:00 +0100
+
+upmpdcli (1.4.6-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Depend on libupnpp6. Would still build and run with libupnpp5
+  * Misc small improvements (esp in radio scripts and radio source).
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 29 Jan 2020 09:21:00 +0100
+
+upmpdcli (1.4.5-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fixed some memory leaks in the spotify plugin
+  * Move the qobuz app_key/secret from the code to the configuration.
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 15 Oct 2019 18:04:00 +0200
+
+upmpdcli (1.4.4-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix the "Improvement in radio stream title detection": it broke OHPlaylist
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 16 Sep 2019 11:51:00 +0200
+
+upmpdcli (1.4.3-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Allow setting Spotify preferred bitrate
+  * Improvement in radio stream title detection
+  * experimental uprcl: bundle bottle.py with the package as the generally
+    distributed 0.12 is not recent enough. Other improvements.
+  * Misc small fixes
+    
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 13 Sep 2019 16:35:00 +0200
+
+upmpdcli (1.4.2-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Small bug fixes
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 28 Mar 2019 09:30:00 +0100
+
+upmpdcli (1.4.1-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix bug in Spotify plugin
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 13 Feb 2019 17:17:00 +0100
+
+upmpdcli (1.4.0-2~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Add forgotten MS-description.xml file
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 19 Jan 2019 17:00:00 +0100
+
+upmpdcli (1.4.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Save the current radio settings across restarts.
+  * Add the avtautoplay config variable to accommodate a non-conformant
+    Control Point which does not issue a Play command after SetAVTransportURI.
+  * Add mpdtimeoutms variable to configure MPD connections timeouts.
+  * Use Python 3 for all scripts
+  * gmusic: cache some data locally to improve performance.
+  * OHCredentials: use stored authentication data if available to avoid login
+    on CP.
+  * Uprcl: use Waitress as WSGI server, instead of Paste.
+  * Fix issue in Spotify access when started by root (at boot time).
+  * Qobuz: allow longer playlists
+  * Use the libupnpp 0.17.0 features. This makes us incompatible with 0.16.
+  * Add the capability to run the Media Server in-process (as a separate root
+    device). Needs a very recent libupnp.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 15 Jan 2019 19:00:00 +0100
+
+upmpdcli (1.3.2-2~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Fix minor bug in ohcredentials use
+  * Package did not install OHCredentials.xml
+  * Fix deps in control file
+  *  -2: add forgotten radio_scripts directory to package
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 16 Sep 2018 11:24:00 +0200
+
+upmpdcli (1.3.0-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Spotify support
+  * Switch to Python3
+  * OpenHome Credentials support (disable for use with Lumin by setting
+    lumincompat=1). 
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 01 Sep 2018 10:06:00 +0200
+
+upmpdcli (1.2.16-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Added the upmpdcli-config tool
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 07 Apr 2018 17:09:00 +0200
+
+upmpdcli (1.2.15-2~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Depend on libupnpp4
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 21 Jun 2017 08:20:00 +0200
+
+upmpdcli (1.2.15-1~ppaPPAVERS~SERIES1) SERIES; urgency=low
+
+  * Faster Qobuz searches (less requests to the service)
+  * Songcast: stop the inactive sender so that it does not show up in
+    lists.
+  * Radio dynamic metadata scripts for radio which provide metadata from
+    the web site, instead of icy metadata.
+  * Properly handle radios which redirect straight from http to mms.
+  * AVTransport: accept metadata-less streams.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 21 Jun 2017 08:20:00 +0200
+
+upmpdcli (1.2.14-1) unstable; urgency=low
+
+  * Make the Receiver source visible, allowing grouping from Kazoo
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 19 May 2017 10:29:00 +0100
+
+upmpdcli (1.2.12-1) unstable; urgency=low
+
+  * Fixed bugs in the content directory interface: prevented Linn Kazoo to
+    see more than a few entries in each folder.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 03 Jan 2017 08:18:00 +0100
+
+upmpdcli (1.2.11-1) unstable; urgency=low
+
+  * Improve Qobuz interface
+  * Allow dynamic retrieval of artwork for radios currently playing track
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 26 Dec 2016 14:05:00 +0100
+
+upmpdcli (1.2.10-3) unstable; urgency=low
+
+  * Fix bad option in rules file
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 03 Dec 2016 17:43:00 +0100
+
+upmpdcli (1.2.10-1) unstable; urgency=low
+
+  * Improve streaming services searches
+  * Fix bug in upnp/av mode (seek after gapless transition)
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 20 Nov 2016 10:11:00 +0100
+
+upmpdcli (1.2.9-1) unstable; urgency=low
+
+  * Fix bug in format of protocolinfo data
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 14 Oct 2016 12:56:00 +0200
+
+upmpdcli (1.2.8-1) unstable; urgency=low
+
+  * Small fix in qobuz module.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 10 Oct 2016 11:20:00 +0200
+
+upmpdcli (1.2.7-1) unstable; urgency=low
+
+  * Raspbian wheezy compat tweak, tidal python module only
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 09 Oct 2016 18:30:00 +0200
+
+upmpdcli (1.2.6-1) unstable; urgency=low
+
+  * Small fixes, missing entries in protocolinfo etc.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 09 Oct 2016 17:23:00 +0200
+
+upmpdcli (1.2.5-1) unstable; urgency=low
+
+  * Tidal repaired thanks to the Kodi addon developers !
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 03 Oct 2016 18:54:00 +0200
+
+upmpdcli (1.2.4-1) unstable; urgency=low
+
+  * Fix streaming service search (type was ignored)
+  * Fix crash with absent config
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 03 Oct 2016 12:10:00 +0200
+
+upmpdcli (1.2.3-1) unstable; urgency=low
+
+  * Moved the streaming service plugins into separate packages
+  * Improved Qobuz navigation.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 24 Sep 2016 19:23:00 +0200
+
+upmpdcli (1.2.2-1) unstable; urgency=low
+
+  * Improved streaming service search
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Sat, 10 Sep 2016 19:43:00 +0200
+
+upmpdcli (1.2.0-1) unstable; urgency=low
+
+  * Streaming services
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 02 Sep 2016 14:44:00 +0200
+
+upmpdcli (1.1.4~jessie-1) jessie; urgency=low
+
+  * Misc small improvements. See Changelog.
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 23 Jun 2016 13:53:00 +0200
+
+upmpdcli (1.1.3-1) unstable; urgency=low
+
+  * Fix cover art display, which sometimes vanished.
+  * Allow changing the xml data directory in the run time config.
+  * Fix execution of onvolumechange (command could not have args).
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 17 Mar 2016 14:40:00 +0100
+
+upmpdcli (1.1.1-1) unstable; urgency=low
+
+  * Improve play state management when switching sources
+  * Support external scripts for volume control
+  * Avoid losing metadata (e.g. album art) when switching from Playlist to
+    Radio and back
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 18 Feb 2016 18:03:00 +0100
+
+upmpdcli (1.1.0-1) unstable; urgency=low
+
+  * Add OpenHome Radio Source (play internet radios). Needs Python.
+  * Add capability to forward ALSA input channel to Songcast (needs
+    sc2mpd 0.14).
+  * Fix Receiver detection from Windows Songcast
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 04 Feb 2016 19:05:00 +0100
+
+upmpdcli (0.13.1-1) unstable; urgency=low
+
+  * Turn off MPD "single" mode if it was set by some other app !
+  * Add support for the new sc2mpd/mpd2sc Sender mode. This allows sharing
+    the audio with multiple synchronized other upmpdcli/sc2mpd instances
+    (multiroom) 
+  * Fix random initial value for ohproduct standby variable
+  * Fix eventing for ohproduct
+  
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 23 Dec 2015 16:30:00 +0100
+
+upmpdcli (0.12.0-1) unstable; urgency=low
+
+  * Avoid error messages occurring when UPnP AV was suppressed (because
+    eventing was still active) by using new libupnpp feature.
+  * Bump version to help remember that this needs libupnpp 0.12
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 10 Aug 2015 08:47:00 +0200
+
+upmpdcli (0.11.2-1) unstable; urgency=low
+
+  * Add config parameter to allow turning off UPnP AV (keep OpenHome only).
+    Helps Linn Kinsky a lot
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 06 May 2015 14:43:00 +0200
+
+upmpdcli (0.11.0-2) unstable; urgency=low
+
+  * Fixed scweb service install
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 06 May 2015 14:43:00 +0200
+
+upmpdcli (0.11.0-1) unstable; urgency=low
+
+  * ohreceiver service augmented to support sc2mpd in direct alsa mode
+  * scctl command allows controlling Songcast receivers
+  * Actually serve a presentation page.
+  * Misc: see git log
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 06 May 2015 12:30:00 +0200
+
+upmpdcli (0.9.1-1) unstable; urgency=low
+
+  * ?
+  * 
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Fri, 14 Nov 2014 17:53:00 +0200
+
+upmpdcli (0.9.0-1) unstable; urgency=low
+
+  * Support Songcast streaming (needs the sc2mpd program). See web site:
+    http://www.lesbonscomptes.com/upmpdcli
+  * Fixed quoting for tracks added from an MPD client.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 12 Nov 2014 17:53:00 +0200
+
+upmpdcli (0.8.6-1) unstable; urgency=low
+
+  * Improved speed for loading big playqueues. 
+  * Fixed quoting for tracks added from an MPD client.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 31 Oct 2014 15:30:00 +0200
+
+upmpdcli (0.8.5-1) unstable; urgency=low
+
+  * Version bump for new packaging. No code changes
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 13 Oct 2014 18:41:00 +0200
+
+upmpdcli (0.8.4-1) unstable; urgency=low
+
+  * Separated libupnpp. 
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 12 Oct 2014 16:23:00 +0200
+
+upmpdcli (0.8.3-1) unstable; urgency=low
+
+  * libupnpp control: Use libcurl to perform the description document
+    download. Previously, the bad timeout management in libupnp would allow
+    a very slow or sick device to basically DOS the discovery process. This
+    has no consequences for upmpdcli proper (only the control side of the
+    lib), beyond introducing a libcurl dependancy, which mpd itself needs
+    anyway (if it's going to accept urls).
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 09 Oct 2014 11:31:00 +0200
+
+upmpdcli (0.8.2-1) unstable; urgency=low
+
+  * Fix volume control on some renderers.
+  * Allow setting an icon to be displayed by control point in renderer lists
+  * Mix build fixes
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 07 Oct 2014 15:56:00 +0200
+
+upmpdcli (0.8.1-1) unstable; urgency=low
+
+  * Minor bug fixes and improvements.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 18 Sep 2014 05:56:00 +0200
+
+upmpdcli (0.8.0-1) unstable; urgency=low
+
+  * Save queue metadata to disk for persistence across restarts. 
+    Set ohmetapersist to 0 in configuration file to disable.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 09 Jun 2014 08:56:00 +0200
+upmpdcli (0.7.1-1) unstable; urgency=low
+
+  * Add support for the OpenHome ohMedia services. Set openhome=1 in 
+    configuration file to enable.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 09 Jun 2014 08:56:00 +0200
+upmpdcli (0.6.4-1) unstable; urgency=low
+
+  * Configuration for UPnP interface and port, MPD password
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 20 Apr 2014 15:43:00 +0200
+upmpdcli (0.6.3-1) unstable; urgency=low
+
+  * Fix seeking
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 26 March 2014 21:58:00 +0100
+upmpdcli (0.6.2-1) unstable; urgency=low
+
+  * Retry libupnp until we get a network
+  * Publish our aiff capability
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Sun, 02 March 2014 14:03:00 +0100
+upmpdcli (0.6.1-1) unstable; urgency=low
+
+  * Implemented minimal connection manager
+  * Help for starting at boot: run as upmpdcli user after starting as root
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 25 Feb 2014 14:03:00 +0100
+upmpdcli (0.5-1) unstable; urgency=low
+
+  * Cache volume value when mpd is inactive.
+  * Clear the MPD queue when we feel like it.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Thu, 13 Feb 2014 18:54:00 +0100
+upmpdcli (0.4-1) unstable; urgency=low
+
+  * Adapt to off_t size in libupnp and availability of log/debug functions.
+  * FriendlyName configuration parameter.
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Wed, 12 Feb 2014 10:07:13 +0100
+upmpdcli (0.3-1) unstable; urgency=low
+
+  * Removed DB volume interface, this was confusing some players
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 10 Feb 2014 14:09:20 +0100
+upmpdcli (0.2-1) unstable; urgency=low
+
+  * Improved event management. Moderate the load generated by sound volume
+    changes
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Tue, 11 Feb 2014 11:20:00 +0100
+upmpdcli (0.1-1) unstable; urgency=low
+
+  * Initial release 
+
+ -- Jean-Francois Dockes <jf@dockes.org>  Mon, 10 Feb 2014 15:37:33 +0100
