pax_global_header00006660000000000000000000000064136401520030014504gustar00rootroot0000000000000052 comment=81774309e07bf49ebfead314a342ae083651a258 pulseaudio-dlna-0.5.3+git20200329/000077500000000000000000000000001364015200300162005ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/.gitignore000066400000000000000000000014251364015200300201720ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ share/python-wheels/ material/ develop-eggs/ dist/ downloads/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt pip-selfcheck.json # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ .codeintel *.sublime-* dist/ bin/ include/ local/ *.iml .ideapulseaudio-dlna-0.5.3+git20200329/LICENSE000066400000000000000000001044621364015200300172140ustar00rootroot00000000000000GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . pulseaudio-dlna-0.5.3+git20200329/Makefile000066400000000000000000000034531364015200300176450ustar00rootroot00000000000000# This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . python ?= python3 user ?= $(shell whoami) all: pulseaudio_dlna.egg-info venv: @echo "venv is deprecated. It is just 'make' now." pulseaudio_dlna.egg-info: setup.py bin/pip bin/pip install --editable . && touch $@ bin/pip: virtualenv --system-site-packages -p $(python) . ifdef DEB_HOST_ARCH DESTDIR ?= / PREFIX ?= usr/ install: $(python) setup.py install --no-compile --prefix="$(PREFIX)" --root="$(DESTDIR)" --install-layout=deb else DESTDIR ?= / PREFIX ?= /usr/local install: $(python) setup.py install --no-compile --prefix="$(PREFIX)" endif release: manpage pdebuild --buildresult dist lintian --pedantic dist/*.deb dist/*.dsc dist/*.changes sudo chown -R $(user) dist/ manpage: man/pulseaudio-dlna.1 man/pulseaudio-dlna.1: pulseaudio_dlna.egg-info export USE_PKG_VERSION=1; help2man -n "Stream audio to DLNA devices and Chromecasts" "bin/pulseaudio-dlna" > /tmp/pulseaudio-dlna.1 mv /tmp/pulseaudio-dlna.1 man/pulseaudio-dlna.1 clean: rm -rf build dist $(shell find pulseaudio_dlna -name "__pycache__") rm -rf *.egg-info *.egg bin local lib lib64 include share pyvenv.cfg rm -rf docs htmlcov .coverage .tox pip-selfcheck.json pulseaudio-dlna-0.5.3+git20200329/README.md000066400000000000000000001450261364015200300174670ustar00rootroot00000000000000# About # This is _pulseaudio-dlna_. A lightweight streaming server which brings DLNA / UPNP and Chromecast support to PulseAudio and Linux. It can stream your current PulseAudio playback to different UPNP devices (UPNP Media Renderers) or Chromecasts in your network. Its main goals are: easy to use, no configuration hassle, no big dependencies. UPNP renderers in your network will show up as pulseaudio sinks. ![Image of pulseaudio-dlna](https://github.com/masmu/pulseaudio-dlna/blob/master/samples/images/pavucontrol-sample.png) ## License ## pulseaudio-dlna is licensed under GPLv3. pulseaudio-dlna 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. pulseaudio-dlna 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 pulseaudio-dlna. If not, see . ## Donation ## ![Image of pulseaudio-dlna](http://maemo.lancode.de/.webdir/donate.gif) If I could help you or if you like my work, you can buy me a [coffee, a beer or pizza](https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=totalexceed%40lancode%2ede&item_name=Donation&no_shipping=2&no_note=1&tax=0¤cy_code=EUR&bn=PP%2dDonationsBF&charset=UTF%2d8). ## Changelog ## * __master__ - (_2017-04-06_) - Fixed a bug where the detection of DLNA devices failed when there were multiple network interfaces - The application now binds to all interfaces by default - When using multiple network interfaces the appropriate network address is being used for streaming (new dependency `python-pyroute2` (preferred) or `python-netaddr` (fallback)) - Migrated to GI bindings (removed dependencies `python-gobject` `python-rsvg` `python-gtk2`, new dependency `python-gi`, new optional dependencies `gir1.2-rsvg-2.0`, `gir1.2-gtk-3.0`) - Fixed a bug where devices with the same name could keep updating each other - Fixed a bug where codec bit rates could not be set although those were supported - Fixed a bug where a missing xml attribute prevented xml parsing - Added the `--disable-mimetype-check` option - Disabled mimetype check for virtual _Raumfeld_ devices - Subprocesses now always exit gracefully - Added the `--chunk-size` option - Added _pulseaudio_ as an encoder backend (*experimental*) - You can now just start one instance of pulseaudio-dlna - Fixed a bug where non-ascii characters in $PATH broke `distutils.spawn.find_executable()` - Also use environment's `XDG_RUNTIME_DIR` for detecting the DBus socket - The detection of the stream servers host address now uses the systems routing table - Because of devices with a kernel < 3.9, `python-zeroconf >= 0.17.4` is now required * __0.5.2__ - (_2016-04-01_) - Catched an exception when record processes cannot start properly * __0.5.1__ - (_2016-04-01_) - Fixed the `--filter-device` option - Prioritize _mp3_ over _flac_ for Chromecasts * __0.5.0.1__ - (_2016-03-09_) - Readded manpage * __0.5.0__ - (_2016-03-09_) - Set Yamaha devices to the appropriate mode before playing (thanks to [hlchau](https://github.com/hlchau)) (new dependency: `python-lxml`) - Fixed a bug where some SSDP messages could not get parsed correctly - Also support media renderers identifying as `urn:schemas-upnp-org:device:MediaRenderer:2` - Added the `--disable-workarounds` flag - Added the `--auto-reconnect` flag - Added the `--encoder-backend` option (new optional dependencies `ffmpeg`, `libav-tools`) - Removed shared encoder processes - Increased the default HTTP timeout to 15 seconds - Fixed a bug where manually added renderers could appear twice - Added device state polling for devices which start playing on their own - Added the flac encoder for _Google Chromecast_ - Added support for _Google Cast Groups_ (new dependency `python-zeroconf`) - Removed dependency `python-beautifulsoup` - Fixed a bug where bytes were not decoded properly to unicode * __0.4.7__ - (_2015-11-18_) - The application can now co-exist with other applications which are using the port 1900/udp (thanks to [klaernie](https://github.com/klaernie)) - Fixed the daemon mode to support `psutil` 1.x and 2.x (thanks to [klaernie](https://github.com/klaernie)) - HTML entities in device descriptions are now converted automatically - Faster and more reliable device discovery (new dependency `python-netifaces`) - Added the `--cover-mode` option, one mode requires (optional) dependencies `gtk`, `cairo`, `rsvg` - L16 codecs are now selected better (e.g. needed for _XBox 360_) - Fixed a bug where sometimes it was tried to remove sinks twice on cleanup - Added the `--update-device-config` flag - Added the `--ssdp-ttl`, `--ssdp-mx`, `--ssdp-amount` options - Added the `--msearch-port` option * __0.4.6__ - (_2015-10-17_) - Added support for _Google Chromecast Audio_ (thanks to [leonhandreke](https://github.com/leonhandreke)) - Fixed a bug where devices which does not specifiy control urls made the application crash - Added the `--disable-device-stop` flag - Added the `--request-timeout` option - You can now also add rules to renderers (e.g. `DISABLE_DEVICE_STOP`, `REQUEST_TIMEOUT`) - Fixed a bug where stream urls where not parsed correctly - Fixed a bug which made a Chomecast Audio throwing exceptions while stopping - Fixed a bug where the system's default encoding could not be determined when piping the applications output * __0.4.5.2__ - (_2015-09-21_) - Fixed a bug where the encoding of SSDP headers was not detected correctly (new dependency: `python-chardet`) * __0.4.5.1__ - (_2015-09-20_) - Added a missing dependency `python-concurrent.futures` (thanks to [Takkat-Nebuk](https://github.com/Takkat-Nebuk)) * __0.4.5__ - (_2015-09-20_) - Exceptions while updating sink and device information from pulseaudio are now handled better - Changed `--fake-http10-content-length` flag to `--fake-http-content-length` to also support HTTP 1.1 requests - Fixed a bug where the supported device mime types could not get parsed correctly - Fixed a bug where the device UUID was not parsed correctly - Fixed a bug where just mime types beginning with `audio/` where accepted, but not e.g. `application/ogg` - The stream server will now respond with 206 when receiving requests with `range` header - UPNP control commands have now a timeout of 10 seconds - Fixed a bug where the wrong stream was removed from the stream manager - Fixed several bugs caused by purely relying on stopping actions for the devices idle state - Added L16 Encoder - The encoder option can now handle multiple options separated by comma - Added the `--create-device-config` flag - Fixed a bug where the dbus session was bound from the wrong process - Fix a bug where the wrong device UDN was retrieved from XML documents containing multiple devices * __0.4.4__ - (_2015-08-07_) - Added `--disable-ssdp-listener` option - Fixed a bug with applications which remove and re-add streams all the time - Added a missing dependency `python-psutil` * __0.4.3__ - (_2015-08-02_) - Fixed a bug when trying to terminate an encoder process - Catch exceptions when trying to update pulseaudio sinks - Fixed a timing issue where the streamserver was not ready but devices were already instructed to play * __0.4.2__ - (_2015-08-02_) - The mp3 encoder is now prioritize over wav - Added `--disable-switchback` option - Wav encoders do not longer share their encoder process * __0.4.1__ - (_2015-07-27_) - Fixed Makefile for launchpad * __0.4.0__ - (_2015-07-27_) - Added the ```--fake-http10-content-length``` option - The application can now run as root - Catch pulseaudio exceptions for streams, sinks and modules when those are gone - Fixed a bug where a missing ssdp header field made the application crash - New devices are added now during runtime (thanks to [coder-hugo](https://github.com/coder-hugo)) - Rewrite of the streaming server - Upnp devices can now request their audio format based on their capabilities - Added AAC encoder - If a device stops playing, the streams currently playing on the corresponding sink are switched back to the default sink - If a device failes to start playing, streams currently playing on the corresponding sink are switched back to the default sink - Added Chromecast support (new dependency: `python-protobuf`) - Fixed a bug where the application crashed when there was no suitable encoder found - Added the ```--bit-rate``` option - Added additional headers for DLNA devices - Added switch back mode also for sinks, not just for streams (new dependency: `python-notify2`) - Added better logging - Validate encoders for installed dependencies * __0.3.5__ - (_2015-04-09_) - Fixed a bug where Sonos description XML could not get parsed correctly * __0.3.4__ - (_2015-03-22_) - Fixed Makefile for launchpad * __0.3.3__ - (_2015-03-22_) - Added the ```--filter-device``` option - Send 2 SSDP packets by default for better UPNP device discovery - Added virtualenv for local installation * __0.3.2__ - (_2015-03-14_) - Added the Opus Encoder (new dependency: `opus-tools`) (thanks to [MobiusHorizons](https://github.com/MobiusHorizons)) - Fixed a bug where an empty UPNP device name made the application crash - Added a missing dependency (`python-gobject`) * __0.3.1__ - (_2015-02-13_) - Fixed a bug so that AVTransports other than 1 can be used (thanks to [martin-insulander-info](https://github.com/martin-insulander-info)) * __0.3.0__ - (_2015-02-01_) - Added debian packaging - Added proper signal handlers (new dependency: `python-setproctitle`) - Fixed a bug where binding to an already used port made the application crash - HTTP charset encoding is now specified correctly * __0.2.4__ - (_2015-01-25_) - Stream changes are now handled correctly (thanks to [Takkat-Nebuk](https://github.com/Takkat-Nebuk)) * __0.2.3__ - (_2015-01-21_) - Fixed a timing bug where the pulseaudio module was not loaded fast enough (thanks to [Takkat-Nebuk](https://github.com/Takkat-Nebuk)) * __0.2.2__ - (_2015-01-18_) - Fixed encoding issues - Try to load the DBus module if it is not loaded before (thanks to [Takkat-Nebuk](https://github.com/Takkat-Nebuk)) * __0.2.1__ - (_2015-01-11_) - TTL changed to 10 and timeout to 5 for UDP broadcasting - Added the ```--renderer-urls``` option to manually add UPNP devices via their control url - Added the ```--debug``` flag - The host ip address is now discovered automatically, no need to specifiy ```--host``` anymore ## Installation via PPA ## Supported Ubuntu releases: - 17.04 (Zesty Zapus) - 16.10 (Yakkety Yak) - 16.04 (Xenial Xerus) - 14.04.2 LTS (Trusty Tahr) Ubuntu users can install _pulseaudio-dlna_ via the following [repository](https://launchpad.net/~qos/+archive/ubuntu/pulseaudio-dlna). sudo apt-add-repository ppa:qos/pulseaudio-dlna sudo apt-get update sudo apt-get install pulseaudio-dlna ### Starting ### After that you can start _pulseaudio-dlna_ via: pulseaudio-dlna Head over the the _using section_ for further instructions. ## Installation for other distributions ## Some community members are providing packages for others distributions. _Keep in mind that since i am not using those, i can hardly support them!_ - Arch Linux [https://aur.archlinux.org/packages/pulseaudio-dlna/](https://aur.archlinux.org/packages/pulseaudio-dlna/) - openSUSE (_.rpm_) [http://packman.links2linux.de/package/pulseaudio-dlna](http://packman.links2linux.de/package/pulseaudio-dlna) - Fedora - RHEL - CentOS - EPEL [https://copr.fedoraproject.org/coprs/cygn/pulseaudio-dlna/](https://copr.fedoraproject.org/coprs/cygn/pulseaudio-dlna/) - Debian [https://packages.debian.org/sid/pulseaudio-dlna](https://packages.debian.org/sid/pulseaudio-dlna) ## Installation via git ## Other linux users can clone this git repository, make sure you have all the dependencies installed and the PulseAudio DBus module is loaded. ### Basic requirements ### These are the requirements _pulseaudio-dlna_ acutally needs to run. These dependencies will get installed if you install it via the PPA. - python3 - python3-pip - python3-setuptools - python3-dbus - python3-docopt - python3-requests - python3-setproctitle - python3-gi - python3-notify2 - python3-psutil - python3-chardet - python3-netifaces - python3-pyroute2 | python3-netaddr - python3-lxml - python3-pychromecast - vorbis-tools - sox - lame - flac - faac - opus-tools You can install all the dependencies in Ubuntu via: sudo apt-get install python3 python3-pip python3-setuptools python3-dbus python3-docopt python3-requests python3-setproctitle python3-gi python3-notify2 python3-psutil python3-chardet python3-netifaces python3-pyroute2 python3-netaddr python3-lxml python3-pychromecast vorbis-tools sox lame flac faac opus-tools ### PulseAudio DBus module ### Since version _0.2.2_ the DBus module should be loaded automatically, if it was not loaded before. It that does not work, you can load the DBus module in Ubuntu via the following command. Note that you have to do this every time you restart PulseAudio (or your computer). pacmd load-module module-dbus-protocol Or to make changes persistant edit the file `/etc/pulse/default.pa` with your favorite editor and append the following line: load-module module-dbus-protocol ### Install it local ### The recommend method of using _pulseaudio-dlna_ is to install it local to a python _virtualenv_. In that way you will keep your system clean. If you don't like it anymore, just delete the folder. For that method you need some additional dependencies. #### virtualenv requirements #### - python-virtualenv (Ubuntu <= _14.04 Trusty LTS_) - virtualenv (Ubuntu >= _14.10 Utopic_) - python-dev So all Ubuntu versions prior to _14.10 Utopic_ need to install: sudo apt-get install python-virtualenv python-dev All Ubuntu versions above install: sudo apt-get install virtualenv python3-dev #### Installing & starting #### Change to the _project root folder_ and start the installation via: make After that you can start _pulseaudio-dlna_ via: bin/pulseaudio-dlna ### Install it to your system ### Since some people like it more to install software globally, you can do that too. In many software projects this is the default installation method. #### Installing & starting #### Change to the _root folder_ and start the installation via: make install After that you can start _pulseaudio-dlna_ via: pulseaudio-dlna ### Using ### _pulseaudio-dlna_ should detect the ip address your computer is reachable within your local area network. If the detected ip address is not correct or there were no ips found, you still can specifiy them yourself via the host option (```--host ```) Right after startup it should start searching for UPNP devices in your LAN and add new PulseAudio sinks. After 5 seconds the progress is complete and you can select your UPNP renderers from the default audio control. In case you just want to stream single audio streams to your UPNP devices you can do this via `pavucontrol`. You can install `pavucontrol` in Ubuntu via the following command: sudo apt-get install pavucontrol Note that _pulseaudio-dlna_ has to run all the time while you are listening to your music. If you stop _pulseaudio-dlna_ it will cleanly remove the created UPNP devices from PulseAudio and your UPNP devices will stop playing. Since 0.4, new devices are automatically discovered as they appear on the network. ### CLI ### Usage: pulseaudio-dlna [--host ] [--port ][--encoder | --codec ] [--bit-rate=] [--encoder-backend ] [--filter-device=] [--renderer-urls ] [--request-timeout ] [--chunk-size ] [--msearch-port=] [--ssdp-mx ] [--ssdp-ttl ] [--ssdp-amount ] [--cover-mode ] [--auto-reconnect] [--debug] [--fake-http10-content-length] [--fake-http-content-length] [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] [--disable-workarounds] [--disable-mimetype-check] pulseaudio-dlna [--host ] [--create-device-config] [--update-device-config] [--msearch-port=] [--ssdp-mx ] [--ssdp-ttl ] [--ssdp-amount ] pulseaudio-dlna [-h | --help | --version] Options: --create-device-config Discovers all devices in your network and write a config for them. That config can be editied manually to adjust various settings. You can set: - Device name - Codec order (The first one is used if the encoder binary is available on your system) - Various codec settings such as the mime type, specific rules or the bit rate (depends on the codec) A written config is loaded by default if the --encoder and --bit-rate options are not used. --update-device-config Same as --create-device-config but preserves your existing config from being overwritten --host= Set the server ip. -p --port= Set the server port [default: 8080]. -e --encoder= Deprecated alias for --codec -c --codec= Set the audio codec. Possible codecs are: - mp3 MPEG Audio Layer III (MP3) - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) - wav Waveform Audio File Format (WAV) - opus Opus Interactive Audio Codec (OPUS) - aac Advanced Audio Coding (AAC) - l16 Linear PCM (L16) --encoder-backend= Set the backend for all encoders. Possible backends are: - generic (default) - ffmpeg - avconv -b --bit-rate= Set the audio encoder's bitrate. --filter-device= Set a name filter for devices which should be added. Devices which get discovered, but won't match the filter text will be skipped. --renderer-urls= Set the renderer urls yourself. no discovery will commence. --request-timeout= Set the timeout for requests in seconds [default: 15]. --chunk-size= Set the stream's chunk size [default: 4096]. --ssdp-ttl= Set the SSDP socket's TTL [default: 10]. --ssdp-mx= Set the MX value of the SSDP discovery message [default: 3]. --ssdp-amount= Set the amount of SSDP discovery messages being sent [default: 5]. --msearch-port= Set the source port of the MSEARCH socket [default: random]. --cover-mode= Set the cover mode [default: default]. Possible modes are: - disabled No icon is shown - default The application icon is shown - distribution The icon of your distribution is shown - application The audio application's icon is shown --debug enables detailed debug messages. --auto-reconnect If set, the application tries to reconnect devices in case the stream collapsed --fake-http-content-length If set, the content-length of HTTP requests will be set to 100 GB. --disable-switchback If set, streams won't switched back to the default sink if a device disconnects. --disable-ssdp-listener If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. --disable-device-stop If set, the application won't send any stop commands to renderers at all --disable-workarounds If set, the application won't apply any device workarounds --disable-mimetype-check If set, the application won't check the device's mime type capabilities -v --version Show the version. -h --help Show the help. Samples: - `pulseaudio-dlna` will start _pulseaudio-dlna_ on port _8080_ and stream your PulseAudio streams encoded with _mp3_. - `pulseaudio-dlna --encoder ogg` will start _pulseaudio-dlna_ on port _8080_ and stream your PulseAudio streams encoded with _Ogg Vorbis_. - `pulseaudio-dlna --port 10291 --encoder flac` will start _pulseaudio-dlna_ on port _10291_ and stream your PulseAudio streams encoded with _FLAC_. - `pulseaudio-dlna --filter-device 'Nexus 5,TV'` will just use devices named _Nexus 5_ or _TV_ even when more devices got discovered. - `pulseaudio-dlna --renderer-urls http://192.168.1.7:7676/smp_10_` won't discover upnp devices by itself. Instead it will search for upnp renderers at the specified locations. You can specify multiple locations via urls seperated by comma (_,_). Most users won't ever need this option, but since UDP multicast packages won't work (most times) over VPN connections this is very useful if you ever plan to stream to a UPNP device over VPN. ### Device configuration rules Most times the automatic discovery of supported device codecs and their prioritization works pretty good. But in the case of a device which does work out of the box or if you don't like the used codec you can adjust the settings with a _device configuration_. If you want to create a specific configuration for your devices you can do that via the `--create-device-config` flag. It will search for devices on your network and write a config for them. It will look for / write them at: - `~/.local/share/pulseaudio-dlna/devices.json` (prioritized) - `/etc/pulseaudio-dlna/devices.json` The purpose of this is that the application should do the most work for the user. You just have to edit the file instead of writing it completely on your own. Let's make an example: I started the application via `pulseaudio-dlna --create-device-config` and that is what was discovered: ```json "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { "rules": [], "flavour": "DLNA", "name": "Device name", "codecs": [ { "rules": [], "bit_rate": null, "identifier": "mp3", "mime_type": "audio/mpeg" }, { "rules": [], "identifier": "flac", "mime_type": "audio/flac" }, { "channels": 2, "rules": [], "identifier": "l16", "sample_rate": 48000, "mime_type": "audio/L16;rate=48000;channels=2" }, { "channels": 2, "rules": [], "identifier": "l16", "sample_rate": 44100, "mime_type": "audio/L16;rate=44100;channels=2" }, { "channels": 1, "rules": [], "identifier": "l16", "sample_rate": 44100, "mime_type": "audio/L16;rate=44100;channels=1" } ] } ``` It was detected that the device supports the following codecs: - `audio/mp3` - `audio/flac` - `audio/L16;rate=48000;channels=2` - `audio/L16;rate=44100;channels=2` - `audio/L16;rate=44100;channels=1` If you don't change the configuration at all, it means that the next time you start _pulseaudio-dlna_ it will automatically use those codecs for that device. The order of the list also defines the priority. It will take the first codec and use it if the appropriate encoder binary is installed on your system. If the binary is missing it will take the next one. So here the _mp3_ codec would be used, if the _lame_ binary is installed. You can also change the name of the device, adjust the mime type or set the bit rate. A `null` value means _default_, for bit rates this is set to 192 Kbit/s. In that case I want to rename my device to "Living Room". Besides that I don't want the L16 codecs, so i simply remove them and i want my _mp3_ to be encoded in 256 Kbit/s. ```json "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { "rules": [], "flavour": "DLNA", "name": "Living Room", "codecs": [ { "rules": [], "bit_rate": 256, "identifier": "mp3", "mime_type": "audio/mpeg" }, { "rules": [], "identifier": "flac", "mime_type": "audio/flac" } ] } ``` But as it turns out this device has a problem with playing the _mp3_ stream when you don't specify the `--fake-http-content-length` flag. Let's say _flac_ works without the flag. So, you can add a rule for that to that device. ```json "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { "rules": [], "flavour": "DLNA", "name": "Living Room", "codecs": [ { "rules": [ { "name": "FAKE_HTTP_CONTENT_LENGTH" } ], "bit_rate": 256, "identifier": "mp3", "mime_type": "audio/mpeg" }, { "rules": [], "identifier": "flac", "mime_type": "audio/flac" } ] } ``` That's it. _pulseaudio-dlna_ will automatically use that config if you don't use the `--encoder` or `--bit-rate` options. ## Known Issues ## - **Distorted sound** If you experience distorted sound, try to pause / unpause the playback or changing / adjusting the volume. Some encoders handle volume changes better than others. The _lame_ encoder handles this by far better than most of the other ones. - **There is a delay about a few seconds** Since there is HTTP streaming used for the audio data to transport, there is always a buffer involved. This device buffer ensures that even if you suffer from a slow network (e.g. weak wifi) small interruptions won't affect your playback. On the other hand devices will first start to play when this buffer is filled. Most devices do this based on the received amount of data. Therefore inefficient codecs such as _wav_ fill that buffer much faster than efficient codecs do. The result is a noticeable shorter delay in contrast to e.g. _mp3_ or others. Note, that in this case your network should be pretty stable, otherwise your device will quickly run out of data and stop playing. This is normally not a problem with cable connections. E.g. I have a delay about 1-2 seconds with _wav_ and a delay of about 5 seconds with _mp3_ with the same cable connected device. You can decrease the delay when using _wav_ or using high bit rates, but you won't get rid of it completely. My advice: If you have a reliable network, use _wav_. It is lossless and you will get a short delay. If you have not, use another encoder which does not require that much bandwidth to make sure your device will keep playing. Of course you will be affected from a higher delay. ## Troubleshooting ## - **My device does not get discovered by _pulseaudio-dlna_** The computer _pulseaudio-dlna_ is running on and your device needs to be in the same network. In uncomplicated home LANs this is normally the case. You can test if other applications are able to find your device, e.g. _BubbleUPnP_ (_Android_ application). If they do it is likely that you are using a firewall / iptables. Try disabling it. If you verified that your firewall is blocking, you should use the `--msearch-port ` option and open port 8080/_tcp_, port 1900/_udp_ and port ``/_udp_. - **The device is successfully instructed to play, but the device never connects to _pulseaudio-dlna_** Check if your are using a firewall / iptables. If that works, open port 8080/_tcp_ and port 1900/_udp_. - **The device is successfully instructed to play, but the device immediately disconnects after some seconds** Some devices do not stick to the HTTP 1.0/1.1 standard. Since most devices do, _pulseaudio-dlna_ must be instructed by CLI flags to act in a non-standard way. - `--fake-http-content-length` Adds a faked HTTP Content-Length to HTTP 1.0/1.1 responses. The length is set to 100 GB and ensures that the device would keep playing for months. This is e.g. necessary for the _Hame Soundrouter_ and depending on the used encoder for _Sonos_ devices. ## Tested devices ## A listed entry means that it was successfully tested, even if there is no specific codec information available. Device | mp3 | wav | ogg | flac | aac | opus | l16 ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- [AVM FritzRepeater N/G](http://avm.de/) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: BubbleUPnP (Android App) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: [Cocy UPNP media renderer](https://github.com/mnlipp/CoCy) | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: D-Link DCH-M225/E | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: DAMAI Airmusic | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Denon AVR-3808 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Denon AVR-X4000 | :white_check_mark: | :grey_question: | :grey_question: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: Freebox Player Mini (4K) | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: Freebox Player (Revolution) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: [gmrender-resurrect](http://github.com/hzeller/gmrender-resurrect) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Google Chromecast (1st gen) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: Google Chromecast Audio | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: Hame Soundrouter | :white_check_mark:1 | :no_entry_sign: | :no_entry_sign: | :white_check_mark:1 | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: LG BP550 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Libratone ZIPP | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: Logitech Media Server | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Majik DSM | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Medion P85055 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [Naim Mu-So](https://www.naimaudio.com/mu-so) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :white_check_mark: Onkyo TX-8050 | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :white_check_mark: Onkyo TX-NR509 | :grey_question: | :white_check_mark: | :grey_question: | :no_entry_sign: | :grey_question: | :grey_question: | :grey_question: Onkyo TX-NR616 7 | :grey_question: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Onkyo TX-NR646 | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Onkyo TX-NR727 7 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Onkyo CR-N755 | :white_check_mark:8 | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :grey_question:9 | :no_entry_sign: | :white_check_mark: [Oppo Sonica](https://www.oppodigital.com/sonica/) | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :grey_question: | :white_check_mark: [Pi MusicBox](http://www.woutervanwijk.nl/pimusicbox/) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Panasonic TX-50CX680W | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Panasonic TX-50CX680W | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Philips NP2500 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Philips NP2900 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Pioneer SC-LX76 (AV Receiver) | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: Pioneer VSX-824 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Pure Jongo S3 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [Raumfeld One M](http://raumfeld.com) | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :grey_question: | :no_entry_sign: | :no_entry_sign: [Raumfeld Speaker M](http://raumfeld.com) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [Raumfeld Speaker S](http://raumfeld.com) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: [ROCKI](http://www.myrocki.com/) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [rygel](https://wiki.gnome.org/Projects/Rygel) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: RaidSonic IB-MP401Air | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Samsung Smart TV LED32 (UE32ES5500) | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: Samsung Smart TV LED40 (UE40ES6100) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Samsung Smart TV LED46 (UE46ES6715) | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :grey_question: | :no_entry_sign: Samsung Smart TV LED48 (UE48JU6560) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_circle:2 | :no_entry_sign: | :no_entry_sign: Samsung Smart TV LED60 (UE60F6300) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Sonos PLAY:1 | :white_check_mark:3 | :white_check_mark: | :white_check_mark:3 | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :grey_question: Sonos PLAY:3 | :white_check_mark:3 | :white_check_mark: | :white_check_mark:3 | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :grey_question: Sony SRS-X77 | :white_check_mark:1 | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark:1 Sony SRS-X88 | :white_check_mark:1 | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark:1 Sony SRS-ZR5 | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: Sony STR-DN1050 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [Volumio](http://volumio.org) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [Volumio 2](http://volumio.org) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Xbmc / Kodi | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_circle:2 | :white_circle:2 | :white_check_mark: Xbox 360 | :white_check_mark:5 | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :grey_question: | :no_entry_sign: | :white_check_mark: Yamaha CRX-N560D 4 | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: Yamaha RX-475 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: Yamaha RX-V573 (AV Receiver) 6 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: WDTV Live | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: 1) Works when specifing the `--fake-http-content-length` flag 2) Is capable of playing the codec, but does not specifiy the correct mime type 3) Works since _0.4.5_ (`--fake-http-content-length` is added automatic) 4) The device needs to be in _SERVER_ mode to accept instructions 5) Was reported to buffer really long. Approximately 45 seconds 6) Was reported to have issues being discovered. Make sure you run the latest firmware 7) Reported to need a `--request-timeout` of 15 seconds to work. Since _0.5.0_ the timeout is set to that value. 8) Stuttering at 256kbit/s and pretty unstable at 320kbit/s 9) The manual states it is supported. No success yet, neither with --fake-http-content-length nor with increased timeout values ## Supported encoders ## Encoder | Description | Identifier ------------- | ------------- | ------------- lame | MPEG Audio Layer III | mp3 oggenc | Ogg Vorbis | ogg flac | Free Lossless Audio Codec | flac sox | Waveform Audio File Format | wav opusenc | Opus Interactive Audio Codec | opus faac | Advanced Audio Coding | aac sox | Linear PCM | l16 You can select a specific codec using the `--encoder` flag followed by its identifier. pulseaudio-dlna-0.5.3+git20200329/man/000077500000000000000000000000001364015200300167535ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/man/pulseaudio-dlna.1000066400000000000000000000132361364015200300221300ustar00rootroot00000000000000.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.2. .TH PULSEAUDIO-DLNA "1" "April 2016" "pulseaudio-dlna 0.6.0" "User Commands" .SH NAME pulseaudio-dlna \- Stream audio to DLNA devices and Chromecasts .SH DESCRIPTION .SS "Usage:" .TP pulseaudio\-dlna pulseaudio\-dlna [\-\-host ] [\-\-port ][\-\-encoder | \fB\-\-codec\fR ] [\-\-bit\-rate=] [\-\-encoder\-backend ] [\-\-filter\-device=] [\-\-renderer\-urls ] [\-\-request\-timeout ] [\-\-msearch\-port=] [\-\-ssdp\-mx ] [\-\-ssdp\-ttl ] [\-\-ssdp\-amount ] [\-\-cover\-mode ] [\-\-auto\-reconnect] [\-\-debug] [\-\-fake\-http10\-content\-length] [\-\-fake\-http\-content\-length] [\-\-disable\-switchback] [\-\-disable\-ssdp\-listener] [\-\-disable\-device\-stop] [\-\-disable\-workarounds] .TP pulseaudio\-dlna [\-\-host ] [\-\-create\-device\-config] [\-\-update\-device\-config] [\-\-msearch\-port=] [\-\-ssdp\-mx ] [\-\-ssdp\-ttl ] [\-\-ssdp\-amount ] .IP pulseaudio\-dlna [\-h | \fB\-\-help\fR | \fB\-\-version]\fR .SH OPTIONS .TP \fB\-\-create\-device\-config\fR Discovers all devices in your network and write a config for them. That config can be editied manually to adjust various settings. You can set: .TP \- Device name \- Codec order (The first one is used if the encoder binary is available on your system) \- Various codec settings such as the mime type, specific rules or .TP the bit rate (depends on the codec) A written config is loaded by default if the \fB\-\-encoder\fR and \fB\-\-bit\-rate\fR options are not used. .TP \fB\-\-update\-device\-config\fR Same as \fB\-\-create\-device\-config\fR but preserves your existing config from being overwritten .TP \fB\-\-host=\fR Set the server ip. .TP \fB\-p\fR \fB\-\-port=\fR Set the server port [default: 8080]. .TP \fB\-e\fR \fB\-\-encoder=\fR Deprecated alias for \fB\-\-codec\fR .TP \fB\-c\fR \fB\-\-codec=\fR Set the audio codec. Possible codecs are: .TP \- mp3 MPEG Audio Layer III (MP3) .TP \- ogg Ogg Vorbis (OGG) .TP \- flac Free Lossless Audio Codec (FLAC) .TP \- wav Waveform Audio File Format (WAV) .TP \- opus Opus Interactive Audio Codec (OPUS) .TP \- aac Advanced Audio Coding (AAC) .TP \- l16 Linear PCM (L16) .TP \fB\-\-encoder\-backend=\fR Set the backend for all encoders. Possible backends are: .TP \- generic (default) \- ffmpeg \- avconv .TP \fB\-b\fR \fB\-\-bit\-rate=\fR Set the audio encoder's bitrate. .TP \fB\-\-filter\-device=\fR Set a name filter for devices which should be added. Devices which get discovered, but won't match the filter text will be skipped. .TP \fB\-\-renderer\-urls=\fR Set the renderer urls yourself. no discovery will commence. .TP \fB\-\-request\-timeout=\fR Set the timeout for requests in seconds [default: 15]. .TP \fB\-\-ssdp\-ttl=\fR Set the SSDP socket's TTL [default: 10]. .TP \fB\-\-ssdp\-mx=\fR Set the MX value of the SSDP discovery message [default: 3]. .TP \fB\-\-ssdp\-amount=\fR Set the amount of SSDP discovery messages being sent [default: 5]. .TP \fB\-\-msearch\-port=\fR Set the source port of the MSEARCH socket [default: random]. .TP \fB\-\-cover\-mode=\fR Set the cover mode [default: default]. Possible modes are: .TP \- disabled No icon is shown .TP \- default The application icon is shown .TP \- distribution The icon of your distribution is shown .TP \- application The audio application's icon is shown .TP \fB\-\-debug\fR enables detailed debug messages. .TP \fB\-\-auto\-reconnect\fR If set, the application tries to reconnect devices in case the stream collapsed .TP \fB\-\-fake\-http\-content\-length\fR If set, the content\-length of HTTP requests will be set to 100 GB. .TP \fB\-\-disable\-switchback\fR If set, streams won't switched back to the default sink if a device disconnects. .TP \fB\-\-disable\-ssdp\-listener\fR If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. .TP \fB\-\-disable\-device\-stop\fR If set, the application won't send any stop commands to renderers at all .TP \fB\-\-disable\-workarounds\fR If set, the application won't apply any device workarounds .TP \fB\-v\fR \fB\-\-version\fR Show the version. .TP \fB\-h\fR \fB\-\-help\fR Show the help. .SH EXAMPLES .IP \- pulseaudio\-dlna .IP will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with mp3. .IP \- pulseaudio\-dlna \-\-encoder ogg .IP will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with Ogg Vorbis. .IP \- pulseaudio\-dlna \-\-port 10291 \-\-encoder flac .IP will start pulseaudio\-dlna on port 10291 and stream your PulseAudio streams encoded with FLAC. .IP \- pulseaudio\-dlna \-\-filter\-device 'Nexus 5,TV' .IP will just use devices named Nexus 5 or TV even when more devices got discovered. .IP \- pulseaudio\-dlna \-\-renderer\-urls http://192.168.1.7:7676/smp_10_ .IP won't discover upnp devices by itself. Instead it will search for upnp renderers at the specified locations. You can specify multiple locations via urls separated by comma (,). Most users won't ever need this option, but since UDP multicast packages won't work (most times) over VPN connections this is very useful if you ever plan to stream to a UPNP device over VPN. .SH "SEE ALSO" The full documentation for .B pulseaudio-dlna is maintained as a Texinfo manual. If the .B info and .B pulseaudio-dlna programs are properly installed at your site, the command .IP .B info pulseaudio-dlna .PP should give you access to the complete manual. pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/000077500000000000000000000000001364015200300213505ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/__init__.py000066400000000000000000000021731364015200300234640ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import os import pkg_resources from .utils import git try: version = pkg_resources.get_distribution(__package__).version except pkg_resources.DistributionNotFound: version = 'unknown' if os.environ.get('USE_PKG_VERSION', None) == '1': branch, rev = None, None else: branch, rev = git.get_head_version() __version__ = '{version}{rev}'.format( version=version, rev='+git-{} ({})'.format(rev, branch) if rev else '', ) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/__main__.py000066400000000000000000000216301364015200300234440ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . ''' Usage: pulseaudio-dlna [--host ] [--port ][--encoder | --codec ] [--bit-rate=] [--encoder-backend ] [--filter-device=] [--renderer-urls ] [--request-timeout ] [--chunk-size ] [--msearch-port=] [--ssdp-mx ] [--ssdp-ttl ] [--ssdp-amount ] [--cover-mode ] [--auto-reconnect] [--debug] [--fake-http10-content-length] [--fake-http-content-length] [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] [--disable-workarounds] [--disable-mimetype-check] pulseaudio-dlna [--host ] [--create-device-config] [--update-device-config] [--msearch-port=] [--ssdp-mx ] [--ssdp-ttl ] [--ssdp-amount ] pulseaudio-dlna [-h | --help | --version] Options: --create-device-config Discovers all devices in your network and write a config for them. That config can be editied manually to adjust various settings. You can set: - Device name - Codec order (The first one is used if the encoder binary is available on your system) - Various codec settings such as the mime type, specific rules or the bit rate (depends on the codec) A written config is loaded by default if the --encoder and --bit-rate options are not used. --update-device-config Same as --create-device-config but preserves your existing config from being overwritten --host= Set the server ip. -p --port= Set the server port [default: 8080]. -e --encoder= Deprecated alias for --codec -c --codec= Set the audio codec. Possible codecs are: - mp3 MPEG Audio Layer III (MP3) - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) - wav Waveform Audio File Format (WAV) - opus Opus Interactive Audio Codec (OPUS) - aac Advanced Audio Coding (AAC) - l16 Linear PCM (L16) --encoder-backend= Set the backend for all encoders. Possible backends are: - generic (default) - ffmpeg - avconv -b --bit-rate= Set the audio encoder's bitrate. --filter-device= Set a name filter for devices which should be added. Devices which get discovered, but won't match the filter text will be skipped. --renderer-urls= Set the renderer urls yourself. no discovery will commence. --request-timeout= Set the timeout for requests in seconds [default: 15]. --chunk-size= Set the stream's chunk size [default: 4096]. --ssdp-ttl= Set the SSDP socket's TTL [default: 10]. --ssdp-mx= Set the MX value of the SSDP discovery message [default: 3]. --ssdp-amount= Set the amount of SSDP discovery messages being sent [default: 5]. --msearch-port= Set the source port of the MSEARCH socket [default: random]. --cover-mode= Set the cover mode [default: default]. Possible modes are: - disabled No icon is shown - default The application icon is shown - distribution The icon of your distribution is shown - application The audio application's icon is shown --debug enables detailed debug messages. --auto-reconnect If set, the application tries to reconnect devices in case the stream collapsed --fake-http-content-length If set, the content-length of HTTP requests will be set to 100 GB. --disable-switchback If set, streams won't switched back to the default sink if a device disconnects. --disable-ssdp-listener If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. --disable-device-stop If set, the application won't send any stop commands to renderers at all --disable-workarounds If set, the application won't apply any device workarounds --disable-mimetype-check If set, the application won't check the device's mime type capabilities -v --version Show the version. -h --help Show the help. Examples: - pulseaudio-dlna will start pulseaudio-dlna on port 8080 and stream your PulseAudio streams encoded with mp3. - pulseaudio-dlna --encoder ogg will start pulseaudio-dlna on port 8080 and stream your PulseAudio streams encoded with Ogg Vorbis. - pulseaudio-dlna --port 10291 --encoder flac will start pulseaudio-dlna on port 10291 and stream your PulseAudio streams encoded with FLAC. - pulseaudio-dlna --filter-device 'Nexus 5,TV' will just use devices named Nexus 5 or TV even when more devices got discovered. - pulseaudio-dlna --renderer-urls http://192.168.1.7:7676/smp_10_ won't discover upnp devices by itself. Instead it will search for upnp renderers at the specified locations. You can specify multiple locations via urls separated by comma (,). Most users won't ever need this option, but since UDP multicast packages won't work (most times) over VPN connections this is very useful if you ever plan to stream to a UPNP device over VPN. ''' import sys import os import docopt import logging import socket import getpass def main(argv=sys.argv[1:]): import pulseaudio_dlna options = docopt.docopt(__doc__, version=pulseaudio_dlna.__version__) level = logging.DEBUG if not options['--debug']: level = logging.INFO logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.basicConfig( level=level, format='%(asctime)s %(name)-46s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M:%S') logger = logging.getLogger('pulseaudio_dlna.__main__') if not acquire_lock(): print('The application is shutting down, since there already seems to ' 'be a running instance.') return 1 if os.geteuid() == 0: logger.info('Running as root. Starting daemon ...') import pulseaudio_dlna.daemon daemon = pulseaudio_dlna.daemon.Daemon() daemon.run() else: import pulseaudio_dlna.application app = pulseaudio_dlna.application.Application() app.run(options) return 0 def acquire_lock(): acquire_lock._lock_socket = socket.socket( socket.AF_UNIX, socket.SOCK_DGRAM) try: name = '/com/masmu/pulseaudio_dlna/{}'.format(getpass.getuser()) acquire_lock._lock_socket.bind('\0' + name) return True except socket.error: return False if __name__ == "__main__": sys.exit(main()) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/application.py000066400000000000000000000327561364015200300242420ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import multiprocessing import signal import setproctitle import logging import sys import json import os import time import pulseaudio_dlna import pulseaudio_dlna.holder import pulseaudio_dlna.plugins.dlna import pulseaudio_dlna.plugins.dlna.ssdp import pulseaudio_dlna.plugins.dlna.ssdp.listener import pulseaudio_dlna.plugins.dlna.ssdp.discover import pulseaudio_dlna.plugins.chromecast import pulseaudio_dlna.encoders import pulseaudio_dlna.covermodes import pulseaudio_dlna.streamserver import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.utils.network import pulseaudio_dlna.rules import pulseaudio_dlna.workarounds logger = logging.getLogger('pulseaudio_dlna.application') class Application(object): DEVICE_CONFIG_PATHS = [ os.path.expanduser('~/.local/share/pulseaudio-dlna'), '/etc/pulseaudio-dlna', ] DEVICE_CONFIG = 'devices.json' PLUGINS = [ pulseaudio_dlna.plugins.dlna.DLNAPlugin(), pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), ] SHUTDOWN_TIMEOUT = 5 def __init__(self): self.processes = [] self.is_terminating = False def shutdown(self, signal_number=None, frame=None): if not self.is_terminating: print('Application is shutting down ...') self.is_terminating = True for process in self.processes: # We send SIGINT to all subprocesses to trigger # KeyboardInterrupt and exit the mainloop # in those which use GObject.MainLoop(). # This unblocks the main thread and ensures that the process # is receiving signals again. os.kill(process.pid, signal.SIGINT) # SIGTERM is the acutal one which is terminating the process os.kill(process.pid, signal.SIGTERM) start_time = time.time() while True: if time.time() - start_time >= self.SHUTDOWN_TIMEOUT: print('Terminating remaining subprocesses ...') for process in self.processes: if process is not None and process.is_alive(): os.kill(process.pid, signal.SIGKILL) sys.exit(1) time.sleep(0.1) all_dead = True for process in self.processes: if process.is_alive(): all_dead = False break if all_dead: break sys.exit(0) def run_process(self, target, *args, **kwargs): process = multiprocessing.Process( target=target, args=args, kwargs=kwargs) self.processes.append(process) process.start() def run(self, options): logger.info('Using version: {}'.format(pulseaudio_dlna.__version__)) if not options['--host']: host = None else: host = str(options['--host']) port = int(options['--port']) pulseaudio_dlna.streamserver.StreamServer.HOST = host pulseaudio_dlna.streamserver.StreamServer.PORT = port logger.info('Binding to {host}:{port}'.format( host=host or '*', port=port)) if options['--disable-workarounds']: pulseaudio_dlna.workarounds.BaseWorkaround.ENABLED = False if options['--disable-ssdp-listener']: pulseaudio_dlna.plugins.dlna.ssdp.listener.\ SSDPListener.DISABLE_SSDP_LISTENER = True if options['--disable-mimetype-check']: pulseaudio_dlna.plugins.renderer.DISABLE_MIMETYPE_CHECK = True if options['--chunk-size']: chunk_size = int(options['--chunk-size']) if chunk_size > 0: pulseaudio_dlna.streamserver.ProcessThread.CHUNK_SIZE = \ chunk_size if options['--ssdp-ttl']: ssdp_ttl = int(options['--ssdp-ttl']) pulseaudio_dlna.plugins.dlna.ssdp.discover.\ SSDPDiscover.SSDP_TTL = ssdp_ttl pulseaudio_dlna.plugins.dlna.ssdp.listener.\ SSDPListener.SSDP_TTL = ssdp_ttl if options['--ssdp-mx']: ssdp_mx = int(options['--ssdp-mx']) pulseaudio_dlna.plugins.dlna.ssdp.discover.\ SSDPDiscover.SSDP_MX = ssdp_mx if options['--ssdp-amount']: ssdp_amount = int(options['--ssdp-amount']) pulseaudio_dlna.plugins.dlna.ssdp.discover.\ SSDPDiscover.SSDP_AMOUNT = ssdp_amount msearch_port = options.get('--msearch-port', None) if msearch_port != 'random': pulseaudio_dlna.plugins.dlna.ssdp.discover.\ SSDPDiscover.MSEARCH_PORT = int(msearch_port) if options['--create-device-config']: self.create_device_config() sys.exit(0) if options['--update-device-config']: self.create_device_config(update=True) sys.exit(0) device_config = None if not options['--encoder'] and not options['--bit-rate']: device_config = self.read_device_config() if options['--encoder-backend']: try: pulseaudio_dlna.codecs.set_backend( options['--encoder-backend']) except pulseaudio_dlna.codecs.UnknownBackendException as e: logger.error(e) sys.exit(1) if options['--encoder']: logger.warning( 'The option "--encoder" is deprecated. ' 'Please use "--codec" instead.') codecs = (options['--encoder'] or options['--codec']) if codecs: try: pulseaudio_dlna.codecs.set_codecs(codecs.split(',')) except pulseaudio_dlna.codecs.UnknownCodecException as e: logger.error(e) sys.exit(1) bit_rate = options['--bit-rate'] if bit_rate: try: pulseaudio_dlna.encoders.set_bit_rate(bit_rate) except (pulseaudio_dlna.encoders.InvalidBitrateException, pulseaudio_dlna.encoders.UnsupportedBitrateException) as e: logger.error(e) sys.exit(1) cover_mode = options['--cover-mode'] try: pulseaudio_dlna.covermodes.validate(cover_mode) except pulseaudio_dlna.covermodes.UnknownCoverModeException as e: logger.error(e) sys.exit(1) logger.info('Encoder settings:') for _type in pulseaudio_dlna.encoders.ENCODERS: _type.AVAILABLE = False for _type in pulseaudio_dlna.encoders.ENCODERS: encoder = _type() encoder.validate() logger.info(' {}'.format(encoder)) logger.info('Codec settings:') for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): codec = _type() logger.info(' {}'.format(codec)) fake_http_content_length = False if options['--fake-http-content-length']: fake_http_content_length = True if options['--fake-http10-content-length']: logger.warning( 'The option "--fake-http10-content-length" is deprecated. ' 'Please use "--fake-http-content-length" instead.') fake_http_content_length = True disable_switchback = False if options['--disable-switchback']: disable_switchback = True disable_device_stop = False if options['--disable-device-stop']: disable_device_stop = True disable_auto_reconnect = True if options['--auto-reconnect']: disable_auto_reconnect = False pulse_queue = multiprocessing.Queue() stream_queue = multiprocessing.Queue() stream_server = pulseaudio_dlna.streamserver.ThreadedStreamServer( host, port, pulse_queue, stream_queue, fake_http_content_length=fake_http_content_length, proc_title='stream_server', ) pulse = pulseaudio_dlna.pulseaudio.PulseWatcher( pulse_queue, stream_queue, disable_switchback=disable_switchback, disable_device_stop=disable_device_stop, disable_auto_reconnect=disable_auto_reconnect, cover_mode=cover_mode, proc_title='pulse_watcher', ) device_filter = None if options['--filter-device']: device_filter = options['--filter-device'].split(',') locations = None if options['--renderer-urls']: locations = options['--renderer-urls'].split(',') if options['--request-timeout']: request_timeout = float(options['--request-timeout']) if request_timeout > 0: pulseaudio_dlna.plugins.renderer.BaseRenderer.REQUEST_TIMEOUT = \ request_timeout holder = pulseaudio_dlna.holder.Holder( plugins=self.PLUGINS, pulse_queue=pulse_queue, device_filter=device_filter, device_config=device_config, proc_title='holder', ) self.run_process(stream_server.run) self.run_process(pulse.run) if locations: self.run_process(holder.lookup, locations) else: self.run_process(holder.search, host=host) setproctitle.setproctitle('pulseaudio-dlna') signal.signal(signal.SIGINT, self.shutdown) signal.signal(signal.SIGTERM, self.shutdown) signal.signal(signal.SIGHUP, self.shutdown) signal.pause() def create_device_config(self, update=False): logger.info('Starting discovery ...') holder = pulseaudio_dlna.holder.Holder(plugins=self.PLUGINS) holder.search(ttl=5) logger.info('Discovery complete.') def device_filter(obj): if hasattr(obj, 'to_json'): return obj.to_json() else: return obj.__dict__ def obj_to_dict(obj): json_text = json.dumps(obj, default=device_filter) return json.loads(json_text) if update: existing_config = self.read_device_config() if existing_config: new_config = obj_to_dict(holder.devices) new_config.update(existing_config) else: logger.error( 'Your device config could not be found at any of the ' 'locations "{}"'.format( ','.join(self.DEVICE_CONFIG_PATHS))) sys.exit(1) else: new_config = obj_to_dict(holder.devices) json_text = json.dumps(new_config, indent=4) for config_path in reversed(self.DEVICE_CONFIG_PATHS): config_file = os.path.join(config_path, self.DEVICE_CONFIG) if not os.path.exists(config_path): try: os.makedirs(config_path) except (OSError, IOError): continue try: with open(config_file, 'w') as h: h.write(json_text) logger.info('Found the following devices:') for device in list(holder.devices.values()): logger.info('{name} ({flavour})'.format( name=device.name, flavour=device.flavour)) for codec in device.codecs: logger.info(' - {}'.format( codec.__class__.__name__)) logger.info( 'Your config was successfully written to "{}"'.format( config_file)) return except (OSError, IOError): continue logger.error( 'Your device config could not be written to any of the ' 'locations "{}"'.format(','.join(self.DEVICE_CONFIG_PATHS))) def read_device_config(self): for config_path in self.DEVICE_CONFIG_PATHS: config_file = os.path.join(config_path, self.DEVICE_CONFIG) if os.path.isfile(config_file) and \ os.access(config_file, os.R_OK): with open(config_file, 'r') as h: json_text = h.read() logger.debug('Device configuration:\n{}'.format(json_text)) json_text = json_text.replace('\n', '') try: device_config = json.loads(json_text) logger.info( 'Loaded device config "{}"'.format(config_file)) return device_config except ValueError: logger.error( 'Unable to parse "{}"! ' 'Check the file for syntax errors ...'.format( config_file)) return None pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/codecs.py000066400000000000000000000247211364015200300231700ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import functools import logging import re import inspect import sys import pulseaudio_dlna.encoders import pulseaudio_dlna.rules logger = logging.getLogger('pulseaudio_dlna.codecs') BACKENDS = ['generic', 'ffmpeg', 'avconv', 'pulseaudio'] CODECS = {} class UnknownBackendException(Exception): def __init__(self, backend): Exception.__init__( self, 'You specified an unknown backend "{}"!'.format(backend) ) class UnknownCodecException(Exception): def __init__(self, codec): Exception.__init__( self, 'You specified an unknown codec "{}"!'.format(codec), ) class UnsupportedCodecException(Exception): def __init__(self, codec, backend): Exception.__init__( self, 'You specified an unsupported codec "{}" for the ' 'backend "{}"!'.format(codec, backend), ) def set_backend(backend): if backend in BACKENDS: BaseCodec.BACKEND = backend return raise UnknownBackendException(backend) def set_codecs(identifiers): step = 3 priority = (len(CODECS) + 1) * step for identifier, _type in CODECS.items(): _type.ENABLED = False _type.PRIORITY = 0 for identifier in identifiers: try: CODECS[identifier].ENABLED = True CODECS[identifier].PRIORITY = priority priority = priority - step except KeyError: raise UnknownCodecException(identifier) def enabled_codecs(): codecs = [] for identifier, _type in CODECS.items(): if _type.ENABLED: codecs.append(_type()) return codecs @functools.total_ordering class BaseCodec(object): ENABLED = True IDENTIFIER = None BACKEND = 'generic' PRIORITY = None def __init__(self): self.mime_type = None self.suffix = None self.rules = pulseaudio_dlna.rules.Rules() @property def enabled(self): return type(self).ENABLED @enabled.setter def enabled(self, value): type(self).ENABLED = value @property def priority(self): return type(self).PRIORITY @priority.setter def priority(self, value): type(self).PRIORITY = value @property def specific_mime_type(self): return self.mime_type @property def encoder(self): return self.encoder_type() @property def encoder_type(self): if self.BACKEND in self.ENCODERS: return self.ENCODERS[self.BACKEND] else: raise UnsupportedCodecException(self.IDENTIFIER, self.BACKEND) @classmethod def accepts(cls, mime_type): for accepted_mime_type in cls.SUPPORTED_MIME_TYPES: if mime_type.lower().startswith(accepted_mime_type.lower()): return True return False def get_recorder(self, monitor): if self.BACKEND == 'pulseaudio': return pulseaudio_dlna.recorders.PulseaudioRecorder( monitor, codec=self) else: return pulseaudio_dlna.recorders.PulseaudioRecorder(monitor) def __eq__(self, other): return type(self) is type(other) def __gt__(self, other): return type(self) is type(other) def __str__(self, detailed=False): return '<{} enabled="{}" priority="{}" mime_type="{}" ' \ 'backend="{}">{}{}'.format( self.__class__.__name__, self.enabled, self.priority, self.specific_mime_type, self.BACKEND, ('\n' if len(self.rules) > 0 else '') + '\n'.join( [' - ' + str(rule) for rule in self.rules] ) if detailed else '', '\n ' + str(self.encoder) if detailed else '', ) def to_json(self): attributes = ['priority', 'suffix', 'mime_type'] d = { k: v for k, v in iter(self.__dict__.items()) if k not in attributes } d['mime_type'] = self.specific_mime_type d['identifier'] = self.IDENTIFIER return d class BitRateMixin(object): def __init__(self): self.bit_rate = None @property def encoder(self): return self.encoder_type(self.bit_rate) def __eq__(self, other): return type(self) is type(other) and self.bit_rate == other.bit_rate def __gt__(self, other): return type(self) is type(other) and self.bit_rate > other.bit_rate class Mp3Codec(BitRateMixin, BaseCodec): SUPPORTED_MIME_TYPES = ['audio/mpeg', 'audio/mp3'] IDENTIFIER = 'mp3' ENCODERS = { 'generic': pulseaudio_dlna.encoders.LameMp3Encoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegMp3Encoder, 'avconv': pulseaudio_dlna.encoders.AVConvMp3Encoder, } PRIORITY = 18 def __init__(self, mime_string=None): BaseCodec.__init__(self) BitRateMixin.__init__(self) self.suffix = 'mp3' self.mime_type = mime_string or 'audio/mp3' class WavCodec(BaseCodec): SUPPORTED_MIME_TYPES = ['audio/wav', 'audio/x-wav'] IDENTIFIER = 'wav' ENCODERS = { 'generic': pulseaudio_dlna.encoders.SoxWavEncoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegWavEncoder, 'avconv': pulseaudio_dlna.encoders.AVConvWavEncoder, 'pulseaudio': pulseaudio_dlna.encoders.NullEncoder, } PRIORITY = 15 def __init__(self, mime_string=None): BaseCodec.__init__(self) self.suffix = 'wav' self.mime_type = mime_string or 'audio/wav' class L16Codec(BaseCodec): SUPPORTED_MIME_TYPES = ['audio/l16'] IDENTIFIER = 'l16' ENCODERS = { 'generic': pulseaudio_dlna.encoders.SoxL16Encoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegL16Encoder, 'avconv': pulseaudio_dlna.encoders.AVConvL16Encoder, } PRIORITY = 1 def __init__(self, mime_string=None): BaseCodec.__init__(self) self.suffix = 'pcm16' self.mime_type = 'audio/L16' self.sample_rate = None self.channels = None if mime_string: match = re.match( '(.*?)(?P.*?);' '(.*?)rate=(?P.*?);' '(.*?)channels=(?P\d)', mime_string) if match: self.mime_type = match.group('mime_type') self.sample_rate = int(match.group('sample_rate')) self.channels = int(match.group('channels')) @property def specific_mime_type(self): if self.sample_rate and self.channels: return '{};rate={};channels={}'.format( self.mime_type, self.sample_rate, self.channels) else: return self.mime_type @property def encoder(self): return self.encoder_type(self.sample_rate, self.channels) def __eq__(self, other): return type(self) is type(other) and ( self.sample_rate == other.sample_rate and self.channels == other.channels) def __gt__(self, other): return type(self) is type(other) and ( self.sample_rate > other.sample_rate and self.channels > other.channels) class AacCodec(BitRateMixin, BaseCodec): SUPPORTED_MIME_TYPES = ['audio/aac', 'audio/x-aac'] IDENTIFIER = 'aac' ENCODERS = { 'generic': pulseaudio_dlna.encoders.FaacAacEncoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegAacEncoder, 'avconv': pulseaudio_dlna.encoders.AVConvAacEncoder, } PRIORITY = 12 def __init__(self, mime_string=None): BaseCodec.__init__(self) BitRateMixin.__init__(self) self.suffix = 'aac' self.mime_type = mime_string or 'audio/aac' class OggCodec(BitRateMixin, BaseCodec): SUPPORTED_MIME_TYPES = ['audio/ogg', 'audio/x-ogg', 'application/ogg'] IDENTIFIER = 'ogg' ENCODERS = { 'generic': pulseaudio_dlna.encoders.OggencOggEncoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegOggEncoder, 'avconv': pulseaudio_dlna.encoders.AVConvOggEncoder, 'pulseaudio': pulseaudio_dlna.encoders.NullEncoder, } PRIORITY = 6 def __init__(self, mime_string=None): BaseCodec.__init__(self) BitRateMixin.__init__(self) self.suffix = 'ogg' self.mime_type = mime_string or 'audio/ogg' class FlacCodec(BaseCodec): SUPPORTED_MIME_TYPES = ['audio/flac', 'audio/x-flac'] IDENTIFIER = 'flac' ENCODERS = { 'generic': pulseaudio_dlna.encoders.FlacFlacEncoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegFlacEncoder, 'avconv': pulseaudio_dlna.encoders.AVConvFlacEncoder, 'pulseaudio': pulseaudio_dlna.encoders.NullEncoder, } PRIORITY = 9 def __init__(self, mime_string=None): BaseCodec.__init__(self) self.suffix = 'flac' self.mime_type = mime_string or 'audio/flac' class OpusCodec(BitRateMixin, BaseCodec): SUPPORTED_MIME_TYPES = ['audio/opus', 'audio/x-opus'] IDENTIFIER = 'opus' ENCODERS = { 'generic': pulseaudio_dlna.encoders.OpusencOpusEncoder, 'ffmpeg': pulseaudio_dlna.encoders.FFMpegOpusEncoder, 'avconv': pulseaudio_dlna.encoders.AVConvOpusEncoder, } PRIORITY = 3 def __init__(self, mime_string=None): BaseCodec.__init__(self) BitRateMixin.__init__(self) self.suffix = 'opus' self.mime_type = mime_string or 'audio/opus' def load_codecs(): if len(CODECS) == 0: logger.debug('Loaded codecs:') for name, _type in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(_type) and issubclass(_type, BaseCodec): if _type is not BaseCodec: logger.debug(' {} = {}'.format(_type.IDENTIFIER, _type)) CODECS[_type.IDENTIFIER] = _type return None load_codecs() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/covermodes.py000066400000000000000000000066641364015200300241040ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import sys import inspect import socket import platform import logging logger = logging.getLogger('pulseaudio_dlna.covermodes') MODES = {} class UnknownCoverModeException(Exception): def __init__(self, cover_mode): Exception.__init__( self, 'You specified an unknown cover mode "{}"!'.format(cover_mode) ) def validate(cover_mode): if cover_mode not in MODES: raise UnknownCoverModeException(cover_mode) class BaseCoverMode(object): IDENTIFIER = None def __init__(self): self.bridge = None @property def artist(self): return 'Liveaudio on {}'.format(socket.gethostname()) @property def title(self): return ', '.join(self.bridge.sink.stream_client_names) @property def thumb(self): return None def get(self, bridge): try: self.bridge = bridge return self.artist, self.title, self.thumb finally: self.bridge = None class DisabledCoverMode(BaseCoverMode): IDENTIFIER = 'disabled' class DefaultCoverMode(BaseCoverMode): IDENTIFIER = 'default' @property def thumb(self): try: return self.bridge.device.get_image_url('default.png') except Exception: return None class DistributionCoverMode(BaseCoverMode): IDENTIFIER = 'distribution' @property def thumb(self): dist_name, dist_ver, dist_arch = platform.linux_distribution() logger.debug(dist_name) if dist_name == 'Ubuntu': dist_icon = 'ubuntu' elif dist_name == 'debian': dist_icon = 'debian' elif dist_name == 'fedora': dist_icon = 'fedora' elif dist_name == 'LinuxMint': dist_icon = 'linuxmint' elif dist_name == 'openSUSE' or dist_name == 'SuSE': dist_icon = 'opensuse' elif dist_name == 'gentoo': dist_icon = 'gentoo' else: dist_icon = 'unknown' try: return self.bridge.device.get_image_url( 'distribution-{}.png'.format(dist_icon)) except Exception: return None class ApplicationCoverMode(BaseCoverMode): IDENTIFIER = 'application' @property def thumb(self): try: return self.bridge.device.get_sys_icon_url( self.bridge.sink.primary_application_name) except Exception: return None def load_modes(): if len(MODES) == 0: for name, _type in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(_type) and issubclass(_type, BaseCoverMode): if _type is not BaseCoverMode: MODES[_type.IDENTIFIER] = _type return None load_modes() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/daemon.py000066400000000000000000000177231364015200300231770ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . from gi.repository import GObject import dbus import dbus.mainloop.glib import logging import os import sys import setproctitle import functools import signal import pwd import pulseaudio_dlna.utils.subprocess import pulseaudio_dlna.utils.psutil as psutil logger = logging.getLogger('pulseaudio_dlna.daemon') REQUIRED_ENVIRONMENT_VARS = [ 'DISPLAY', 'DBUS_SESSION_BUS_ADDRESS', 'PATH', 'XDG_RUNTIME_DIR', 'LANG' ] def missing_env_vars(environment): env = [] for var in REQUIRED_ENVIRONMENT_VARS: if var not in environment: env.append(var) return env class Daemon(object): def __init__(self): dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) setproctitle.setproctitle('pulseaudio-daemon') self.mainloop = GObject.MainLoop() self.processes = [] self.check_id = None self.is_checking = False self._check_processes() signals = ( ('NameOwnerChanged', 'org.freedesktop.DBus.{}', self.on_name_owner_changed), ) self.bus = dbus.SystemBus() self.core = self.bus.get_object('org.freedesktop.DBus', '/') for sig_name, interface, sig_handler in signals: self.bus.add_signal_receiver(sig_handler, sig_name) def shutdown(self, signal_number=None, frame=None): logger.info('Daemon.shutdown') for proc in self.processes: if proc.is_attached: proc.detach() self.processes = [] def on_name_owner_changed(self, name, new_owner, old_owner): if not self.is_checking: if self.check_id: GObject.source_remove(self.check_id) self.check_id = GObject.timeout_add( 3000, self._check_processes) def _check_processes(self): self.is_checking = True self.check_id = None logger.info('Checking pulseaudio processes ...') procs = PulseAudioFinder.get_processes() for proc in procs: if proc not in self.processes: logger.info('Adding pulseaudio process ({})'.format(proc.pid)) self.processes.append(proc) gone, alive = psutil.wait_procs(self.processes, timeout=2) for proc in gone: if proc.is_attached: proc.detach() logger.info('Removing pulseaudio process ({})'.format(proc.pid)) self.processes.remove(proc) for proc in alive: if not proc.is_attached and not proc.disabled: proc.attach() self.is_checking = False return False def run(self): try: self.mainloop.run() except KeyboardInterrupt: self.shutdown() @functools.total_ordering class PulseAudioProcess(psutil.Process): DISPLAY_MANAGERS = ['gdm', 'lightdm', 'kdm', None] UID_MIN = 500 def __init__(self, *args, **kwargs): psutil.Process.__init__(*args, **kwargs) self.application = None self.disabled = False @property def env(self): return self._get_proc_env(self.pid) @property def compressed_env(self): env = {} if self.env: for k in REQUIRED_ENVIRONMENT_VARS: if k in self.env: env[k] = self.env[k] return env @property def uid(self): return self.uids()[0] @property def gid(self): return self.gids()[0] @property def is_attached(self): if self.application: if self.application.poll() is None: return True return False def attach(self): if not self._is_pulseaudio_user_process(): self.disabled = True logger.info('Ignoring pulseaudio process ({pid})!'.format( pid=self.pid)) return logger.info('Attaching application to pulseaudio ({pid})'.format( pid=self.pid)) proc_env = self.env if not proc_env: logger.error( 'Could not get the environment of pulseaudio ({pid}). ' 'Aborting.'.format(pid=self.pid)) return missing_env = missing_env_vars(proc_env) if len(missing_env) > 0: logger.warning( 'The following environment variables were not set: "{}". ' 'Starting as root may not work!'.format(','.join(missing_env))) try: self.application = ( pulseaudio_dlna.utils.subprocess.GobjectSubprocess( sys.argv, uid=self.uid, gid=self.gid, env=self.compressed_env, cwd=os.getcwd())) except OSError as e: self.application = None self.disabled = True logger.error( 'Could not attach to pulseaudio ({pid}) - {msg}!'.format( pid=self.pid, msg=e)) def detach(self): app_pid = self.application.pid if app_pid: logger.info('Detaching application ({app_pid}) from ' 'pulseaudio ({pid})'.format( pid=self.pid, app_pid=app_pid)) self._kill_process_tree(app_pid) self.application = None def _is_pulseaudio_user_process(self): return (self.uid >= self.UID_MIN and self._get_uid_name(self.uid) not in self.DISPLAY_MANAGERS) def _kill_process_tree(self, pid, timeout=3): try: p = psutil.Process(pid) for child in p.children(): self._kill_process_tree(child.pid) p.send_signal(signal.SIGTERM) p.wait(timeout=timeout) except psutil.TimeoutExpired: logger.info( 'Process {} did not exit, sending SIGKILL ...'.format(pid)) p.kill() except psutil.NoSuchProcess: logger.info('Process {} has exited.'.format(pid)) def _get_uid_name(self, uid): try: return pwd.getpwuid(uid).pw_name except KeyError: return None def _get_proc_env(self, pid): env = {} location = '/proc/{pid}/environ'.format(pid=pid) try: with open(location) as f: content = f.read() for line in content.split('\0'): try: key, value = line.split('=', 1) env[key] = value except ValueError: pass return env except IOError: return None def __eq__(self, other): return self.pid == other.pid def __gt__(self, other): return self.pid > other.pid def __hash__(self): return self.pid class PulseAudioFinder(object): @staticmethod def get_processes(): processes = [] try: for proc in psutil.process_iter(): if proc.name() == 'pulseaudio': proc.__class__ = PulseAudioProcess if not hasattr(proc, 'application'): proc.application = None if not hasattr(proc, 'disabled'): proc.disabled = False processes.append(proc) except psutil.NoSuchProcess: pass return processes pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/encoders/000077500000000000000000000000001364015200300231525ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/encoders/__init__.py000066400000000000000000000117421364015200300252700ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import distutils.spawn import inspect import sys import logging logger = logging.getLogger('pulseaudio_dlna.encoder') ENCODERS = [] class InvalidBitrateException(Exception): def __init__(self, bit_rate): Exception.__init__( self, 'You specified an invalid bit rate "{}"!'.format(bit_rate), ) class UnsupportedBitrateException(Exception): def __init__(self, bit_rate, cls): Exception.__init__( self, 'You specified an unsupported bit rate for the {encoder}! ' 'Supported bit rates are "{bit_rates}"! '.format( encoder=cls.__name__, bit_rates=','.join( str(e) for e in cls.SUPPORTED_BIT_RATES ) ) ) def set_bit_rate(bit_rate): try: bit_rate = int(bit_rate) except ValueError: raise InvalidBitrateException(bit_rate) for _type in ENCODERS: if hasattr(_type, 'DEFAULT_BIT_RATE') and \ hasattr(_type, 'SUPPORTED_BIT_RATES'): if bit_rate in _type.SUPPORTED_BIT_RATES: _type.DEFAULT_BIT_RATE = bit_rate def _find_executable(path): # The distutils module uses python's ascii default encoding and is # therefore not capable of handling unicode properly when it contains # non-ascii characters. result = distutils.spawn.find_executable(path) return result class BaseEncoder(object): AVAILABLE = True def __init__(self): self._binary = None self._command = [] self._bit_rate = None self._writes_header = False @property def binary(self): return self._binary @property def command(self): return [self.binary] + self._command @property def available(self): return type(self).AVAILABLE @property def writes_header(self): return self._writes_header def validate(self): if not type(self).AVAILABLE: result = _find_executable(self.binary) if result is not None and result.endswith(self.binary): type(self).AVAILABLE = True return type(self).AVAILABLE @property def supported_bit_rates(self): raise UnsupportedBitrateException() def __str__(self): return '<{} available="{}">'.format( self.__class__.__name__, str(self.available), ) class BitRateMixin(object): DEFAULT_BIT_RATE = 192 @property def bit_rate(self): return self._bit_rate @bit_rate.setter def bit_rate(self, value): if int(value) in self.SUPPORTED_BIT_RATES: self._bit_rate = value else: raise UnsupportedBitrateException() @property def supported_bit_rates(self): return self.SUPPORTED_BIT_RATES def __str__(self): return '<{} available="{}" bit-rate="{}">'.format( self.__class__.__name__, str(self.available), str(self.bit_rate), ) class SamplerateChannelMixin(object): @property def sample_rate(self): return self._sample_rate @sample_rate.setter def sample_rate(self, value): self._sample_rate = int(value) @property def channels(self): return self._channels @channels.setter def channels(self, value): self._channels = int(value) def __str__(self): return '<{} available="{}" sample-rate="{}" channels="{}">'.format( self.__class__.__name__, str(self.available), str(self.sample_rate), str(self.channels), ) class NullEncoder(BaseEncoder): def __init__(self, *args, **kwargs): BaseEncoder.__init__(self) self._binary = 'cat' self._command = [] from pulseaudio_dlna.encoders.generic import * from pulseaudio_dlna.encoders.ffmpeg import * from pulseaudio_dlna.encoders.avconv import * def load_encoders(): if len(ENCODERS) == 0: logger.debug('Loaded encoders:') for name, _type in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(_type) and issubclass(_type, BaseEncoder): if _type is not BaseEncoder: logger.debug(' {}'.format(_type)) ENCODERS.append(_type) return None load_encoders() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/encoders/avconv.py000066400000000000000000000043311364015200300250210ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging from pulseaudio_dlna.encoders.ffmpeg import ( FFMpegMp3Encoder, FFMpegWavEncoder, FFMpegL16Encoder, FFMpegAacEncoder, FFMpegOggEncoder, FFMpegFlacEncoder, FFMpegOpusEncoder) logger = logging.getLogger('pulseaudio_dlna.encoder.avconv') class AVConvMp3Encoder(FFMpegMp3Encoder): def __init__(self, bit_rate=None): super(AVConvMp3Encoder, self).__init__(bit_rate=bit_rate) self._binary = 'avconv' class AVConvWavEncoder(FFMpegWavEncoder): def __init__(self, bit_rate=None): super(AVConvWavEncoder, self).__init__() self._binary = 'avconv' class AVConvL16Encoder(FFMpegL16Encoder): def __init__(self, sample_rate=None, channels=None): super(AVConvL16Encoder, self).__init__( sample_rate=sample_rate, channels=channels) self._binary = 'avconv' class AVConvAacEncoder(FFMpegAacEncoder): def __init__(self, bit_rate=None): super(AVConvAacEncoder, self).__init__(bit_rate=bit_rate) self._binary = 'avconv' class AVConvOggEncoder(FFMpegOggEncoder): def __init__(self, bit_rate=None): super(AVConvOggEncoder, self).__init__(bit_rate=bit_rate) self._binary = 'avconv' class AVConvFlacEncoder(FFMpegFlacEncoder): def __init__(self, bit_rate=None): super(AVConvFlacEncoder, self).__init__() self._binary = 'avconv' class AVConvOpusEncoder(FFMpegOpusEncoder): def __init__(self, bit_rate=None): super(AVConvOpusEncoder, self).__init__(bit_rate=bit_rate) self._binary = 'avconv' pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/encoders/ffmpeg.py000066400000000000000000000103441364015200300247720ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging from pulseaudio_dlna.encoders import ( BitRateMixin, SamplerateChannelMixin, BaseEncoder) logger = logging.getLogger('pulseaudio_dlna.encoder.ffmpeg') class FFMpegMixin(object): def _ffmpeg_command( self, format, bit_rate=None, sample_rate=None, channels=None): command = [ '-loglevel', 'panic', ] command.extend([ '-ac', '2', '-ar', '44100', '-f', 's16le', '-i', '-', ]) command.extend([ '-strict', '-2', '-f', format, ]) if bit_rate: command.extend(['-b:a', str(bit_rate) + 'k']) if sample_rate: command.extend(['-ar', str(sample_rate)]) if channels: command.extend(['-ac', str(channels)]) command.append('pipe:') return command class FFMpegMp3Encoder(BitRateMixin, FFMpegMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or FFMpegMp3Encoder.DEFAULT_BIT_RATE self._writes_header = True self._binary = 'ffmpeg' self._command = self._ffmpeg_command('mp3', bit_rate=self.bit_rate) class FFMpegWavEncoder(FFMpegMixin, BaseEncoder): def __init__(self): BaseEncoder.__init__(self) self._writes_header = True self._binary = 'ffmpeg' self._command = self._ffmpeg_command('wav') class FFMpegL16Encoder(SamplerateChannelMixin, FFMpegMixin, BaseEncoder): def __init__(self, sample_rate=None, channels=None): BaseEncoder.__init__(self) self.sample_rate = sample_rate or 44100 self.channels = channels or 2 self._writes_header = None self._binary = 'ffmpeg' self._command = self._ffmpeg_command( 's16be', sample_rate=self.sample_rate, channels=self.channels) class FFMpegAacEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or FFMpegAacEncoder.DEFAULT_BIT_RATE self._writes_header = False self._binary = 'ffmpeg' self._command = self._ffmpeg_command('adts', bit_rate=self.bit_rate) class FFMpegOggEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or FFMpegOggEncoder.DEFAULT_BIT_RATE self._writes_header = True self._binary = 'ffmpeg' self._command = self._ffmpeg_command('ogg', bit_rate=self.bit_rate) class FFMpegFlacEncoder(FFMpegMixin, BaseEncoder): def __init__(self): BaseEncoder.__init__(self) self._writes_header = True self._binary = 'ffmpeg' self._command = self._ffmpeg_command('flac') class FFMpegOpusEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): SUPPORTED_BIT_RATES = [i for i in range(6, 257)] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or FFMpegOpusEncoder.DEFAULT_BIT_RATE self._writes_header = True self._binary = 'ffmpeg' self._command = self._ffmpeg_command('opus', bit_rate=self.bit_rate) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/encoders/generic.py000066400000000000000000000105251364015200300251430ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging from pulseaudio_dlna.encoders import ( BitRateMixin, SamplerateChannelMixin, BaseEncoder) logger = logging.getLogger('pulseaudio_dlna.encoder.generic') class LameMp3Encoder(BitRateMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or LameMp3Encoder.DEFAULT_BIT_RATE self._writes_header = False self._binary = 'lame' self._command = ['-b', str(self.bit_rate), '-r', '-'] class SoxWavEncoder(BaseEncoder): def __init__(self): BaseEncoder.__init__(self) self._writes_header = True self._binary = 'sox' self._command = ['-t', 'raw', '-b', '16', '-e', 'signed', '-c', '2', '-r', '44100', '-', '-t', 'wav', '-b', '16', '-e', 'signed', '-c', '2', '-r', '44100', '-L', '-', ] class SoxL16Encoder(SamplerateChannelMixin, BaseEncoder): def __init__(self, sample_rate=None, channels=None): BaseEncoder.__init__(self) self.sample_rate = sample_rate or 44100 self.channels = channels or 2 self._writes_header = True self._binary = 'sox' self._command = ['-t', 'raw', '-b', '16', '-e', 'signed', '-c', '2', '-r', '44100', '-', '-t', 'wav', '-b', '16', '-e', 'signed', '-c', str(self.channels), '-r', '44100', '-B', '-', 'rate', str(self.sample_rate), ] class FaacAacEncoder(BitRateMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or FaacAacEncoder.DEFAULT_BIT_RATE self._writes_header = None self._binary = 'faac' self._command = ['-b', str(self.bit_rate), '-X', '-P', '-o', '-', '-'] class OggencOggEncoder(BitRateMixin, BaseEncoder): SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or OggencOggEncoder.DEFAULT_BIT_RATE self._writes_header = True self._binary = 'oggenc' self._command = ['-b', str(self.bit_rate), '-Q', '-r', '--ignorelength', '-'] class FlacFlacEncoder(BaseEncoder): def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self._writes_header = True self._binary = 'flac' self._command = ['-', '-c', '--channels', '2', '--bps', '16', '--sample-rate', '44100', '--endian', 'little', '--sign', 'signed', '-s'] class OpusencOpusEncoder(BitRateMixin, BaseEncoder): SUPPORTED_BIT_RATES = [i for i in range(6, 257)] def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self.bit_rate = bit_rate or OpusencOpusEncoder.DEFAULT_BIT_RATE self._writes_header = True self._binary = 'opusenc' self._command = ['--bitrate', str(self.bit_rate), '--padding', '0', '--max-delay', '0', '--expect-loss', '1', '--framesize', '2.5', '--raw-rate', '44100', '--raw', '-', '-'] pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/holder.py000066400000000000000000000116151364015200300232030ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import threading import requests import traceback import setproctitle import signal import time logger = logging.getLogger('pulseaudio_dlna.holder') class Holder(object): def __init__( self, plugins, pulse_queue=None, device_filter=None, device_config=None, proc_title=None): self.plugins = plugins self.device_filter = device_filter or None self.device_config = device_config or {} self.pulse_queue = pulse_queue self.devices = {} self.proc_title = proc_title self.lock = threading.Lock() self.__running = True def initialize(self): signal.signal(signal.SIGTERM, self.shutdown) if self.proc_title: setproctitle.setproctitle(self.proc_title) def shutdown(self, *args): if self.__running: logger.info('Holder.shutdown()') self.__running = False def search(self, ttl=None, host=None): self.initialize() threads = [] for plugin in self.plugins: thread = threading.Thread( target=plugin.discover, args=[self], kwargs={'ttl': ttl, 'host': host}) thread.daemon = True threads.append(thread) try: for thread in threads: thread.start() while self.__running: all_dead = True time.sleep(0.1) for thread in threads: if thread.is_alive(): all_dead = False break if all_dead: break except Exception: traceback.print_exc() logger.info('Holder.search()') def lookup(self, locations): self.initialize() xmls = {} for url in locations: try: response = requests.get(url, timeout=5) logger.debug('Response from device ({url})\n{response}'.format( url=url, response=response.text)) xmls[url] = response.content except requests.exceptions.Timeout: logger.warning( 'Could no connect to {url}. ' 'Connection timeout.'.format(url=url)) except requests.exceptions.ConnectionError: logger.warning( 'Could no connect to {url}. ' 'Connection refused.'.format(url=url)) for plugin in self.plugins: for url, xml in list(xmls.items()): device = plugin.lookup(url, xml) self.add_device(device) def add_device(self, device): if not device: return try: self.lock.acquire() if device.udn not in self.devices: if device.validate(): config = self.device_config.get(device.udn, None) device.activate(config) if not self.device_filter or \ device.name in self.device_filter: if config: logger.info( 'Using device configuration:\n{}'.format( device.__str__(True))) self.devices[device.udn] = device self._send_message('add_device', device) else: logger.info('Skipped the device "{name}" ...'.format( name=device.label)) else: if device.validate(): self._send_message('update_device', device) finally: self.lock.release() def remove_device(self, device_id): if not device_id or device_id not in self.devices: return try: self.lock.acquire() device = self.devices[device_id] self._send_message('remove_device', device) del self.devices[device_id] finally: self.lock.release() def _send_message(self, _type, device): if self.pulse_queue: self.pulse_queue.put({ 'type': _type, 'device': device }) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images.py000066400000000000000000000103661364015200300231750ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import tempfile import logging import gi logger = logging.getLogger('pulseaudio_dlna.images') class UnknownImageExtension(Exception): def __init__(self, path): Exception.__init__( self, 'The file "{}" has an unsupported file extension!'.format(path) ) class ImageNotAccessible(Exception): def __init__(self, path): Exception.__init__( self, 'The file "{}" is not accessible!'.format(path) ) class IconNotFound(Exception): def __init__(self, icon_name): Exception.__init__( self, 'The icon "{}" could not be found!'.format(icon_name) ) class MissingDependencies(Exception): def __init__(self, message, dependencies): Exception.__init__( self, '{} - Could not load one of following modules "{}"!'.format( message, ','.join(dependencies)) ) def get_icon_by_name(name, size=256): try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except Exception: raise MissingDependencies( 'Unable to lookup system icons!', ['gir1.2-gtk-3.0'] ) icon_theme = Gtk.IconTheme.get_default() icon = icon_theme.lookup_icon(name, size, 0) if icon: file_path = icon.get_filename() _type = get_type_by_filepath(file_path) return _type(file_path) else: raise IconNotFound(name) def get_type_by_filepath(path): if path.endswith('.png'): return PngImage elif path.endswith('.jpg'): return JpgImage elif path.endswith('.svg'): return SvgPngImage raise UnknownImageExtension(path) class BaseImage(object): def __init__(self, path, cached=True): self.path = path self.content_type = None self.cached = cached if self.cached: self._read_data() def _read_data(self): try: with open(self.path, 'rb') as h: self._data = h.read() except EnvironmentError: raise ImageNotAccessible(self.path) @property def data(self): if self.cached: return self._data else: return self._read_data() class PngImage(BaseImage): def __init__(self, path, cached=True): BaseImage.__init__(self, path, cached) self.content_type = 'image/png' class SvgPngImage(BaseImage): def __init__(self, path, cached=True, size=256): try: gi.require_version('Rsvg', '2.0') from gi.repository import Rsvg except Exception: raise MissingDependencies( 'Unable to convert SVG image to PNG!', ['gir1.2-rsvg-2.0'] ) try: import cairo except Exception: raise MissingDependencies( 'Unable to convert SVG image to PNG!', ['cairo'] ) tmp_file = tempfile.NamedTemporaryFile() image_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) rsvg_handle = Rsvg.Handle.new_from_file(path) context = cairo.Context(image_surface) context.scale( float(size) / rsvg_handle.props.height, float(size) / rsvg_handle.props.width ) rsvg_handle.render_cairo(context) image_surface.write_to_png(tmp_file.name) BaseImage.__init__(self, tmp_file.name, cached=True) class JpgImage(BaseImage): def __init__(self, path, cached=True): BaseImage.__init__(self, path, cached) self.content_type = 'image/jpeg' pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/000077500000000000000000000000001364015200300226155ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/default.png000066400000000000000000004100761364015200300247570ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org< IDATx$}y򪻫{vvggb8 xAҤDRp8HV0l* G6%A! ]{wwuYy>~UYgWWWuWw"29*+RJbĈ#F@1bĈ 1bĈ#cX#F1Ch=qC!=ιĈ#FS0XNS8'Nڵ4^l1bĈ#Ƥ!$$a᫑-&1bĈ1MJda*kM=Ĉ#F ;mց!韺؄1bĈXCXi 6ɠ8 0F1b QO 8G4^O ~@ /#Fav#'#F$y؀8?F1b5d> >)= #:CQ >$a#F1Ɖa2j)pi !Y*FH!$??J:-wޠ#F1bDv}G@( K. 0Q 05^5A3 ӟuP1bĈ#ưD/{u X &=@ _ZH*=ֹ>bĈ#FqbG;mØ'f P ȿ!y߹`pb F1bЌVw/g:|.~lc Tv!@Q!o 0BJ/- 旡[SW&40K`IPzla\0 %L*DbеBx7]וl6 c-ND"Ѵr9ZzB#-bxlb^{mww'2Vh}@KPA!G&h|tǾ(d" $턯x/^y飗]Sfܬ2ٜ'ŜRUѡ{C(*/o7xÏw_Vr]~z MhB @^/nݚUbL +Dq%<}TŮ^mǬ6]*^jTrh}{ ht D_k  ɺ"r<שBBga>;]7 (9T" 鳪ӥr񽿵wBNGaQa MU@X-0D5C 0iBĨ-|!ɧhvh;G`ꈒ~[*?¯_vU̙K28u|o|\2\h#~t:0<2I|6oZnVLt E($~o[8X3bL!V2i^~'w;x4vǩ\Ѡ1uM%YW?~jj:7?w(E \"$桿ֿH+˗/g?_rbn۱H)>K47}.S x7b?GwY FtgD5J棁b2x/tA H멦?q_qOޖ%"t&W*2nߺ[k Ee.ASh,33DhS}U[ZZJo|O}/~ԲJ&=}^@~ÛIOgŎvpt"lTN>Tٵי6?lkl qHq{!q"ҟ!edDpb sKsϬ?o}oiD~b/15>Tes} //R.nj1MB<Ŕ$y'f(B5^6D7EȈ@4^ $*B6KՏ/]^έ_n=Zotc5`t ^H!a-Dz=ǿ~cs\?~_}WY=|<7 Pȏ1 )A 2 { bHשy?ZUknj1]M՚n-. kw$^AЩo۸S+E]Q?x2.R4Bd  <<鲾џ?s//M,ѱX$,.9ۧO^D4g?U%~5@rʧ׾bT%-o ]x#A牆mg`hKAi{ͭ9,F#%dTꑍR$D3,~ǁK /ܝTh[iP*Bh 4PT3˚U[[_{u5Zi~ݱU`:tv0TPP߿߹:_'0i贓hK֭~?*@";?o^Ԑ]i[}NUP0,3TElnS Hu3Fl,fr82y70 %0ID)/0=ȡh0 #?J=v7ZG Д"Y ~gk,4E#kdY,עd<+N/Xïj{MBUjqX%BsZcNMq4.@4H% |@ KD $;d_k\;(P%p 3R~~)O?Eos_^zgaZ'ڈ{_R7j0X}Sw0rN%s C?,9ךI-Qmc9z}.RXgp6RJv]vk|ӌSc2.AZdCiH!2 Zۺ_y>` 7 +4-ә^qU7L{S?oZ?$΀?{Ej AIݮ`'XNU-=j"1e !;)Y6xU7CˆI+*B!xsk6u~" ӓqíۛ\b|_{Pirxf&-JVyyuEV =5|7 ={!&x _xY>sIД iEtU%ih25MT gWf|K߾oh^OwDƤX7@kIRYFP d+D2'=k;|Cvj XtTDe+PO_SA6rΟa>*<,!$ X+͓ԩ$WMj5 [ʕ:s2X!Vsث?yu+\al]p[v| &lqHN,T}cKOq۱H_$ϏPWa78Xo&Y?އւtrg>%,]?ڷh>j.O/~onhjHPJ9ՋJ8(܇B]ӉfqAJ6vXgfR4xɭ UQ83;g]L~w;,q2vw(LMS-ԨlӘYNB x UJx}agZ#P3 RѬAZ`ƚ18wЋ! Z-L.o{I^vo[·Gp8Կ~9D\aeiNUѩ;TMI.?U)|0pái-`-B2(8,(:o_Q28@C& NNSd4BtL3} !D3X ܷ B .W(,_>kw?*_%B!@eZ)Q:XDSr۹?w%`ʕ+3gU: ͿTi6|(f.k)4`.{幫\\y/24I#\pܐHK竳KϞ1/XE&!?RJ\ϧhY+UPwSCfn~ثN3۫AKqc0PA* lM;Vʶ׈ ʅ\Ӵ0.Ii-B?7z1Agſ5I+. .R M7l7w?ZcWX{*HMT*jT뻸E*cA `yfc2Ї`@wI. R)} UU?뼒Js5O.*񳟹? ׮.N#/ 4}O|1˅lt0}9_`r[Q9M?SӼAmCZP ht{o᷸])l 4i}:cl+$HOWICnQwc'!geJdVT5L˗ry,֩h[.*Aƶ=sD-gāVWu J#/\^b>zn_4hQdSw?_om \rl 7OM]f@.J>}1uVvw_wWyZ׫ cdVLh l?(: sn)a^/^Ό0.gOfKv;|N?֌]bu*톉h&r𩅗֏> @;mf@$&wI^:$O`;kE&"65BHs)o Ps=Rd2r"4A_ȿ;+~ZOe EQ?={l}HPP]M*F@JadE(&PU}`0axڌ?({|Vbm˙%1ҏڊ5]T' WR<)%W4-ԨC/vMKl&3ڹ6G v@!hh`c2 C]$?$tcPе$sYx$k8o`Yf.qfj""LsGг,>5DQDBY'f'>o~_箓bL b@ P+l@Y?ArrUoRvF@=yZJ6A!FP: b>gJ.@Pd2=Ҿ;RHG2]x nY\,dS \Xz\j<,\s+*<3-=8pql7~s`|MX-K3/`6)=Ń#jZuXHMf=GOh$"{<}>I@~@Я$p?4?D߇[@дj[GnOz.EQ w<5S o6s\zi cF-<М:.5-^yəc6wI>'}B@5O~x;ڕp%XN?:@_l6 ;-Z9FVD IDAT&SmP+T]|bi_="eZvy+iϨثpcvs\e^dɝ{؞feUXN!Phݱ/tB"CBpܣ>bWSY`!#}#9?in =:H$-P/skAD:֌#>.VvMv٫#9?A˩S_z;~@2wGGH?? 욅sa~^ټzI-Յ'yj [q-ꃯsskr{.erS;rx~_lVB0Nɫy%4Ea..UO7JRp/@Uɤ؎ex4l:z iFt1f3Äɥgxgcuqkߙ -l(XVzS`FKˣ% yv3g@[/?[S$!cy+|d,w+VKkcICDI>Gղ[l*S,ل)&O^Y -onQihmdui:DzX10o@)8 ւN;&7Wkg!d|K97qFI I2Fl"StWI"jxR%\e[`m EVj.t.;E,.dO_駖03$M {Ig7Ol<Bёk%2,~H>ItU#cdI̐KdIiZrtawX$+*mAJIͶ;'Ȓ(%5"wvʹ33\/];?y@4Rک(ߗxL bEoRo ECS!p=wr&' g1l~@׵9#tj#󹋀P~8FN&Eت1/}JVw7g6'kduGՁ+-{LR8B턯&{O' șS:k{%j=ZR'0n+ضe8x?QB:50:v}hb"Ўh,^AU v)Wq\րk,qiElt-E*sF8iQ.*G(T,kXn\*V<|1ZZW<)}bk'7BQ 4>y+;7Qftowlc};7ֱzͷQ,O29X/j"gHa\ZnڿR\deפfI9LpB8IYw1D۩v~gƦ={{X+o9+vX8rBH MCU<͍b~ߗlTo[X{T}8_TF[bL+#$>[nwp= ˩xv_AI'A%^FQ46 7G|"2gyXY=1O<'c6x0 |J}* M#[ѨV{klk%$rJ <0XH\f(B*8#<צTd}SlwDz+Ht$9;7n}DU7UWhjfx×iqs}uS}l$YUH/'`c" <0W'0v*hH-"oSnatEz8a)#ϹYyHv=_\z3%U7[-&faF1|);M:=/P 4@t?] )vkc]Kk 6i(Z"g9"z4`&״J`ϕ m 1 2_OPEN &( xƧGJ')_Q4҉YgbeiTEǤI<CKq~Et-y$ٮW.Kq D7G:8pڑO%ٹo'Gb؏tc R(hA>sK+ęW%%u7>QBS\XzT"?B]ʵoW5ۧ ,tHL;  jvMRvzR&a@MMr~Ef3g'<'Zj<0b#i"4Rl?"̿)5sOGL\ϦXY>>rm׭<&wڿhds8EȒ˲wkC7_mQwQs!tHYhT|:Eeuٓ B?}ǔ=]jSiinhzqy5¾gtr+g?b}}>`}?EU\s~g^o`"cFΖ9}|KMUHiC}ʦ$)F'en帧8F ouh^Tw4߿LfY} ׳vd Yy{By#>1{7+W6&2Fv􌻐PL*ɳgWxEwPP6Fh+1S8G7tӔ2 r@ASj3Z9 .,]'Zr܃c~c%tU1Z(xW"$ XOٳ+eRh}l[u۩1G瞉 >x_ߗ,R^dVǝt/$fx^鸧q1{iMuԶ#JM\fx˩vi(|0 âPOs2C?& 0nx\Mr{k(菔_?(3J񷊵DiCz5Y߽V6RJp =$ܨ#i8c7F;z꣗R7AJǣO*& ޟG,_: < ##:ԸiM5˝ɳ rQUc逓t*_d&5_|PR\OlUجn פ"mH}gl2ϬxsoN 2BϦzmZ pRW߼bq*cY(q&؏٨Nc_pրN߉GܻE69ӵ]vj}WGwY<%z0]Y.u)lWw0:-1B3|wcrd<žIhR%.H94-p?VuݫT"HŅ端u= `6҄MJ"N߼YH$UM3 h>e(Hh&]'>Rx'FDd|#wL8>`'ED>s8¦UL6̧,KxDяЯb5^At5Ddf9ZߢD| @U)]GT,ǥxGn>SNCc*bbНtgJ2{Rn խ"Tl7"A=`0}#D>.z"UiL"J>(T򳽠*\ϟa!vU͉6NhI>(&)GI klnElzF{jﳠbyn^:J.:3zYjdTAJ*[k=bm`Γ!PXf~O,^i{[Lox\0vV{STB_xa|ߡ{0MӠN߉\zcm=,J."7]BҊN_w/sգ|r?ߏpdE ڶP+ js$c,UmT-T "F=\gP8 fcL.ܽ}3V 2U$^r+RwkN ǩNIuq D94UXݠbny'dL2ZbYKx6R]_ k[?>0T 0g^BE ɍ@6 Bpr=֋e ssAnJ9}jQ*wHeӢd ˹ :~)D~ΠmWߧn}^#HFHby5Ul MY)adt ϡk vK;&5>8f9 ڿ$2uBژl>B]gg_dU{A~ڬZ3Dۄ $4(Bi &Lڇ€8{RJ2i2il4RJF':Br"ϏoǾc ∂%gR.?<"sˈJWqS&#I=مgtM3]'= "PH'灠o%FH}z_fo# g pN9 uV6FUtvJ(U7:I: 2z Z}okAxw !(?KsOkli\mC8}q4t tUCiiDA/5D;% z1 Lh ;X4xpbL*xi`;}FD"O.B2~Y,RwXnϵCZ" 7ߤnWTB!UsgEWbwãF]MRw*TV^`^tjwLr2¶SlbadF(u7 A# B6Bw*T/pеB)ŅAāaT|iQiJ.: r(@Q%S#O:N h-|ѭ]8NǩP.~H> IDATY\FKNSr֨Xn-`FzA?B($4—.S 8(CO%>oc|ܱ/eۻ%{ =-cd4 У\n/'MB6H=j!@"14#x6TExmۊEYd&sTA!QgPUL`n6&J׍+ 4=u\7H=,0rd2I_GQ>MG ٧X_Ʋ+ޠT]U>*9 f- % "Tf$gg&˩ dWXtQee W*:!伏@уhUpD00E0=K-*uL`%fٔA`6zeXPa\8^)Gsj͊-=0.@|ȱ K/_mc*5|Ӷ{PxEHM]G^K݃&6]KK/i)TQ15|ߝ+hA:9 BP3 I:Zw+kqEA&1WC|_9Uu@ &a MY=z}o gz|1+HtE֨*ā D@ղZ6)Cgy&Kq)Gåae9X7QA@SUrgWf0t;vN݉lt=2N@17{딊(PmF@wVסy2ƓSOa)U$v}K)8n*a9UL4F?$~VߣfvV鿹o# t*:g知dnwEƙ2͡#%LyH2I{韏X7t  }б<s?a }ve4gخ^:I$ f5J]S92/'?Zž9!@Jmm40NWN m)}jM,{B PJo`99|IJMECSA[{٫<  ]!7VO' z!OaߛߋBƜM!e/οʟwk`BOrBEҿ}~BL"P$U|| EtyEȹ}h~u,U04|Jjt<=3û,i/Ii=إgRS&յ"B,v&ku881@^`}uwmLe3 t=͒qxkRZywv)z8n1IbCK)zY;L=#TңX{YI/Vo7߷q =,hmN) :?)%#I J  # v=6fvܠD-_﬏܇tJe^x,9 C¹5^[#F9mnwAt]gjWfç?}D5I @[oo>|ߦ˖?%R&MaO:D6rGe'1tH) Rrbx:w+x-T)CaϤTpﰈ ;4;%J"E4׻Ub>;@九]@ xsĉJdhy򫧽(Q?ҲwN(?|knB}9#Ǵ8!O3mipp]5bj`l6Zn(#O$BiEa?DVťn;!@#}R?$I/f#WRJ[bާ S+Y]zڸvpiͮGHIIvV01H$t!:Ե,sģ?N"(h:VEpd#Eh-y.0.pϫ/=Ekp;\{ت~z5 wM\d&LT5y1jOMj ΋hOJ, 4=+&9))o#aoo}39MN(DDrPs'ԙzkp-]50˅ τN2Ș}i 5apΫk#t.FzX+!)~2cj]|}pŔ$g~Ν>؟6/Ar>@Rd*"O9r 4D&D 2]'HDPQ0O3<8[~Vb'xʒe [6֫]l>Ǩ^ f;Ey4K0&MenC@:9S_^(CԈ,4/=&iˀQn]( )fL3Mໍ?{3:Ѯ(ȩqE+W@uk` M 5sEM|w(ٶvKagz@>.Pܗb 8Lq|~r [ybc/"WԢ6Q]b,#QQy(N <5ht̽8`Aw[  cDrg{(<07nbl3k|9j En42ƌ/`yEc4:uޝJxD8u03ZNZ)PFQk:jQZi+Y\V u 5@^P6%`ڡw8Up'<%WHPVǙwyWJE`::?S 40jZzE\i/b !}W++i}j {w~{)l8eQw@L*xVp,' %w.Rt ?Qu xwpwiiOπ?ZBH !-q!9zG7dU#r"xƹSƼؘFS8lLO!"@4PćN p)|>:8|S+%9(Wsu6t$VP<"xD*q^wPyV}ӤE6wk\D㚀/jNiˀ(5;^jnj\LnSd1 Y:^n^D7n LcвLDl Ù6WC@b 9' Vf’R X2-}Orki˦lEp=y|Vr>a!€c{{ ,/XFFcԢ uqLLwsTBK;h"|.xs~"/֟1F-T*v1FE-^} V#wܯifj (X_)^zC\$(z6>_-sxIR4yUy|k9`d f81-O3ddG? *0ef;S;r"`~=Eo[Yrx^`kɔOT9}8/p{lνp^CA;8:=J0k:>t;5sW:=_#cSM8:tUAopvқZ ˉKZ%;]AA, 8v &ٙB_0a8:yߑA#CD4C2BE>0p`,'e+o++l7KU;ԍNvcKn_2HC$!⮜2Θ4O(~a_䅹{`̣Дד1gC|T{wdǷ~$a`xZq7o!'I $,]gy!J:5Dj!'n4n̹-wy=wN /K[Er Ctkأ}e[bioьvxtcPiAiI'ZϘsh6&rfgjٞ j5`L%GNYw~a"kBT2%p^s n~1,y =i 8GS^'^č[?>Wv5rΩ'-+P6at<[Y_a8yxfZ rO35 c/~$XLq}ĕu4k+jxT8н\0IbC}feP8X9A=͎rB4u5OpӃ)OZsR=߁z|X ّyzY/UG{n4pD@,`h~gktIEa@BO'hת5̈́QFMýWcٟlbgwz-Ý>& +9<)ߴLQ z#;߿ ydo|=30. 5G}R`2.ǣƨWШO_OeKϟHVEm_.e`u%G} Xعʩ eH ڻhOZ5sR^)ȶ?<4)`a5Be:',A`E;j;o" Ȃ Lk=`jʀqҹ^A?Y0Ьh5?KI_eY&pp8/s_}tYjo%ǒHRr/Qv?7o7?eU/iЎ{cN~[0sۺmw)7&x6Nv0 D5Ԣ2Y3w_xAߩͰOg|'r1.,Ux1Fgs3pq~Y鶼 M9%pt ٣4F8h X9UHn+g8seX0%>YG1nDW; IDATsįkۼ)P!5/9u k7в?-B'Q Hy2Y"E#APA,Ke <]?7a_}4ht߫GSD@5}9/0Xfm ^0p;wAշ cn~~/5k; tLYԋ'u&SA6 ,Ų^KQpb b*U3d"WKsy4C(P+ F"1ߜm<D@z X(߳@pB'LYa' &btȕH C[m|Uu<_y꓀0"0[.qn!Mn*}Dï0=DvsJB~۟l$`kOnҝp,͏1[} = *~h)#ޱS@T'W#0䆁3$8J.^YŬ Y|yy ͨ7J6agpR?]ǡ)8B:fVPV4.iE uBCc 1k&)aE `N" 碍]\WIJ/6כ{ Fٓx9R^HX`<<ų_,%l&w1E?C鼍˗ya`6;B, "G+]!cM~/ | eZ`(y;F-4=ԫk(O(x ie gLi `i୸Q l_ 1OG8CTiU@CW%^i^_ }Ц 9 H >q3ˢSve|!; ?PY':doW-qȀ:oa@ɛ/Z,_0g$LbƷt"$yZ`)0 IJ}=Јc﮽ܻf9?!H_k F°v-\c ß!M|P}:"_(YH aBIF~0n#=i|{x8w$ѹkG`|s1ǗiԄ(kձE:h>QjQ:ܩH73?oL<[ON0-oپVmd"s KOXg6r-^q3 OwdL7Ӯ͗yxDau^Yp4x+SEX!#dA1$5F._?_B`2˯w԰k7o`h G 5<{G'yb6;BuBdO0Q_px쬩xڍat[EUe(& 42#N@?oN d~H^T7ЬnVM)p{eK Y\Wo2Zuџ l,[{RAV}r`O A'nkd#s h"V,1$A 3UQж p}% }~3Ek$1 ? jy7ƦdY.mpgy܏Iykߟө@Zte(s)4M0à B7jF |6j_2Ɛ$} LҤϬ+J:@c,F,!Nb8F\iY@=^C`ھcu W}L me:* pp ;`j8߲%zNyka k70ZX7 GD8}^^SG?0}Pa Ya8K-%XpTN{LF_ֹ0!`4ȩ"Mʘ%!$pk+&† ֤FW^]F8c?g;fb )6כ;x56Gt|{qxpU/!qt5NO^@g!EL&{%+ \N*k}{ћ8iX-9j LZ (;e9ݪE6`rԣ ԢEe @O@.Rߜt[gN8A%\1FWi[.ԟcc\zhG-Dz@X6y:ZO\xC4*uuO8&cجo:q,Sqg5} nǀIWt=8Zc;AtzlcB]s΍oAI{{8˖oaeNPGuW}tN`ego` k"QDTa_O C@y6;?vq&~&-&@GcjG/9gjց1A?%&ܴ{88^G,þx-XrrRf!Xю\GzǡddhWtJPGk5KO h=kaeq,Fܩ;_=hB?} uߕpnFYϷ:L1$cz;ϴ}\.=WSV5+TNJκ _'/k/=&i%M󢷄9/DyqnH@~qfy<6,P ]έNC ^$)m,&XU6w-JJJ]H+  *{a€3)j9+yS6mN,P'y}Ĩee|.v(ف Y]8 clU:Dmr sp !Z2`?X)VAI-oi؟Lgt9.0ˈ}v}?8ftU:5aɟ{/-sSgLm$hkCKoNtIki&H V<4= YY^qi40=\z1塏OX0WD78j,ʴ~'Vx@q~sF7"~m/0=# ̰/L|͗3q"V[m{ +$>_c F^l6 U t1H476D@{D"`~unj1U'v<7&}z/! u:o9; ` n7&<>. }_*E"GέՒ*WXbn̼to"ӗrfhG7 _a8{ )YGOt(&4-l䜟 oӁ\c'{PzhW/![C%K* ,* щ87vu% [;=0V R@gz 8Daoq(%zq,`کJ-w/GX,¸~;ϴ(ח.^hՑzF%g yyl[qqH(N-#0ii6:lY@Ac~.4fcu4[HC_ #ܯ,rwkR)I"lԽB*/EѼ\d؛>l70.0 #W{ṞԈ\BꟀ ڷ$96vGb>ΰ`zƀKPm,D VêJnvc;wDGFGVٌCjs8$@W'@$(jU+psk>WDQ y^ыy`Z` w=Lϲ)OVPŕ֖֟?53uzn} ~î9W&!ԩ N- LVpLyO|On'31VBy9q/=FwZob[ N_Ut;1tYp+{_B S~xyUCo7=L}h:Sg W?!gg~ω%pΓ~s~{J0EU+=[^S1j54p^N8qޟ?! . l끩 ₿M#1 8üziri&J (-~AAw8a0OO`8R{̱DhTWN?@[ՙ9r9,yĀ1i[wъfu `A.`co<5D@7㗀6oJ8c ZNб@7$R @0Vg!fxcے,9OE WrkI8hlʕ;j9l2ϭ/$r0EPEAo|82!A-s@d\.Wbt–LBY3ڒN+ށ#3״)GB/( H 3u$d@.WX}_tC!grW@*tFv;6x:/S({)PW86[җ}2ɕ>/M&/!F+Me/?FAE&]$&rFe9|Th}8\nAƨuCHtUxi^t~zqC2qJe-8=vc uNf57e h1?869ct\g{ -?Ӕn;>yekJ|qL. sCΝWp}T#yQRmhL]9>` >[pI\$y9-̑ F]4:t;ܿo e~ Ьt,g-РM@$! ӾCΊ@K{@Lz}# ZT8qܷ77mۋdJ2OQ k!OX~C~,=*]! CTeI9g킿 %p:8͋Z8:6Zsy .v.2ũ;1S{}4偆PWwł5G|"k^փ BB\^ր%@L|?ɰZ8*MOkWx ch.R!p3/Gl8q*wK9obgkFʔ}UezQstm)Ƌ;u\s7K П4i"`EV%29HO]gyT իSíѕ}DB] (2s+9?gӊ5,%gy1Nl|y( ӻ!/7/:f, oUA.y-5Kڕߎb\Ӏ_‰o3$lqdkaj$ Ā%},8@)٪ k՞C*fԔq; 4ɳmǴguf_H0i#*A?:(sNw6wrbF9C#Q⇬nhI%p[vˏsLZxy*agu nŗLWWK(q^O X"karx0)`_".gw> $o!׸bl?q. Zn-nk?cnH`Z5 *,!s`\,p9v߳<7Ks!9vxY>4TQ H2D0ǒf i<< h;㙒f,:V5::@O8\;y!X湲m EߑΌ]nڥed^.Vvv]QJN/oo?+I;@hrj7@~׸u}t `<| Hg_B˵" jaSO}uze Μ6AyꂀYzSqK@3bRm)WlGw5g+/5\1 *MrHt+kg14ݷ\]Ա ! ,‹%րIC@ `YˁJ~s!%zၽv|*>~,ˑe9JP8G!up𫛸}mtӔxD0dټJCg~z t6KF}?92+zޗ?lKQ!Q9烹͗ѴXnߔKA|> 3_v 8#b\]A 1@%`޿ǝK 9yu2PgmAJ4`1z%Dg"r`OBDCk ps:_wG3A \MƘ wIr:AΑD ;ac< &t^D5~6Aq{u'1~w_Z>Y^@Qx`Z;{s  >}I///H+j`0a㹓59u}Kc.2MQVm}.Iь/ lGXµ n$Op~bb`@:Q&=`zjp.bͨF [ t2 Q2ҠU=5(ɂmץI`Oc`ND.9Gjs; d^"6&88c6~7A+|d`/* 6_"وna_wÝneR 9.]l}X[טM˦q,xՄ!"qjaX%Kd6'9Gt9k 4}Oړ*ط6:%0墸U@| .C`-D.7s4k^ܯjN€?Ae\x㍿B.°1&=kNH:W+.88P@@N^ )X"pJ#JwN7j.~+}07r`, `9gqst<~^eNxʹ q 0H.8 IxfܯU?H]czk1>4?|k$yNy,[,{'gPw0kk di nq>n9}L&|' 9~.:]|/gsp4홊Kغ9J Y`2EΞ %@KӐNyPp8ۘhAAދ/4NEgn:l~as,;DwSߦ2 4]s&22]^P_ @\^}UR6mdLN͞j]LbXG$c8=j'1OfHhFz W[ 1mڶ[pAAnB0uܺPYɎ@\ì_z4S2҇ c^S*9<@TSz6>6ؚu}}fSGu/ X|d`Ooa8Ng-.vE[d`$A~hO1aّJ5O /KOTZ~Z-0*%<-2Z__Xgs%A*榎`:R@9NeOshG@0/9*1)Iu0-⊷/ LִX HzuRGi Mߙc%u(g[VuT?#k1Kc{| N?u[4z/]`u21EO-8!0Bc"V )%/4Oqs|4B#[?ZW &ߩtP;!a"`im5؟̐hJ]0mKuɔ3+qѵh 4H@zIǭ.x[MH$XMtB #2 cLrNG<~1\`/sޱrb5288aa8?~%mȲ3SC[P{/;n+M@D5wIII,e9LUS}UEmK,k!%qgX"^2I-$e'ދr@pU?[vwNK y%DhbdÁ4')@Ro(@xJ-઼a~[B&RfmƁEjX~>YLB lٜ5u@A 2!F=ssR5nϾFd@GzCipQJڡKl [>uk}hxF`Oq°g œkq"{iJ#FǿTBOD}I8άMB(HÆ"7?@CWJc0=c>[,~?}VW(Pw=~LJ0e@! a߶@f-{%\e!0xx5{ PM5]寯&67erm+'?S%g?zޓ a9 .&' $ w[φדة&8a pmC݃v ҦGH"lz$ѿDKJJ];mq=.L`>K?Ϸ3,Kŀ_ ÛE=1wk ؛2'=?p.8M/bhW@ wJ{+`ITL6/86ؤg= k.k.w/pL/HCTU-G$ǃqZ6wI=) } ¢:LJ),. "\_IY32X"?F9|7syHaVQk}dҞ'ѿN'hdQ֏cT,B β Bu`ɴ/ ]m޻:@ 6͟Ux )@o:?^yG1}uyD^1]BQ9CE\j o|_U LG%g_K@0{0 B=*>Dj'B?ɄFi -`2 Ѿs+H\EtJi@wGgu3)v^Pu[q IDAT@Ow7dD/hx.$F+xʿp|ӯTb "R(Hڼw uNuԻ`ա^' I{7A4+LcW!mq8!"xo+Yo:x_ȅca{p;}|- pfCؾ瞑qcwZF .AX߾& 1 r ˲N ǿ~;,ckmv/~,gh?m=y_elpw?|w=GypR?}  I^Z _bN/AEIIO`ş B:e'}1EczRb$- J+/ .1`/7z9nQ6P :Rjr %#tL6z !$Vk`jdk{oH _ة',H[Ջ~O[ avB1e]{#vPieÓ _@$Z!899l)q^cryfqf3X);~̑|#DG!$VNASy:øB]F[/w564f!M҂hI쟀1 udwxc "/r IY1q;6{ 5&ӗen| 1y8 >34#F >Mq?ǟL'(zgɥ\1i_dyOyE"kR?ا "»omJ_V|~,MSb:}a2>9%ι#im`+:^z <,ĈFQӧw'˘P}+𰸎iu5R'#?S9`l}='̡A^bw `Nѳ Z"h Gjh*+Kp \,.򢃡P1fZcZ#9A0jL{9S)l +,d4j\deR\Dk"ßWTb'vCDΏη4հK\Zi;}O;!L4މa.}* 5}Id>NNVv^`{/o+";Oh'z8P3e׫ԫd>`wc#RYbn`frGZ^<< W?6geȲ]Dk!~EPX648X zF!(xH!@&u\Hquw^! l- Pd_+%Kp`g~$ LG /eG\/EeD$ < t._e89]`cŰH)[^W~{_^ ΌmN#ӳnF4;^}ӿxڃk*c|ZG)w^;ՉO8"@6_261/`eI} 'Ϧ >k׶=Y_inUDz@b4Q8-vJN. zo(ȸlL/D.Yٺ ep>o9"$݇v },}B8x;tݻmܻ`MeJe ]wDvn+߇cG_lbm!>qY&WRxEc8c#E9vnO_P?<@sZ? ʱ]oB( `r88`[D> zg? ;[C6M ]B 0bK`ۮ8$ş{}v<~AiLd~yp\=fVR`3a 2{Mg<7"d;Hag4@!Os?XB=w5{~"ˬ FoNaEel8 Anc/8} wP$mY VJ@:APmLD&@ !BT\`O68׹L )ŭY'HM뼿 H mJ+@]Y2%#n84?{7q1&'sll]ǟwO_=l|h܋~KY;F XA^&TOd7u >|L(GȉF:XUY࿊5ru mITS64T#i //knh,}[Q!*\]@&24 $2_ @[bY #9b~Rb`f3_?ji Ax8gg :]o tghiQv p^ ~?zLJO([wtdɾo8Oh׿ 6}JGe}5MܰRnkV?I`3ųMRR|o{w/oސ>c@I̦ xռ>fz(<5]:Ў{S=' -mi:A4hʈ|0Z&dOg~\Iҧ\KK;HFd~mda_|F-A$;XTJHCԜ5{ݨٚh#]C[\eL< =]ey71&/U@kE&[GDpaOv"ptjㅃxL8S l  u^" ?>EU7[ 1`Z $$I%PC=y^pPOw87jd!AFX&taG{%xP?g# Ev4 {}%f%Ng?H?ixECȲ>|~[ `* ppJŤ Ƙ=2c@epiB~1<xjw $oWc;{t JUEj0ĀwF;9ǁX&g 6j582w2Gx/Z=]/gmvVAB X ՃvSgV8€u"ao>ȭ#lm?ϿU8G0?w|J+p4 /d}| l^[ں˟2ý1D Iն"SvfUTWt; =ch0R_msRd仞gYϞ%+滋@ {K"`wA gsMhdRh`ieoDhEΰ p VK7H(F 1gm^w4Z!~y6m 1{T ~/nP `' ei.+Q |$]0_2,QY&5 jσZ:+B?>~wş?b-D[?`$COi9ly6u;?X)2GKHQa _a#{]K\1/|;+,h/U,~9 i^SO-_rrQ@9m[R=h ̛I{h:mfV~ptʉaWҨ8bݯ*֖at{ rFhQNY֊?:L0>_&mѼ?:4cgZ*ھ)RVEJaH1p^5fmEoZSWdƸL~8uyW9U.Vf|'ؕc5M:>ߔP˃` ^& 0Y5< ACL1J@-O׵E r'hؖ3 +pr b7e\>i[uz`Mb{j(e,O4~}D77sT:<\Jy{OZv.qCя{o;ZFEk\D9Ⓩg8,nҏY _y,ȤR29 B=jϓm}i *7zCu3Y/W<#!*V~p<z KA{b>-qܨ F Rh7T(0!Ӷ#uZ3ߵWlqzi4B^-R З[$0AޕتO8C۞esjAwR@Xv-|tn%-h.;֍5@ NM eesd\ Kx};*fXM2뱳QᡱV丱7iejmg4w%o8ȜZkiŰߓ~4fs_Kn^jz7DJk%?0iܤcu ,<G?0ѩ_0\l8fj~{g I^nż݋gxq PO}8 T ȥ$dR@) ~!8Q4Gd[5?Ѵo3sb=`N>m/ǭz)WK)p?iN* ",CJU],dN>ӺNi7BdXZߺԭs1)J]3 + ] )0r:D CQ<_*xOW:c'5r[&1.ˋ&뇱+ "O@X˝@@N=v-)dr흷ÛGa{2 kAS)fuBT3#HAh? ^lbD ,1ekwt$V#@YҶy%C@ӗw{/hik}p}_^"~b IDAT  ؟/3AQ.wf߀PhUph13oi277 "!׳s+ն$xowS=S@sCpoUUs+ BpՓ`VE8Ϭ q|zσg<kV-&'e*tŋ?λ8< #,h#y>ĭ[PQ/>ok@;~ ' ))I OM[g䄃&#^ -r AT2>m`j)r"X/}sD䇕֙'n)Яsc-iA$!# c#fx4B=\ Po?oC+5 Oad rnx(rwn`S<>}r1d&X.+4zKO)1=|g#ȧ,O#[ptNo,u,[53_}oܖ/nDbmkQ 8`pqa>⊵I'HC%[rh4 fD0;(%/h;4zȑ񅽞lj=ǘnr!!Np^3:N Q5{]69yzLN6ḬqRVcfFLy M'c6f{K%;.yWb2-Md8ymvmzwVâ? =@8h#pΤhSh:Km OHAqm q_oᨇw~:khO> >ż|r$`zL,4Kz89_Co4NEC9@V.2b |c.䄃rō?gy4>e˰|kbY'ʱ{tcs6Of! 31F8 mT֊̷,a+ۄP)<À,&Phh1?[ $s8[+pm;?p\$`Xdxj#1 XV@n:rXX3( FYb&b+wnߣ$g p%45~왃*5؞dtgKD/w˿v͝5 4{\T/B%v?WW3}" %2y* Fә=Y=}0܂$ Z(5R *s fHڅnS(C=õt:k@\(?vCWN8(9/X6&y+,Wv`ā'< NAx .n](^&"@Ck8h4 Hf=.?6 $r |*uL hR6G0I A`3a}WC# 7z5Y{'C2-J/K3aK ~gލyY0)kTy'$`In߼ܐ!6"Z#ƪ_=L6{ 1Z4w m`OYH k>vO\)N@Daxß=@>pH:X@7 t]A5TSAK@-fݜ1ДP6 j&'sgKc *s$KHY@5 @:j 4j^̠ PCU9TS+h7?X-LW 9`4@,lO#an",@2Ǡ|` ?(zE* \t`Z/QBʶA>S?քCh`N4^$@GTd'e= Yu} ИQ.`|.JLQ09rQIφRb w,xpmgp Q!鱲~?2TJ/?G:+a _x0] ) Xs<@xG`(Q$̛~ڣ=8`#B8X&34X6 3;I407ZI\9qy(ˆmsźDrùJ2rlo@4b`N (+s>@X39pg\~m9h-6 2&}΁ 9h{䖃0 ` @9_|V.n0`#3m>7 3<4(qaBF/!~ k г%GP;ASк1/ @狒A?]>Ґ5@U T€tPؼ$dCf}|Zb} #t J[:@># !]Bp@:sLBb} ifv:?P+Q1f8u"l~ſAS΀BNaN39T:vqrcNx` @ 3Gr=@D0k[a&qigΓfuGs`Q4ލ{;k}U<ev2ܿ[bhp 2 !:}4Zp ;/6{:X.sbĭ=P&`n:k <,to=օVhx^D?y|/Ô90p.^wo?do"oB%ԋFyt?ZN45Ta0|fXS xӊ"o6çj*@h)0GخE/Y|&h4:hPV6a@Aߋ'(]uu*A<]qDY}.IC(R<v_@41g@P.L juBlDR;ݞp!qw&2ٿLɪp_NڴUom|5;Zkך5a';ڸA ц0xbpe3^Vs+x!9/q|@iӎZk,R1 zHztkp9tWS4Oj 7xLc_j; A1~7;XJ̧{Pg F[ ^>X,K, M(+$$SPhq@c-Ǽ[}J⤤#Mj! D xrm b܇ C%J(%GX;$y:j[-d[B{gJsŏ}9Z76Q˛6cR}6ŀl7@OlR "BQ7Op:ɷqo4d `vn_GN932 ½Ҧl lo2cO? ~wO4o}Y4֯az:htc{т(Te9z |z8N,(UUp@,`>+A8Da[$r&`UG> d,@Ɛ[oBC*hz)ZFwϯ@>'e)3u;D/1}L0җ;PY99k y oJG'x H@1@-iꣲ$SG h0i |y:8-kLڷݵqomyA T4k _ݙ%@jkR'`ؗry`nܪ~+l ;TdZ߁P r{ !$J5`¥w\Kv $SOBk"`scaIt?]𤰘ع#no>_crc}^]?$zưǃ߂hǩ߶(O;TҨ\jf{ "Ȭ,/Bo<%g'ub@ݾZV+u蝁P/`+۰,tj ǵ%e!25+YvKk h<-1Njps}k`' =d_VN*L _ݝnB=WB\/nmj7mtZac?6"M56ǚ .}nh9[qiD ,mdr}fӿGWAkżݯC(4UcNm^eyZғ8|X[ [e2FD`Eu&(<2%*UA{!sdq6) @y Ocu@4f@Y4{(7}'PƒA 5w s  &mC7[ΚC@N}.[7q.dzҭޢ3YRu"K4ۦeۛ#6XS=”.V6/9(+5]B py&(e_ztVV8>Ymuidc^ DiT@*0HFB؏>~v3<9'˚t,hQW h^Yp֜Zw@fjoD9N`r\BJ8(.;"J V5)PNAdbmq:@Zo]yPqYA {[k4Y?ATbQR8[\\"{q =(Vq[s^@p=iɴӆ7]Rޣk` D<}\Ii0͔X$Xn1>q|8aъ89bmcH3Wys'ug$RWSnm D @ 5$5jU/@r6\Au 5!B% LtY:re-H BS߁l%l0s`Ш,΄/0WqR kkOod-sc9¥׍!CeJL`wPD;|;Z[,͑ lg}c~h]HkԵi/Lu9 õ+zf{_Gڨ-[kBY688Zö֖rNoka [tge 60; D K}wyG_57Gq)U`hL*UИOf).}UYdmA#r}᷵j t5dއ,FW!6ց|Pj-8%0I{D&NׇO>< wUgbAf%z9{7qZneO:zr :Fw.{ Q: IDAT5ɑMxwݓyd_-o!}Z |q2@8\K[X+!mc#,;<՝/cEZ0{Kn?AϜA0 LJNd_b9*|ۂRnN ti.[@>j8b^Zˌ#3 JX=.e iy4gk|m[XN&+Mh#!tuS#,B F=!-;j"LSϛL9` h4pAlxvZah̚6^N`]8yTc4wϙπ:a<Eܐ U`ooaG1B"O$kVm;ӎEV Xƣ޼c7_֯?lΘlJ "H)rel9n+hޙqڀ㮯`>QrfcZ 7$H U&AC34y l]E11^¹ae%eGt-O} E4*h]2r/$915*,Pcp'2//pz4[zt+ЧY,ܘ3">ehwV57]@CcR-28L_HOAhoY[ߥ$\~c 5zPKߠH^ǩ JW00mҦN7<NkrWWڿ{>%@k}cf_4@F؝g& ` 88id)O+j1Qlm~u):`-XJsgN]ߞ |1qLLhIdfA*)U|x#LN  6p7γYjNy~⓻&E^ w mkO!5YU'(n(| z/ȇmch}5s*( $0s5rV.3>砇Lzgfu ,Rhϑe?,Q`%}} 07qb-`H')2ZH59Q[ ܷ[/ =TIYcR5X%جt <8-kH5(&r_d@k`Y))2kw'qJs(U_s~M8>-wuFM8:!toK"ÕKjc|o;w'c읮]m'q}>fI~9+nrk+u$PD|`JFq,`sO뗦i YLN fqvUx9Z5PEm ;)̎{'aj@/FV$Bn* uC#dJMVKoA4nd45澝Jd4H^Ŭwd dPcFMbă#WC14 qN@Biy ACSɂF̰q_p dB`}!_rIȣ @6&{zvfվ'yޙi6'H(ԕG=Ό*eẍk5sC;-J,;xݡpAA~ڼӄ2@GϯVj" n UlE( f9 1k2 n7soMc,J rRÃS+eJe ] ⺶6ڗ5+W1Slf]ٕD؃%A720:AKQ@*Db Z kPmˣrh_C^+@7]9EWAR`|<  ,HSܞ 0ڢnOK@(0;jBvûi·` c]+h&aη?{~xLtSzO~EWV~ تir>_RM>mZ[7e{voy!`kcn"0ܿɵ/}$2$"|Mnk-yVp|wrE-޵kzpkcdmfdvӞLɗp{z 'p.joX2keL`,>C啴߽ibt0Ś=?i]]2 6EIHǘo[PDmN:F٬-cP:l3ҊwFxcbW~܌?V b]4r 'z,.Hx}ׁFܿ;i=Yfxllo FƊo v?XQ(6mo4\= ~cŌ"+J[azdm @wL_ɕpSdʶ2g\~gbQ/6U}7[kθjţרnuZzySX~Ǩ.\ jR crE~F( `ms *5Mǘ  Z&퍖KyPWe" )*'F?J"w?sc}sDZh }+]XY}0A敧^&'P0班Mn} BYZ8HVS۶u#6t=Ku+;}`??`s 6:?[-,A ;WR.Ybٮz}Oh[=XgiAE0uAO s`MA ` }5$aY@yD%ch=[oNÃvFY>omBQa XBr,wI"B1VHS*׃DиÅj!P!L3Qq_)ֺG!î7%Ea-._BQL3ؐ{͢(-fq^wm`< 6h6 ME[j@ok">6E“gc ΰ;nuA~k6mc,(%L' ̟?d)׷ {(,6`>KuگYֵQFPgP>me-M޶/lVbzco{r@laLe1Ġ'kkKl:G#tvƈ͚p "Dv=S=[Pu)[y11_1R jW@ A,cb h~\Z*XtZqJH(a^,sً[|ٔiђ$-k x@ֺztz-\d;ͯ?-܎sNxG<>v[Ѭ.ЅgJȟmȕKJnubnY+nAw}iێvޚXIYe1:v`߆ͱCdxޅp d]&[HQr- cqjuG0E1z!Mf_`m]5khɀ@k>jr=  QI*e`ǟ~|+a(|ɘ;clECP) ,3iJQk@=5ҁ8?r%oBKM[=>hYw|Ŏ=lC@;}v\Uᬞ66UhvX0 eR`wh ^;r% 9y F;8D= jg" "Aq=u.QZcS@o֮C"G>~7@FSvvu2_NB)aNXVQ.`XHҒ?elV\7Bl(V>mVߜwzZsrdz<jPXY5?4w1Łmŭ8g'rӔ,/aI,v Mrfq5ز╼R.QDc/=Yi4A}ZUo͞彎4s|OX[w: )ӗ(=T|"ز)H<t >%(T@}Dn+KZ[, 7, Dn "(Qt{F:-'4w}q+TgZ0.[9_};' 㲿 Goko[7,+%)d=+V&*j߀b!G%1{vē9i)J?G^Ii )F}&%)[8{.W/W [)å͢weu][}%{5~[[tw à?Bw'hރ`"P(G7P[HP7%PdH0ŽvЀd%֜ PG@q2qa;;<4Řg(}EPhx*P~Gf3)r~{Oh]ƿ$/ !I= =XࡋDn];85P䖇O?K(f;H5y”cVGlоJ1)]n? "uno CcbIFdiȕE]kl[׹*μ؁po60WБz=&\>\>CS6zt a~ :"ɱ6 2n)UO1rHvH)Z@c1X[ vFXAB ^ ?nCd՜%49ǧ嘌5_|:ȁ^}[_3c-'<|W[<Q ֧D0R|sT+"05{;%qrtƋ?x23 Ԇ~e i3GW %ʕEyW$;t+?X޽WּnĬt~{-a@[,)g0x=4sBH0ȹviG/`mKP2WBZ-iYã?~(Qލ8fmox,43aBv[:50M `(#ͽaNz4~sJ޽qV1t,`LGn#Ung6qKv<+۟o\WQ.>~Q>Ocx+@x۴qչ]Ts3 5m[~w5k5(tdNGn&wQ gvgKl@4B" =& 9KCzW(i{T:?R\)Pp2+[݃%~ń܋qnO6 @}m= R~ybiUVG0@[bg9G!YEi\`.MpFXsyս6at6/I{A?eIZ;+ [a?-͋6q#!EF4mjt7>1](TLG{S.8yr([u y{`KX\L.dq\J+tywh_gB-^J}rp\s/mպi4pq/km;&aL&?\یyXUk5 d6:Y+^Y@'\?G`& `8hKWζr6sV/$}e5YH5?+cДQ,(O̾CP PcSt۬fe?$qL~9% "7+~4`OW)-J~z0_bC{%)M8*҇?(q#ŝ=^P@2{1ۄEV F:WA~^Q8{|x=z?!"eMy R ?rz3eAWa ݓ˗k7#U}w|(ݳ UfF5|>p@ȨUiWR{0_:/ ˣg~1kP;qNQF~G>Vd}``jP؍K"Yrpl= IDAT/"+0 -Τ𷧻WZږfq;6R_)!E\ɒ/N|7-/R)'<,EP-mB+P^t%lPtΞoS]|<@ڢ+ J Bpr~P .U+'7GZR=*[%BYq%$O3P!j!jd|3\|:=Ji>ǣ(~Vr() Iɬ A WA-!v|O4:imKE(q2ٲ ;4_^0֬ZCd+RܸǿǷxSg1OkA_޾ f'y\ gCd<׵,t! ^ğXu ԫ#{}=Pk+E!*յLBUj} FPX bJ)~5lSg k?{5?zf煳ΕݩvQRβE wãyAYVԽ)}@3,D[ Y0e98J, Ea(sCQ>XVx4\Bgͧ:O.-WDQ)s|8#/m"o%kA > *Y/pxxuuCv<6ʁvE (ժR:ʄgTȽJ)QK(ؚ >BY6UUq,7e -X-%ǵزL ,_f4-0uEV%yQ%뢰/Зļ/bJy%r|8Ôo+W(Ö^GgW: ktJm7ʁjUkVݶ*G;pW{c ;jۓlC ⬳諬^a if8i1 (eP` EVPd% ]$UXܐYZg%YV{缧7]EU0rr4yE^g[ @Am0Wr!aBoVtkF ,wo m?~ \]]^$D1GH0BGMk- vV+sI, cr-WJS<-JKY:ewcݶ;U4)HtYΟoZz-_q%OҰ\F(s\1$4,0:jT `kBk-c{#KX+~5Ʋ_v;w.׺۹! 8Z=],g9En'ch~S[N([~~[oceYF$,-IӢe_}V.\h~W:FLk_K0S˭^:oINs Pz\[n7nNmšFSkl} O3Msܰ<· -VCCcX Sxo'u F-.[`Yr ɢ$߹~mvuK xUȥ0}+|d Ա:=TJ.8OV@kK(2{~`>ЭWv (lP4n+ֻ*EL00!`eαfvͅҒ,rҥomuB$˂dQ.seb,V};&yӠ}^7;J,y2Bb?3€-0F "$ F$ve }O{ӻW)0+S\5 0Npl *R\A.Tqc=xW|h%KK|dG۱~KͲ6=zW~_)WVP+Ĵ (mS}uAӦ5E*=Ā+cWU<5%d L:tMN![)x F+l}e |zGA(sS!O ܱ(AOCH3E||@Q` CQte x|cW[h *3^$W D}A މ\[4~jc F$tQq@#&6 +t ̂J!П/H<] x9@tYIxF MM~k[~ qiwU AWZ0,UPd+]7ŕ\ɯEKkJ}"šB9*5DZAxm?zz_ !2@a ) ː2RDlv -!ز&lh|vDĻzӽr`[|@iPS,qYʛ:Vm@5kʅв;ֺ5{xţZ-B~-^lht/s\)&E$d̲`WrqkZw1r16= YAʧcڿWI(Ro/+Ի0FѿEQS{54z:E@=D#Mҥ1-}P[T;l KOh6 -vbu ,愈:PģHY6r ydsP."uVu-r_% te:t'p@Xf #n=QVf_~{>OEZ qlJ4Fu dr[ۮO9.f:PwBFG&mv@.>i3жi?'f< [sȅ-Z~L8ocV|G|gy3W eɊBLk~!WDD+t .FE  wX)Ux^oZs?Ֆ;[b%6O!_b1>>xK]Ք.oQն:/ym\: E7Qa "x'"Y$4UeO o74cG9.KEXorzy-eaڔ[֊x%EV}UR1Yv+]R_k 143;ɯcoHy 0A)0*ZT*8Ж6[b ًm6bABٽv#LArƷ)2_hrqH ,iA?qJRo[D`4 G"Xs2-;$872Q׭MY0U`,}y\WKugOMJ%e YO)F`mSQ^ 1*ځJAZ![<ƖM%0V[d2w?}үg:/cG so oP1HU "RB2+(A;ƻ$e%eeYWߏ&!*P,gh%_{A| Ms }X_fO7ܧ[kנ@7|v׽&4,) ^>?Aih*-*W /%H_{;|V#@8pSD):D%|S0Qm[Q  xn?m~^iV?֢FS_?}R) l#YAo`vPk_L7B?ʸGZvC)dCE8Gby9.$Rرfvge{" vY%)ym4V-vf V@sK\6R.ױ{12;Y=W+-inMw+z * =d1bpt ?L=zWQQNxF]jVLʁoei&%&~&/!3_΍p&rxqHt|b^LI Px/b2-<)ɖ"B4^If9<𧱂i̝P[:Z+nع6"J' B/Ryf}W;s?ADpc͏w2>jvC0EP)_ul!/62/ZIoƍՔbQG ,'(Fô٦,Y8 ƒ5c,Gbz=f(Xsܕ@ J ;{1"7,YN2C?>Ұmm]\h;$z#ؾ{l] r^@z? ܿ pݭo_=}z@JE瀏W E|7Qfe 0]34wp:\i]_iE~w__d|%@%ϟa)-Eki仟T5 J(~=(yB0DӐIYf%pʁ{(`^pc?" 9YZ8}eߺƓ8֔ber.r .p~%uX;=ײֿYz_1olf S+0Dit~ȳ?y_~Cc`ֽ }۟"a쀾* P@|RtE .mpQ>xy) >@vw!Xgl-&>c?):)@OsEeZB^f%:Pv$u>~K^-yV iU7blpmPgeޙaZOC K Ҥp7/K*k>ޠ_^,\Yo\)oX;֋PUش]HGG+M&K)?Qj?k,[̽bcE֗{6[P7 [Pۄw?C18aώWD woWQ` @Q|5:~2~!lS(`Ѫ֟|ƒdN )h*tJ@)EAx2+ɒN)J+i,gz-fQP0XwkIbQ4loX_@xD2w幡(ʺVbmh.--%Z}ĕN9y~_ɥɕBV8K!8_zQ0>7?U/UF@|Wxi|BZ7^#92uRO]k\@@!G GXk(/e=~"/~똔Jϭ,M2U$? w]z)"YiIRd%EJӊ hNcqˇ3}sx?fzcD4j ƐӌQؽ3[.h^?-fQ Ԉ@Qmj׼~ֹߓpᦵA,hc;e!&^l[վ-qݼum]{vMPOr~yP~~ν׵ a-ΒmꬮAg{w@t=&q>j]`v`^BL([5{`=-?Ё9`-6/1˔}0[}č 5˔`9@GAǚbY !yRrtɫԾ-7u> }܋xgst+DʍX ePsk7\x2a(( J3փOk5/i3Ҳ>V@z8|8ԗ8zoBK+୔ya}ֽ?'K7J%Zn@[MGHgb,B 61X#Dx|@)-$ԣ6 mf&֗tnSI%w\ɕpR):(lmbFןCyvJD[ɗȩ?t-z`w s9~Æ\*5n*^`Jwn4Ni@H;,*k ޅܱÓ)ǘG5T|KU$z 5 ?Cվ^iI(бA49X2= n{_ZNJEah'dq2{dδcVk?9&=]-2?z2#"sCܐ%;!Θãti)W?v'tu4:~[l"Y[<ѹHK[+*iJ~R.Q1,N>) lؖ"w v@?QH>׀hRV,Rkν Zg]W5LH[/Xo o}L?*_[Կj6k -C/S(J$ш1>Ѱ'n6ߺlު_uֿdFqx=m2 9/yReB|=FY.oRw_g<[imw U.(}0!,(29]$ k,Ea9K*~swD4)yd47z|Ϣ{.ʜ8[ple#e~BSb1cQ:WDA8"R)y::S!+mVܣr[k>ޗߓ?E*]mGoJM_Il@{T*0Gz:{e`PZSwo~Pmk g7kAvQ;ױr IDATkKDOi''Ni !klmyBO>.Q7 x?ާ~\)B!]m') qa O.ѿ|d, 5IheOy N@u[XcMBH8NH9m 2!g$3qJ y^%%AZLS^<[x6 C[o]v2߁-I_o^m@Mػ1'R9VWKwkmXk!;7_Q,(qP* #_{=tsk n_ahcHQn'ܻ]Gs~#޹!Cl5TڹK0]k oCJӝC[ ?,:#Y|%l * Ot:֨Xs)SykY<3C0d3Է( KxJr4c֘ c@ މ"M2ȳ ^YH. "(-ţd˜,!4p -lNœ(ZXqVzQ,1 mыn-݈93LkB,͙ܙ [$BJws?3;Jؿ=!U'.S ދx'Dgʁ}u`64S$Xd~Ip _,]~$wJI%/k+͚-V\mYo+d-AO9>_zvyD\ϵ۟q룿g9{h or/*}XlЗ-xz`ANL|y6yLڢVzvzwT3*|VסEHS>rP@<ƚѱh9=<E{sh+Jп%)rjQl č!yWP fL^QB? ўSf?͘ܙRWz˸4 zkh'ޗy,P$/3ލ\kosj;WJD $$O]"+k@ oJbPd)(v;I/EFʻZ0gun,?ox ݵY'W[_ )s'ϙ>"=.M_`t~H8x?P&KvQ,Cv(u@ݲmbEOQ]- 4Vw "9q9vh# {0prcK7C$c/N(uLnB׾JcGX "glfx;@K1ϙLNzx;7x8!L\Tb5Q JWFiEBYŊI xy"-$(-qo 9>yprgeM؇ b,jƶ[^ EՈ+VXEᔣ~:%ޒioǵ [)Pu}x/vN>etc mݩ;(J ( YdwHg/<_{>LiekϪkNұ,enkf |rg5fI3fY+:=;/ځ꫊x~jm+V5̏?wng Kw*p{D"woL0k1N8V"+$ l{nAJ6NU7@_qbPU&ܻ J{wH'O`cu>8O<'Oi2J@+@׿-ڴI a)ݷdZl,C[%hm+c;1daB5 @Eӹ:LKi@EYR1 wF,NR*&\^̉=Z-n"pֿS 6B̉GDm֠ b]mt|b?"4c>Zx<EFխszz6myl^tȿMN+֔$#űr/Xƺ.} e=h"-GJlvx(1]w,=ض,!|5b{Jut˜xGu yb )9}_E w!`vrœ'NzJ@ ;w=w+s9ZrIb VGʉ ;7@Eȕ= N\O'M*50~1˙4_> ~sy^uAo,#]ēj`qm9`{}3luZ]Ȓts(H)5N$aY{&{3l1`Y z^TJ޼ jf Yh>a='9ăXۍVلN}9{(Q;h'ٵWc*c`0 "^#uV?PW|Ur~HT4!?YQ ބ99@ 6~|ޙvױl|LeWֻ l#Qig?WrQm?/@Sxp"#ǢeQ`qJiA(crץ6bOsƻ!#g=ޗ7/ȓbE0aqRCEV@CY>Sz@~}/`u wv#v~ Ϟ/yy\YklkK B2GG[K0 5˕Cxx*߃uZn(hC|Ϙ"k6v-_EgRA ;?4̐Rc"0&ڿ'׬QUMr`аz;` }Z7$(׊EG u|A'L}E>_f}nUro-ȞLNW@)I3(@!Ix/zN:ܨXc)WdJfycEG{_῾#wY J)5`>|?\~mѺ%2C>5btk88L9xdH ep~5*kEmUmI92":V՚lg$k;RV$d*֕wx/2d]d'ʢ2p<|`KuÕ /\"NB=Ȼ1ݿ@+Dœ7 F^ 83MVcX+Dx*+ nl{wF2o>I۷&йX٥cs6 g3x[a "6v)Iu_7I.jA*~.z3\厗"@i3ګ_Ƙ<"b-@Hcӈ,7cUl,rUÌPUd3nuBB 7NN{No2L ?arrQ k2N ^ ~5ր'.*|p%<;||6Zcn x]bt7}oMh}A@j+޳.)؄aMh5w|s,T8u.FA+(}b&+B.Q MiHvABC2u Ԅxwc! ](+-x$e 'ʤϵDu/0f!%>F*??},0$[1fSY[}q E#_ 5:( T"=gT_:4de*o9"Xf\"i(m( y^`>NJ@ƈ77[xii?Ehpy pa&\__C㳧ZƓ Q$kN0`f{=t1PV%Ff8x~U!Q 1"Ĉ B⯧bYRXG ݿ@h'@{`/׸ A4q^cҷh"2 zT==Zu!DD1@ݸgA 4"|HV"1'[7l:×@@%Dy)#"~Ѡ !Qm+غJ r>׵ (*+,>rL+p\{nY[m?egCYJ,+[W{~FKZ -]"j~ *cN˹izTZDD 4n7E dI'%UXE.\(K"/P$} k_NUEQ!I"I42:i, $AKwePd%Rm[\E^bz6lb,k\ 0DqΛH++LGz"ı<ԗmw2?};(OV1k!Y|Z] N[3}48AXwkWrbTUL7Adm[pu; UI-_nmiHn`:J V rǃC%QŪ4Ha"-q!f^7N?ABF2#1i'#8 P{ $a0x8bcWt(ʂ).K YʼD,Q,+Lq:+p4v['RVӹ / IDATJ- J`AQNFU&;B wo~28:7pBC=e6nWd 4񼊉EmV. ru[-u$kg',p΃ v} 8nN^Fk;v56Rs}T'V6'#= h& !OD(3d*[ &C+dvG_ oQ"Pb^lei5[SpogS wy*yQvH (Z_6}#;93K/3d[0߉fB>͑tSDq&_)wJPjM9Zb IX,SvU`p <_qy?P y9>M5Շji(B=I pr/` ۃ@{]XWX̾WWel4ǣȗOE_D|AqKߠ`_`9;Qu1 $C[H Lx=^j D727dK/.]Z6 -F+@'`ZxlB@V%h["@ r1(QӪ :>F~ig!օh$#]%,0|H/8ʝfmI(S+@d(3rjB{eͶāTiGA `.)#W@oyL0j4A~Z:Hk&Yk? MO,(u_>yF,9xxҨ5N󕦯Bȗ}zώv~., D8ΐ:YQ_@gezC; vYgbz1 B'č ;o`5 8; N|ܩh9ǤgNʼnH D#(2H@ 2Z0@#:Z(["Q \_=3ujM>ohkEBҗ$B(Q4ŐZrS0`dA߀OJ[Ȃ[{h<XH1D'H&2{nD kT9ژ/P5 ‚^6ۿm,%'PA0Yy=^GV0 <3~RhtQ]@Y4庬'1;*J0~1T *!~3{_q񧝵wHv0'jb9G]P|]_vhWU7ɽ`Tv'k^e& .nu|@CBb~cBP#w/xS{ 4|q@5f. }B$P`2ϱ?rF{`?P{*elaڬ[xuO@^Y%[Z7{0LLO1;RV6% #u-IKE*όuk ɇ5jdH̀H-%>9 4ߑu~ J tiݩjՑ4-E;ȕOAE,Z7yFgm*,;| P".#K֦mZa{ $Ho߂qO 4uq YT#DGUi⎀0ؑ;k`Cŗ%]Hl0rU!H[b$;8MM@ mNﴑ2LO0:fsT?1a 2kXZ?ό+w^ f n @w]li O-\YL JUynOU _@Rwo݄L<myyjV`µ b=3Zb}-EbtJ A';OKTaFPFX=|tml0͟NvA;'-7b1!PY9.aخ>P]h Sr$P*(aDȺ)4&zU}@Y[鲤9~9ł2<]$8X`I $)D.׀]P)7v ϯ.uԎ,>]MM]`3-ԘyIP^ct4Tګyy 惯j [ +f޷ua5! .E2Ⱥ >i,3xl.(iYoI0όv(=nd)-ZQNOQ͠Nޤ jˠOK4u9`3q#}!!J17lzgF'(P t3"YJ=rHgUO-Tj@UYU:D~:)D6q9IYS+ &c x8,v噣;y`/mtdAX.@C6~PiKs7Sc}眢_dO4X{<pel_oWbKeDꜛܗ%KR{ŃQ8`q7jPPWr?,x&P(3|r7u?Hv^}o(휷Kz`ܸ6j0t @qd)]Ryi`daY?R;91kz/\ 2~ꪨP TT Gs~ 7ntc4Q4Ҷ aL1㚙XfBOKDW6dP-n\?oZuBq{H Mse\gK"?b 57s6 pΤZyf$bdd k&1ZU(`fr D)s~rUK/Zbi.d^O۸f!:1y W9C; "ʬr>F, ՗PT;erGU&l,2ȳժ̦@=1b6)3D eQbyowzc.HRdDIǖ;6 xZHY;ЯnnԖXg>safC+%UuW "~ i_g_I(N歋S]4Mw,z Z/:/+~rv7|ڼ|Yxq&F"V|$_:8vH9*W^EZ< a7:vg7gx'#I;; Zrs~_MB?{ oyYooShj"gSfYB Q@fۂ~U+$˄+5)z=ex q x $?DFGR&P`5 CsզrC:B&a;-s/ML&X_k)ʼB>+vou&m{jj V3x|?yhY+SWU_'$T+Zjg 5Ysњ6 }HY(vq=~:[+,AQ[{X}blwݙp

WN_YX} 7_|1E^:}$o7 _`+&Zh 6ٚ)&1"\4a˕+@h wCU>d^kl=߈/XܨOQd;93 c_6 ESG|J+RY̺<aurY*_%ӔN–,`ϬU  IܢI*%K0;cC; (P %>,5=KFǀ_1N2W[N=0A0\Fpޠxq~$F>vnr18u>{ۯ!g?jzTgcDMc45rY[?`{anxV_~k ֗ Խk)"b[ӀE)^D߄DI/(w@EQ,6Zޞ5^\ +Mzx_%IÁW@RWjl<ͨ=GV'h]$!;^ge〔׃Շ-ZU' -LLq ,-OPZN8.OZ7ep\]#$C~J{fySWI(UMFuB@3kA0g_ r6G9Adg QdAKđXBc ʭ` EZ˃cD_H 4@{L m@LuPRUayDa^c1i˂zqt&IhEY6 HNE4yY/L}>C&ROm<`w0<_ʼ{Q*Q -||.IDH[{ýא/&8~.f S-zկ~S/VS[XNN/_^}kg&`@[(b_2Vr4h +HJɳ6)Osk>Q{t6 )QA.4A 8 BB}DHww[c=^?!JrŃCBK/CrLcPy!2`~X}?6HF`JRL>׆3'+y00ٯ^Gh.qAW.~%.7sM.`}&F4ybxj=[OSO[{䅬KsTC]Yo9=D qap-_'?zf(@q}n?Td_hBZ~=OOX lѬMf&@ꎋxꓑoZe5*ݾ zb=u4@եP-x5f_V7I~_ӑnr:Ւ@b@2wPQ`vo7gve2-yYIN"`u4NRxk ӭ!/5g_ReU!PJLxW1@Ns\zsg7y G[&=;82'3mC=g)gfN 6<= Ubz=Q,fC{;0~'(g 8|"~AM2,e_h LTOg&%g6'e6$Gב#ζQZ(#Ty g@%[s3)K8pdޫ ˿w$Bot}#l ddUautjiũZÖ ='ATiR XU%Q, NkCd,Ŏxr+ `8VyޮMK^= 0sh a˅Nߚܯ³ uj>g.2rvTxw/0\ڍmav`Lș>m t&Ci!_WQ[P5=M!ʗP8 f3TҦܲA o3j*h FM0RlG:V&K3E1ʢ~D~:B6@/mӠl@,! !s߫Q8AGDAѴɓU -5Y^$ʊ%ѻ'x[ Y~ > lR-) mJX=^2ݽ}:\7rCa9_+u+O^8E^g>ɚ!  FGbrjMJO"d/_sMR9AukA0)8m<])n B3Ϛt @h, 7Qme6<17j64xϏȦ$;n&rf"ޤ(4XN]R7'〖WbT}T;V'm0d{1;j7%19t %~^>eQ*KGx-d1%.>{k&F uYp4槕/+, /*?_$10y>aje0M( Txܗ5peUdGڗB*?L/Ȳ@fCGW{x5q!,˂3oFEn܃YWǫQ= &h qց|N ڧwY(3gm, E#@&RB:$xly"~_"lSQss.PC;2 7e?-#yIhow<05rtp%D0~o|s nRR mKG$;穙u=$jܨ%`{. Qo WKW=@V(& }oj&!%ڱi]~<:`H ;[[QYtrژ{hn9@ ^{*,!J e7j TtjHDM QxL{mXׁ3 jxMlbY` ^ampMY-֘h3l-@<.G]ٛN`Ba]/@apA<Y>?_&x``Aj/s-|#!pB\Y:O|^']8?DwQ.*bJr:iP76HSg饹XNY!XP;+Ur4fuqk)_K_6^Ƕ Y]Pս.OA ȢBtw|+&B7F A ƒY>eH;Flo(0~o|k I+%߀\,M;ZnOvc$62M~WXCq.[ 4N9Wrj|vM -lK[S=VEC1"l4?/RV80T< N@ LJDkұI'@#=G+Ϡ^ [!r @<Mx XΒmmzgi1tnA < S֎J*˅/Uæ=RbX)J$qU":6 z|`Th P. L=7tk(yn6ߌw7AFAwYjwuYû'@-W O9|mظ}~֟5[F|1E]#P1?{9r d4e0ԗ ր}`0|k陸(Bv-T?3J'қ̂T77-HKjn@9:`u/U]kXX܌V;7 " AG@@UC 4y\p-XSv¦o>CًoǬm>Ȣ,r ^5 sJo3Xl|T9$'o]LgMQF[)wav ?Y꣯kDjB1< ѻ9o'-)zRr_* ,},<mdWqu磴Ih Gpϗ7##Н'pw aty_Ԯ%Li<ȱ䮻~شLV2iJDx/ws|ULhkd]OXJld M2@T*] G/ 7KmW 2Y*I{yxh#%BFhouޚ0KqyT`ڸU:0_>Anヷ/r_瀿'$::F#fe}o.yw2uh5Ixkw[z ?occŃOU(Q]I _I N2ngQ?o#, U|>q/r< `.QQV~UMf bp@!@ާk@h>g7uO7@ ,K+$y&nˇB^LzHX-z=z#"r4rmҴE )"MA @Y]m p†~;7-ǘf:&ng6}$J-w: B6Y`łh(KXsL~owY>F.4֛{f-MlNM?o=0&u,*+0%ua@Qʮ [@`u2rd`fXl' jZJ#Q:_t&GGx(n#+Bnwq#8-3v" yuoK8 AٚK7{\.x; 3z| (@#xG7{={XGXWvgOD( 'gxW k%xc,g )bl\6&&Ah+@-Df@AuC}4:΁2m|MV6$ +X@R#8K,Ђ'`I /5 ҞFj7~x|3 (vVn*T|֨2Dj<,O #$cxORyxyL6@d/W%wo|{q٥41 8sPP^lp`ܫj>Vxʹ/^B87-LPN0:@DBg}PYT8zt QaQׇ/zh C`|חʁ\kAȠ&rvPt> D MɁJ8L9鲲0ߨxi:<ͺ.܂'`g 0KȪݍ >9je,/-p!@K)|rMĖLυ@uĝ6'`/QUa;@2P)kB@g|~VlWO0e ̛4:0W.dI8zF}zU2 >~; ^YesE{@/x :E_&-x>LeL6an#/&4bEg}Mнm,j͋g&~hgqcK6gzjqIh8tpOd.[{MkۤKƛD<,:/JBV(δ?{D5s~RHFoc}_*ӽD{C^d[^/! Q"2V`(gӾfAp5$/1ex-@p-yә;޳z/XKw] }_Wx LJ{·|0eFp+^HDa]gdU"Jsӄ@I_t~n7yMv6~8p "Js? o}Mʋ?A0(͡u$\\6 } [0x%N~yGr'l}8Dy|?Ag@Njuq`D!"ӏNЛxטc*V5/{2# ߷ژm1m=gV343c>aXsF1p@g6+??<y{XaZ.(Y<Az;9p+$D&>eZ"A g(өz"!X\, Lj2O'%DR&+@#.%/#o,Ֆwau:8[q|)iC@k#?8:!OR>gfH!jh$@lu q$ɠF=D{*Ov2gl$o~OʭM xia鱠Xv6 ' 5g]>Nh\{&Mls8c ܤPn3G mW/2U%VL>[2T*[q3]6cyZ?؄ۯH@$Ն8F t\H[4"1bm7n]}'sd-𛋁¼pg9h78Ga~2=dsxO>v}"ao['*<80󷠏ǘ+ڟz(%}nuVмטހ7/o6#?@@ſ: NJ{4RG$4jƆ-ݑ#C~DGΫJ~.OGvW 3Ot~Ϧw)S}{2ZCG'PZC',_J  g0}Ppc웯]s~Z?Ɓ7nLKho?Ewu.XO-.ߧl{ᝤr _Kis^i:z rff$}[ TGȏi8*BZN>Mr'0m`RXv й~ W_Bn5WVj#Jd1kaF7}ͯ0B7#s!e7u4!o`M:;/Ɲ>^zׁ?3!6K]0in,9"; F/F <2_Llo`|}_:#87d!z Ove~r0at{$ -+0[L:{gX@&Hyݛ&(8~KPN߰OULTEI YUBjEbuq:x<^W2]C3>O2rGN˳1޳I<",-Z gH G~Yۿ[{v9j Zԡp]ݨK62#\{d9뙭EߧQGH }I" cbJZ'?E IDATQA?5xʻ*^v[Ya)v74~P?P=\&|*UT.!MO'FrTl$cEtO{YSJdtPE,|)_A?G !<"5b ?@8|XOEuCD?bR>@>@͎VƝwu V)Ox ނQfxiFhpm#J'e8RDf)CUC$faY "hè4BR ED)0IibF*4QgmOVh!'qSxd:T}1BAFմ餳e"PiJ<H  R(]XȧӴPyݥ`*y)^xY&oB}A)X^S~K=nRJajAI1 [u00ה=$S'14LM Kaw@e9Rў})~4{nI2֭gZ>[Wia`<bgSA9h YОk,_t@6[~o$ǂ純C/"`s%$,*~2 KǢ]餻p<;õ\8y.L6?!eê`X\Bg,^@x)͟,Xdg,]&h0 /jce/yu;ۮ4 Ʊ \ 4EY@FFp8/i% J8inIAi@ PR^|8#bԚ&:]TZUP $Iv_([ 920WCR%2œhQ]xZePͱyb8jQwbilXgatॿ'=,B_!/x.ÀUZg;swЛ®7gJT>.B41#?Q0og<(TH#,kn7, 42H} ^,@?O'ku  ͡al))5رqs4&>Qvod ce%#|i>]h")BgE&wQ,ܒ[T"ە}0gg >i$n f؃?`lq+j:_=D0TX)_?>N B2z:D}mΟN} z|\D[&d\누<<͡ +[צK(/E%c;M_RD#@~~`㟠[՛ܿC<SIZTL~FN˵m: sqI;.^Ys!P-q٢BlT³C :EEbv},Sg (+g0eVs_ _v4ӿ (?%*])3K*||rbϊ±7D G^͕n|YӔcں "`Oiԅ/ kEڽ&xQ,@D7e~5W%847c?9څ?^ȥ f <ХDD1#ĸ;?pu{gnϖL V(H[rZ~HsO{uI4s#\]FVNzdEFsAybuZ *NJ)"' T䖜":(j= E t6,Q]6"À0M4wDI^J )$0mR( /M(^J"`y)(_p{Op/ÄHtMh_&9)#_Tkp{D,ġJn;vLu)sn~A04Y" ,A a(`ZHoSj(4튂τJz8siAՅwS4}o iPw' hoәiV=Zԙƞ[* 0e[hZn!p._VhKٗTPrQ *I([GZl⓿ן'S]8_B\X_e[kǿ*N06BPm/֏Ko~89gҧڦT-3*e1hSԴ,y FQ6)% Kκ"TNjK8gQ ԰𐷩?#@,(?o)g/*Uӿ:/|5ywzbkXySlcP}`:i$]4zki6]xH,nua%71Y9˗hh@H|.REkɶ~R4Q$;K ȉ@|)8kIGc$,'WtCBy+1|xЎa0A2S b4h,6!܁ oIa%ӵxO5,^-jY\$TJW@Q?t~+o l A|/dBт N̋:&h]9dZw*ZVfv Rp_D>U}bל_Ƣ6%~z ZeC8?C<'A[(/jL,OTx9'b+mr4WH)idqF hq`BBvX9 />ѝw)v_otF>DG9E;i[l@i0ʭ*R/k6=TU~~;x PY$%礀)n+Jw}'?J{&t&>"sP&"7\4ڢe+UL" 1^#eWHB02FAϯA6Nb  D{j4??NJW%2 zQk@ #QLS{QWVw6ntnΟvMJ)8r?[E3 jp0tIA `Zlb[p8:m54Kx 4*>^/;\BXv](7 F^ H"N޹7Qi7ӜͭPW %]ijP)lw @&Ş{ª9藺+,Ұk, ]zpB7ʩ0V0*/ 4dꑋO>]G#<,2uZп y̮uy<3Ѯ#pIZJ{]ĮXX\ͷ7@" /'\iJ .WUqh.9զ^ =ф-j :5ֿ?"rCr~SL$@o=,v̴m& X?3#6n0 $1[a( 1 /4!Zhc?7}6q?+. 2 {Fgm~,_!lY:},ԱyKDL21-ᤔ!nO߄a ~?H;o]6x e?襥`Т  ߺGPkup_~ ^h!@]YK%kRwIgD@'KyNH*Eֱ:җWIbWc,D42Xl 2Od2 QBxYhvGFt kxI}.?OSi\ QPzݿ,9,rM5D}O$@Z;&Q)΋]9IPw3R@i0L|qt9\+*NO/@g1h1Kx'1MknGy #%Db-50 }eD(Eea!REL2I)|,90q"(09 ptno픞{ Y@RFy!^`젶c T9v 3&i\T+v% OK9Em!Fީ!ML}CE+i~Zˬ} P*I)h4m!vK!u_tMձ@-+iz_J)CW8Ř/\>&WXqb9DbM ksE30Q5|PZ0;&@ؘ@|POY BRH,@V#1D"BvD GVy$NQF0+-N)͝R + $怢S5}ԟP~F`9T[UT[Uoy"VH&*M922'p~`%l}Ӂ1vv=3iX*N+RQ8Cfgy]9 1L R`8#Lf%>? \~G #bi> d}ȵ{ Zt@ž }`?@_1 $q ?;YO|qs?a|!h`4A&j 9jg 9Y>PI8qP-Y_/  AbTQ|v<ҬҬ <8#7;*ᘭ6,z>tOwxz4+}~R[*s2! ,,/캿a4;u:6z畐4H@(yp&En´+AϐiqD0EljYOTn=`1{Y@M{뮅D]Rh4?` (U  !y/.e*?Z}nfu'_b @0~QjFKՐ'e/ªXhv^ c?s$RAHPl`g7m:@WM*C*vBjcttq)L񓡃a;ukpфb W^kp{rB|" `/Y46>Idŀ i029JK@}\tK5@ SrҊ|%SfFG %V Z!ϷC$E0nuWgqQçCtnomrZwo/f ͏Υ ,b_J+{龢R1]դYY? ΀0 Ժu:u@za`!p}l/ d3 ?{c?pϻ^"$2B$Lv\gB㯟Ý3Y9k9P$ 1{ec ;mէ ÃRMkc8?%#~f`fIx_񢴎$8(ca}-b1,^bY@4G6A0&iη챾0=p scwB_$?+H[2G|zL4E3%ӿ֚c)(p, urZW̫ dUmW-VHԇ7Ζ 0(Jxtrc IDATR}M)M'k;h dI!&B@/)EG87l򈈭pT|m@ BwBvv9(DN `)ƭWHOUƉIH8J=jlM>0"2Bi a"PX;F0 1K %)wdE)wJ D!h`9[[0+w$j)MmFYqn"xfE,(5kKw*_lVR^&yt>=zj 7N< _ OYGx_ybn$/&mF{_"\ WN EJAi8^Zq.>d`V,W=tR7+V݉ (D5AP.BNc6(Akȉ@fU _*m+ɁH!Rw '8:KʯJ@%!Vb/@u{Q ~]Sr~)݂h +T&'8D:g.#Zܳ*ZKmHć;O|D~T)U^%d={bFw?ہB37z8\sb`xQ%]TmT-޴arf&hJ$PEy_= @;PtReL@ <`IAB֞ 0p PZ)AEqkj)0; f'u/Aj6C??E{Mܽ;#\V}NLz] ڴkrM^PLk1^H &;AP4jJXB>ɤ0< PN pF45!yd?|#I!%" 5MRA9褙fNhO\Kc ^ƌȈ-=_Esm͵圸jD!$dM^6#Gm6i~4p 4%!Y"0g/-'`\JYS~֩5-[h4ssnF@i_D&p;˸/Ý?`x0ђ4&ݥ6CI|mH] -x>+K9`2D4IDA@)JVCLo'R0$Ne)y(Clɒ@^ nTD@[&h4`~`"O>IP?cK"co1kل)GCDpں\~&uOKJ -8벪}i_%ERTjU6콱ß: 1ޛ͏PMZ| |eI=ppsD``A9}Rg }AB)gY j(/"`5;ErPi4@ iePC7B*Aa_ZHe]RgpZJ0Dz6nTZt2g_f.+x{Q_+VY@.QDR2kiB^tE#TTtV:)7vx_Bc2~ w_˹ {aP :~]4D0K!H7`A9%wi"2;o@. 1Ŧh4D&g|(˫K׮ s)RuRG#q *&:77`kI[Fs0L(XꏤiE[TN, !ATt5_Z舘 LJ\I_*ު֬aa}!s(dwf1b&y'~7[>p?Z^/&/*4 đR_B}L?Fkcӝ9K$5|΁L-RLb0 Ul -9"[T#8ag_W6OpE7`XfEOٜ^2>]_7@)pW,`\X\P5/G菎{*!+kEjzFB>_u) I @@SG=QvQgaJο9nvwۛOM<}B!Ukp^rf`kR@$ !| SH0ũTDy$'(f)Ubt!:k/vUogw g:yԐR:m\78pBo aK},TTiC>ȅ耝G;Zl{Ҏ.Rw%>Ҩj6sKsR$NMٗ5Ca٨[4;k y|Tu_NGϒ`8DF}i Q$،:_t,?Ř*C>,V74R%;Q4֏+RtV:M@<џgavPO]Gx!><'@]L}}ruHv *LP= {cLS '؍kkp, pTk2w "(, B`6!H8_ )(`zyt?d~<StVWcލ+g ]wn^OwplJuRMe}!O~~W& FDBiW c߁A^jc|pm{gx ;_ڨ7{sEs}Drĵ\(:@&}Db #Iti#L/#wP낦i+`{H8)  ^=P.mTʨD@\|{J, IBF(J OA%/ DP h o4Fc(hU+h2K K% sD.,E%m^1,(&M&5:}2Qp$"p~Ga ԛ5A:[.8O1 4e„J D~k)&Տ0kľw[0~HAfiCPA&yuɀuej> urj{ ZzQ`HS;+hJ r3SXN9INaN"fݭpp?`-$Z0 jCvK# ƦS#b>&vF^p"!u@-p~- W (b4 4; @G\ ÙgV8dW JaTU,/ѪaPp}@@8gsnM *e4JBY?WgWE OiAp'mjMJHDGs˩d@3ͅ&(p'?p:pJ_'b=-;pxbTj6*564E:raTj w:HaX\['.V7w#;5x$?~y~Rɳg/-n4_GZtD_%r@xx X6IQ@DiVT}AVYroIDC[L" 236s,? s/EB"iPju+MQ\֬X$B%jE臥$XNXOF<`lR}i!C,@f#G /D$X\~FxGwv>!mުi֨´{kIwt@ L?U$8UP碫k2B@ 5ye މeXS(@nVy/W8%J';X[f pSm`TL\Iڹ^io>Q9 ڜ j"9F?% p B]ڰ/U/M[@H4y#?Ch4M  N|PPQ(;D*#AjJ f[ GE$N&g#d?1Z.d_wt^GML=:5`w@z4{KM8ȌF]뢻brSP5~ ցr4Y3L@2@VAho4U\e-!$q :{wQL0N6*Ub qwxЏpE[ -WUa -n hĞIKDKP>̩BK P0[TDEd?v/*EXPM4SOP\VZՕq?{HM(C&XX_@{jd -U& PI$,Mϻ>_@i@@lOvz(p.:6xpxaIBRumf[ )eJ]5B vzovЙZ+Y.fA~@}SLRJ?(/m 'd J8$ \Ӊ#DB {kNp^''x1pZ-n IDATo"FQ>,XD, j@ݢ}p[HugƪyZRu DSw0G'ޝ=hxEІM216 V V f5B4K bh,a듿Gsyz$}D$йu O N y,D@_{JR*0AGD@InxRjh/  P!9kvDBteob8%fcvڬb"]UC/ ϴ&5FOsⴹJ+QƔ)GnhN&GA@K_2!++.QNK7\A!ĀYZ[Cק'a 7n ^<}5cA  Vy@h@D3~F|u-B`__BLK֪PJM<< ^KVXk@yzaZ>5?ZK-Iȫ 3"y81|:FowQpG}Df7bWmU8N!|/~s\AI{_twTɳg.,}Y^p̵$L:1MP5>ׄ pg$d'(1GL3U-},gaW/?_ 뽲iDMtV:XE,gQi9DBW, _wO,[W\hbr4ApOʗ(;>|_htx%FۏGN໲s^-˄թѩs|ߝ*5Bi 2T60p2PȎC67F}T;"ɫ}@&FjaQl'$i$28\P5@"ro6m zgРevH qӜ{CXX@g9Zs0#B9IJ  ElcE\?-V&pibw0bmI<7ș6X3~4PoPo֐i 8/%kpfG#6F{XyDDF,u<2@  PD֑9o9]]оNcд2*q2JV' #`vy>Slީcg3ڻqSȋN*9p !0,^ ݵ.hJ13ٗrNd&؃7PmT[?B`=(nv#vՂ]Ц_g|ł^a }SiMU#;؛"_6^PY7fDk0Y D@'uVa٠ HD&q\;FdmY=/;a9Ec͏jJ:R\#6`ڦ 3%# K~ۀ]F;_M9[B&xG/S _  p i@ifx욍R7?]CZ5;ѳ EQOVWɶftKY+:\[^VB[- ,3+bС89L +G} ò1xzO®W_R0x}"ɜϓw/qU %hd$/#(`'Y JexL)~R*YXv1;R3A_G bd/ի2N6'oQh3(! bIa/ Z +W߽j1v1czwb4xw p²L4;u4;M yKe_I#G@/E6& 71dg:t]W榳L7IuIfc&Et7ɞdd{֒+ DLl( cp >ցa܂=;qǽ?J;DSt c!* /.#>`vi zki[|.P+S(r07 M#h Im jST 5F@9` P9(LئaHHpmV݆k_SΕX2/@:,vyS&ײPفcvsuQ?8(=\s!W8aMBP5nƜΙ`CwcWkH=dyc,$+̯A=_Ob.âq2" Z.V z b3q,g"L \Qmm8 :`Ѐ,Zn6U!G飍_齑`bvs=y0@E pJ/$/nc_חҰc|nÄss;ocWPUSz aYL],eRw> M@;縫apQt(-=o<\.kR¨(SOƱM %UKmFhH 85(lCJ#BšP˝#D0& "!`$:V~ a)B܋w:B/ cNH Pˆ6Vx;ns%%gH&:.J{YWTU,n-b&3ŵl}Gihtw\.2C7Q~U"Lb}>7~Xv *n⻄RVO6"4їԥsv*?$/~XFZYi8<Ǐkh(q*@|j;M[ 1  :7Krk_CIVj;vVDZ?Gz>Ehqm~D7~gʢqB%VQ_֠!ZZ4m$H!8):›=lS95xDQ^[PӄB_ܻ@c/ @p!ȅ: `*CBqoKoϮdAq{^ ̭anm=k"{q@pw :]p`nnNҤZ K,D1&s``=驜:A-Dzm B8.Y`#8 |-o=r1 %,م >V^s!Dh .P/Vjs:n tlT]6>"\P_VDDS/4N=eI "|^ ~ʀ`@AOZ(tWQЃw~^{.@|&l@;pe5%Ēpqz3Ew,a3C8a"Ig]W!(M.7|āʀRr( ۾(a>D1h]־Z1j8|ZG*P|U `vq@)|'Z}rnpnȅ;vqF7q'RN1 7|wEQU,-5WVf[GuBp0J@$ D>LL qA~). **e`lR*j\O騗Xbwu;;;m.c1y^9#%RN+@]kI\ k\XhEsH !9$z)ލ Б. @8ǵlpqP>AXäa.Ce^ZlV<.3( Ը B}:gх0ؐӶj8㌁ӧ4*5D: h#K<3 $2ip@Bf΃R1;w v^pdFjqy/"{% ۘW۰hs+2[ H!T*BKP-rPӫ4 ܓ348#ux*kOe&ZͶ]FRc*5 Ii!`vni:QV]81H! H//6sPڎ+?~g_ t߱#an*1ٕ,.w3h4+nӁ:N\Nc<V}&2ؾ|ߧQD#+w﨔SEAzi[+%'CisPT jl­IO%ƒ)τgVڔƮCF}cT 'Xco\/ҫ|4jl(!<{\QִtP= ~ [iP5sp*"3х@A,.,u\_P/?`QZNœ߼wk0K98f S%л{rS}sm[z&mEw&?C%2h$yLlTGm\Ǹ́VCzNXZB<;62{_oC4J58u'䚖& #Av% tP+ԏ~O<}Glq+k`PFۯg(J,:뗱|T^"2DuT=hoj{$ \~om < S* B (ds97L AJ\׭zwFAMQAf[J(i3[}#}.5MTm*/Ot[cp k?!DZ 4ys:iӣ ^-@l& 5OhP:j$O_`˨$L%>k"D0`Τ]~갹t 98MA*@d{Hʥ\* g?(,f_Q !d *%1rjj=r g. Ha 箌Sq=!d?f |:NCa=~}}8d&lv݆@5_ݱL $e(Q+ g.o~qqsj0 wr?xg_k߾Z|i7YF~KW]~,UStq ?O0>=p}8 ^̒ݗ ?8AHg߀{#RN c~D=)V!\4#k~1G|&qױryj7VP-Tel"րI I򘯩~L_C]1Ե@?Qp IDATMc&mG¿1FYVw뮚a?^ @kXXU\|"V6kqq}^)B(]a> oXc`quYNj,ŝ3,bk穡̅6EqgWBEmaXU0q;VS1ΐ,'EE,B<O6xfh>^ 018cvW>[>.Ԣ{_=ޫt$t*8w\֗ <ؾYN! ,lDf*{۠fmh[]y 4#EXz X2!s;Eo"9U`UZ6m,m.ɮrg?ؐSrQ-,uȈR XCdVpص2\ˀWrj,懲(E!(u!(T%@~W4 0)>@`fu83`T x:2] x |#UFzxt]6 dEq+>HcN:p ^5jCO΃mBMBdu~ ɅUlh(&][H!<ߺB*BPjk8/ i7/D7|zT YN ^}fҰ%XzG`+Wt(cv!b7pkx:\kMc3c 61 \/Oŷ߃&Rwps+7j tUkWKџN0䀽ޛ["8^3) @M(LZޞ 㶻"sP/Q(Z2+|*880դRyk[7j d:ia3$98S׏H󑬾,ث8=_j(>|j0Ǚ 3L`Q\`vy?5޾Pp?Q?xm˯vo4w`oSTW { 3gd2,gf>SB+b1!D .l}1?ӌ`.\Nvr1}3E[Wc^X[q jx0Ks/"CPAe߅S7w[?Lx{&;l?WcMQ +2;QI!ZBq3.0vSe5 \p]ʍL" ԧѨZIM aʘr ]oOh6ls]W-!>g?#w1}nC,SJ<`苌D fوŔm=} }{ q]PBD4BMPӚKqBm\Opc:`.CE ̴Tjq  X !J5/`]B L7)S!E@TUF72|0J#h]!MZz| OQg/` pS1,\oAǰ!OéCfF$Z{ 淸o3/!@qrŐ# !Hͧp&`nutzNo_NϾDwLG,Rũ>( b42[[H..*Q#%`Lpǁ!{\@Q!ƞ1r}i`lhYǐ^^[KTQ=c'v(D&la,Wx,n>K=)ޓu\̯d8jW4 ^[w/ֆ^^,a)a :gXOHL!ZbB'L" ׮ \ۆk~iul[+&  9~0elCQ+JQUd/s0elOG\5W%p1uc 9`T~cOlwq8qt~=GU? .y~u`Di4!V%B0A.̊(5P䊤W҇?ҵ˨ p oOXO t"DW?=s*vCD`|LSB}=N7`XE$vNn|{ Oa0s6) ֗EqԴ@-10 5_0uZ"BV9aXeɝX{m9's/XX_h#0*2C)"zޥgme 0]m$iGB%4zH Ak5<-(c9x݀׺r%>d,r(nP/P~ j ?oWQg[rtw1_}p U3VGBŸC#90Є5N<4w/_9m#ZON)D2iqj(<{D.r{é2l8c(&^Sq~n2dWG^ !&G.3RvL=a @L<-;  +,l|9 F F)P7,.wY)sϟ!5-Pgײa|W\O @Q1iIrw( =}v%}pAѢ?Ax"*  @`o~r/evA.TZsqQT$ X$2B#P3įQy@Zj [6ԹFg ǸSi7 %̿gM_`5"RpXbT .0:zx߀7p.](Aa`qx5qоӨ&qTsy.u5uqnXb+[Beo:(z F#xFdkۈSkP;!;EI ,}_&=&D!Hp:\Ž?߃UY5>Hcq)3 8A ;Af,91$iۥ-.ˀw טb5h{~ Al&kb';a| jmg,{?F 4h :{%L;B+QLz:]B(J|^+}ze'[mͅ6#?:6W·#[:ᾹosƘ1Zet. ^e2w/4Qif`H 8 0Bx7T\1=i?Э)a[y(rrۼ94$īg>!ZZWA5jY;(e1В}g~AyHTF-U=Ff13mw9f~B$V`w?ψ h~LZG \C<`*\㢼_FPTV場[B(oÀEUKO.Ԧi@4! NBKh6]h@';- \:<$ (V#c:l'cBw&j8lXٵȬ_k(z K/AGRD`DfTy, jI)=,.?+8xtB|QSl/2?BKİzu޸9^y<\yQ)Yl%wxy%j3`3,ܯy)Y%6mAHn M0 ~_[U%BqyS+`gEEziW>zԬa صL{߶vK}OWjʍQ u(1 ͡QyVQ[Nz:cu\wXX%@iIo:H,<1DՠhX*5L %&=Silm?G\܂U*c/u9Nou!e(kgP`ȽUӠ( 낻ܗ߹cn|+ }~824_o"p78|vABu[9BD< iZ\"6!{19xv^yP5!]V QO*սjR_oU!|BaD&FRTi춾iu] MKZJ@' tՃ(< f 'b%fK.3ގhg-kװ0s98ʳmYSaJw]JTWsa[UE";KHdfQϡwjө(P1f7iSE=Wml^<. 4t/geL9a"{Q|okA1|BԅQ6QjZhhkj"5f4+ B6}c͞C.;_n {O[dECxb8K T Dw @m'Qܾn(Y )+Kte+W׶eMI6m߇k0y5ǵ,! 7a[DCC4!fx".}PÄQ,^( @;]v=^΁t[ooa2_rP>G l#r P"+e',^?4FCVO??GAɛ@rׅ(Jϥ0 i >35\ R#(8oXBF sgG]Z~G}?<]FA f ܴ|hI'W (PY=jJ9OX=0?SdthY=% H5WhB8޸Pt:_噃(<\*T͟'.N)P*C|PN# w¬agh?{Ylv~5sQ)K[K0uO`^o!* y{l88I_{b-ϿoW*])څ?u -oo*z.q@ Z߫, 9~,o=7RxVU&Nm` 윥 ̴$|>U Ppc8RP>sJbC2 ϊԦ8xzv1xi z^\ƒ? Ar,)P jiJ ocvrf]XQ G7R Lu5&M{,Bj$/*}!L"]tWp=-#\ӄB@|B.TS3FnsWެ#8&\ٵ﬽Nie8g?B|)DZ[7V*4H'7h%H[&ϛWm4wPk,hi.>|iev"GLb )j%)Jjޤ1\ӄ&(|h*8Q/keS:J6vO31.ZZi~Os*BA"wWn=;~ppBɹMBEs7o\M̛x\!q翕<` 1"N@3@mk2V.Oz#6D -Y0(AM / lz~szWi,sq˯ _~ Thx}zAP iiZk>1Ai 9%úhM5&gjܐzߖb˅F,][րSm JlTF.pxmyJ-8s!Ǚ!+u? 1E2Q.F_0{4Cm$H~ KxA ;,tg6w@3. iP Ppׅ;@|mGj{P ~ e}6E,EiN +]{~OD_ׅ闥R)Jڅ?Ҝ)hNBW +OhD{ o2P>%ދ\z̄FSO Д A"0H^Iod bOoOy83pj5$2)T?{6ڭqWkP2Lbta<ǣm8ǷG@! t>4:"}]d~O߯6WjcF!_b g4^ӧKKB8!)9IDAT^;`{V,i< { `sAXӕ_/¢M3'\^=#O}dٕx d:n/ H* eD@3,&b[x؃ !jXh i[D27oL'Oå.:\[f>^AwoMҔQ}H#OZƚQ~( Ek!U!Z~ ^wlj@v]dWc@pqAT?7XeqD*\hn( ~l|cչ PL[m@i :_r.w( Ai`.9P/֑] #1@=BY^e8_WapGEaޗVПH$YJc3{oމ ch~B HRXDx /xŲUһY'K˥)"B@]IC "v @!9ATn_!r9&bv!K?wp\ fł`ktd"L ꍝiB9\ZU#JB!5"- 3g-߮ mk~D<s~a\v*+9H5]H@ bc~)]TM6f'6 c@8#͝gmxfeMDCmqcRVD2P#300>[ک a3|}W@P+v2\TsU^pMQ|fb0*[H/zEA9(*!1c:(a9vw}ks[@A\,(;7͛k)Ъ)LauCZ +@E{\V?oSK~EUUmky>_|̬j.`[7WM&_ztS1%gpWr[EL5'Ɠ3( 8!ZBCz1dfNFPE1akYh5xKua .XǠعp}V5$ZW)*b 1w;oGzUu]jid|^5cp^ !z~*@'!AJi d`9XL{cc>_iksƭPR[1@owI1q NTKTsc?ƃup=>EnOW"5bn]-H)v̷ @,Cb6r5e5N!SZV> f\+ȿAoAh^!DX=6(<,$ Y'L HAb-e`:dqn['EQi$)XO)Ie #4M#-1}(F3)$aYXuhsbQЋ^qZgT:蛫VB?'{(LFFm12Ƹ뺂1&aR8?:௢}3B9Z5*~}~J eo Xt:t( "\4;?i9>!/mHS~ʁU!/s&;sd! Hs}Ʊ0K &Z>NީdfJ;$V:1QO1~:}yfCF W띦~p}~:Azo`!dWe1\g! \aFDDDDD ܩR0Wmo>@ 9[BHrF楾 dU"}u}[RшC bh' `1(~<؜VGK8 B3 5\BX=r"!8 BBK 2+A1#"""""Wǿ+.+ y=?V(7 !ª 0%@xȕ*U<9QV """2 T~.UԧKg~^{ E _L" R ?J$#""""K( h/^UhӸz'CS1@>~<7 @tvhNyDDDDDD"L@ r)uwD[PY' "1Ȋ^T,?sB`@'8ϿCWf T: a&HEG'C9GRXy *Ѹ}6{$ @s Nmc`XN8$z_@F(/*@%0 RH55qRY' 2oh&NF=g]*"`۟ڈIt؍m:#]м!*?He FCӀ ێ*`mH?!cU7%$ieRjc*« E~++h^DSr@5W6ZxvpQ+tjt5.D  =LN xEd|.B! c溍p/>Fȣ aH1yxj&@@mV[G|󪮮 W+ ;8lr~k+[s(]~ЍJo%o`fr09}f+P(M CbQh( b{zz{_4JUPIY1P`3?@"` ʢO_z͛Q7ws43 Z4UV^xfxqt $M@hZ6n֯ʒzA.}A3:c !MC#P6X\S=ktc(wLYuHV ᱕SG )дftAo/ L+{R =L FE (c0ox/~oy470KS!P4 ,b 9ڣ5Y-iSQ,fmCw/K.Ч@^,hV0`l?4&`ە%La-mv~~ޘj}4j?U)!TD Z;uo'WC)@ORݲLihQhG/H&(V֍/e_6RwEˢ5ʦP(ןRՑieWlfQ&D29xn(J6X&h-@r_آf"^`L. ׿¦휪 &(~W> HB`I/2{:rdO(ȭ~" 0],x;w ޓ(!2n?+[J6@~ii}qmsaO kw{Ԫ5B6Ի F6mGXl.G`m+l+DDh? FiyI,1izt<ϾcħM$@#AܸpB=y->9dZaQE瞿t^ؾ9U.Tul)ij`.<~[ h+wy9d8iH1Ld_Zq rUk]>RSNA#id_Z_Zc†Ñk'Q}t@>۱nCa1qC]N+,Fډį~/~ xg)ӧe7em1[GOmc;[s[iTؼ% g TpEEp,5SQغ aeZD IQ7Zg'AVZ?rXd n3nb>A2d })=nȩMm<0<(ȕHƃ_R^I&n3AҠ݌}t])n12ʫs-O Sl: C$OPx6 vw*Db ݿH12nf׶+>(ž:J2 0ngMӲqiH1`GVPa3~1٘ wBH,bV39ˆ\x RC'e(}mPPn@:$"~t5/xIb0GR w/UqgYT(7|X\NHg@#P'N143FX!pl[%lVv Ӿf3ZOfwp.K#(i++\ Wh15X&Tdlh>>r _xÛ`fD8I6ϒ>},Yƿ& ?/7P4$ `iXD[#`#`GNMPCvy1[`2ŚdDj[~ύ>PPê\jwUсjĨSGh|'YeQs,F `* vd(ƿ ̮r:BIǝ# j9r9lFJiɸ$ OY8.O J!e]}_"$ ihB]Bxft'Ww|Pe! X;N6]5A@'`\=tv@g~dh5rDcA[!f͜Y"̂\={*q8#;(JeXaq '@-bAGp@kd9[n%`o"$NZę!Ϭw!|zkȗa 1r0Gf5u $B}rLK߀~8p B$@8:>~4@}bY8d 1Nϟ)k$u z!0-gtW6`WLg=p{NCh4蝭0GFa `` P@qg;npZ":agYPظ 'V_3HHis# ''0\)bYk Ɵ۾c;;9w )JYY!~䁀6 m-rha`18c`Z< PD~6ֽܚ ȿ0]Ez{ϼuo!*dX¶nA%v`9Flv9N8I,[[NM#ʚp8 ]fxX&Lwjos-?YJA#X!yqϝ!Vv8ۢk><杶ޒ@xΌ,6-c[+a`t#&׽Ur,^ۄMCt߹;-m> W!asYdGd6%od]pM[Ph4Z:%R Ɵo/Nl"e8n1҅瞐\(-Cԙ>M瞄sO?̓ybrހY`\BĝX:ȀHG$[ω `ZfJ'QuC)2m&˴4 Y1^c &Yu!~Ah:DD65т y10RidZ/#kr?V+PZod]Τjn\N - p"/@i1 ci9V2Bw2Z Q.K8VP]GN,uy-v-e)NfgJ( V?%ä]Y!tOPvGCw?Q4Z ^^_a:[],Ur|":՝՚[ú_$UeûG|I8 qEl ڋH˺S9ߢC5W HJ*lt4S~%DŞ!}ϝh:q_/kVpR71}V.9uP;9T́`ꭕ%Z L:m|?@8;Ű (Ky%uQXBxf|h}@>^@%g_D'"^8@^zJP ˮ/+C{l Hvc(`'5?]w#d ߺM/ iGbʵ_Fh x3S)Z?s.xI>1-t#0yowa'8T{d` `+" $ҋT%GN{9;@6mGqG/ < ߼ὦyBA_( < ^u= [f֌jRv'{~i" j@PI?~TɧH;xʔ IDAT<I<%.╽g0,sreB_SZLZ;`Bg}}+㯨DUZdLL ~RvA֗%"P🼃|:̾(.)<2<`T]wEa#dziDd͈RP[?n1ڿxمG~E%x_еH)cN2﫨[DH4"N6u~T(A?jSOmxf-_dKVu X}ˡwMS@d \u= w:R^8l?Wf0vj.Ck(ن5zy$ήR%1-k9[D,V$\#'/EeLxtLHL)ѩGJ449ZuO2HxdgMg[ @eMO5^2sƒ,P[1DƟ%a oUK˗toE P119-Pi!ҡ!h ښ#v>ŭ0x #j֜:,}Z6;&FE hÓ@ W$PeFDk60&"yh18>w}0Gã(l؊[߲` H`2uڛw"V:ZwBhExT`8E5Ehϩ:#cf+3НtSB؞jh'uP·xdx5qPj[!}2V[/('8 m>W+Lw"fk7NdwtD@d^܂[Qo-e7 PB~=[y37ܙRх1G[¶^?%jO s#UxQg24cTe}-G˅-N(Cȭـܫa VaNLg߰ [}֚b=Li("#<"D̞!-HtpLa7!6 x%`&./ CaN_L2/mU&+O%93,#7GdYN^D{oUj3C<V$䩇#GߪyBy'#4?fQm2M 6N@W_I۪*2oht/nd t|7~h7; ouU#uI$NZsN@hZߊI#d!:zR$5N B@ 3tqJ00s{Yd: kY3U;pot^uic@ɫe%a"`TWG_q1C>RIJB4DzN:0GTIFH3VX UU˰TjU] qAh₺0Ri4Rw=nBf.Czh $N^Jvkqўcg7vZտif@UI ʪHH)D} 3/x=u\!fm3bH4j;ag4N20\;Gekȕol }s0YR( b0zchو7oxa02Y ]{;9e4Kvz-|o3@;L t PoMN8#[|"hi!4MxߍӰ#eH@ndZ̓kPV {m|H-}'B]~$3ҧrh},&Xf 8GH8 d8bi3˂;yM4ƺ]=>*M Dނk5Byj = = Z,Jh>o9t#7OP +8 @l3gCJ[@m>; jn2b ?$u >yNCߐ]&"g"DFx^[0|?`zo]&8XDމeYE,e8}&gp4eqT<1ڄ$B6Mωp Ν?Rؿ1Lw(lFqk7 [vأFz sGsW-3MyyZBvXQ8H]&EUtBErL>>PH&Ew-}_*; \_qA]M+7Gn5]?;s?`q֌}_9|] 0c:&.gNV-NY'RmZgME7?-Z#a ^xf~WS1Y_'4}Z>Ic;jYh{1pMc)"Vlb*`SI83ԦejZ3ds-Iw#uihHQ__Bm~;CɱNe$%t<UE0 7|LXJdֿ?m+!(Bkס?R_1+sOeߪ}F' ⍄2grKe !N,;&?9p0'a8Н|rDion5]OAA77VZ$]P\a!ֱ0kNr6bF;V)趱ʧYG~VZq}\1"{ZA7~Lίa33Y}Ƚ\ۋ::Z ums%}&L|yim`=v0MʆlxsH&ñ,r]]zpmy~Mʿ =ٗZL}r/G-Z/9p\$ס:8^f7 P /?SWH9T%\7h@Ė OCdt%QS<{d{oU\ֆE{m3,+ZBJ'U.sbYR)o4ʹեE"]s #V&##_挿/ ̫ȾףoB.]?|*p:A9(kۖKYn>%[ұ?r`fS~P"* ^\pߪ0=h>b˯3ZΝEhJcYF&KzjD̊ (pO8zx f5u1(㯨FuV OW:"j5gAA;exw>zszVg_CŷwB!OoYC81A*kq (Z$mи@X/+~2P[ f'-[wa3:)VJEg/lE})_t?o-IoP-Wm8,^^q {=/[٧_AV^gO7Fp̲=3A?Mg -䅝50<-W*cxo5B)J@@΍_P<0o_Qُ+~ c2 8~w@n)fI\] ! w2:,:[wB{_ŝP }[mS\S,bP+ rDX 'u8gw}pxt uh>.B#7ޏo}Tz9_{ Z, }j;BSZyed_싯#qbU Rُ\;BvRoUh_ Ay> [rSvh\9{4߳5(nS +OE̝!@WvP 6t^yqUքܦ 鞬W}*܀D!6KS?!znLPO_om*=r+!\q-[{<-WIL#7=/_ BJH5}v#uc̝[ D4'  ǀBh: ύ1FW-B7^Ba<9ĔojfN6[ݡ>D)q'Ct1u(lԦڌԯj(hVܤc3&<8oCnAZp$/CY.&~tr/m E9KGٓ)_VN"t7nH$pSThw.zrz0r˃ݗ)_aG/F}vwhf,:ƸUA;#+2$K#w<-զRw? cp2HY?hIsk8@|5,._{OTc0tP-ҡOiE)ۛw"p 0 PY0` 0y qoryϤ&IVe|i`Jj9 oКHM+@d^7'X~(#kH?"ҏ2I'@!ӣw=Yo!`ݓH.7D-QHD[G ~܁"rʬhGeGFn}<cdy f:-!/bgS"0'ݛZ:iHjUo"u[ }f%hhpR #LE Ed_~#ZnN#̫nG^BȾhrxْL?ql@K#߫$r'1 %ɱ IDAT/GomBˇW`_Ht2ϯ^FxfmatHwGUuFl>t֋FӤO](nZB<ht$N^ h< #18s0` hZ2%% }ld"h)H g^x%k 5DoVQg_~W-Y3+dtcvpULbʄy{ UQ%[|gtRKoV#3wȧZMCӻC%g͏ڍ%b_یbPd|6- v7Z>S0/A1` :ڠyMӻ%`/1WO3Gn&VVd:2ݎv\Z4fo+$=( ?-m: $O?B|}j;:[}uSW;Z",v#zk7 f vryyCȿR3\^ l_ 1ϯC4|aFH5H5rjӡ7'<5(g{vXo7Q?|mMe0z(n2OEɵF!H !2oD>-CC憙-}at {5'GOFL5 0z+0Fk{Lɵ8_. ( PWNGaVMmC>Ȣ .近 v^?n.9 fRo&\fldl ~7]7i.`FH_? LOܿґAB7~ۻ$*7c']hLN[@᥵#/?d]9ӝc؈(@?&MC# (nšO5/~ v}z|0SZ4tQ~#b?}zb^]oC/!7}hyl W*V#d_B  1q4u .ٗ7 b ШuGm/ jy/}dLۧf1T {n}HR53ju62{0B-\ݳ[?e_`&L^^ N%=`OgsJꞧQ|lY@T][/g=_Vh~9 #y˽+chtycv@oXW߂Zu3f~7ƿAE w<>1EDWxaeyJuxAOd2C@#EP1AwE qRH.4ub0@Ec4[qU1zM=hCc׀ȸ|PXM5 4HQ/. $bEO*lf) B:[=`uҷoOLħh47N4RIb+,jTrh[bsc$;93ȍxf8 Efj/P@`])KF ^6<M @Kw/Ž~6x{@<h+ (8PԯIB8@<ž!^FZe"һ5MN xׄ0UKV `v*|i09>b@@*UMT]i;l)c+B'y3t}?WғIܒ~e\Yz#2ް. 7` 9eКKE#\ghqЦƱ `pm˾t1=)ඖ㵑AKiϲzn& cr-Gp* ^]N;=FE=Ĕ/vdMYF*^ U=7G )< bw Sokz+Pآ2#׷T H T͉yf8/. J)_]5Q`|_-c, U&6hO ca[@Ѣ# @>UԿ;^R?Һ $|iLmmX*hUvybar m=Q:kP?3W_Z`%ں``o=ز2訅GzQS2f޻o+jSlR/#Jv4a)Y~֫G@o Mt@7 v]>50E+CL3CK?:0pL.0_chHm9ӭLx4a i Ȯو[;w͂9 @m]h1t)x#\ch*0_D\\@{c@kNkVL PitZj005CբE@T@31I8TE7J[ȵܫK J'3_0~ "qk?*F IJ.rϚm}͢)ĶG\_2~߻R>#P-EŮdWm(=oc0c}U3XLߣ#RN4@PK 4z4'l)QEMBxג' >iMS5So`n rWTbw3$j8ҵܺ{c+tS*''U&w] Oye^A3"p h=Q#O!EMޣqwJ5OC?B̘ nxgD͖HvA\N6RS".Rϫ36/ b"% O_a G͂O MgZF&=TdF O7)u!n((1gbo?p$MNyõ?W*lZ@AF @3ScK҆PGQT!b_WJ_$ A4MĴ@4}7zIVݮ B(ȭTDK&ZFEA%I(YPd_< "ִb;74HhQ{afkB"օ <]v1zKR3CH?"1hz5c4FazOZ~mj~K>ç!_Y\d 9gB`*NTlDdAcϋ-)kBn5Ծ.4r$AAGI?ʢ@uI!UEI'i%@5㑶b%!Du 5%1sy6T~y~[RXK Ю mFDFK95zeݮID@}]m~8?]9FDZu0Jyf)\MP@oN=|x ,fQh (AKD:[a Ƚ s?4 6a|QDYy]ɖc0F7~s 'oO0p_eMzHMR_ LLIz"*#b!юs&C Тa,8#'yO\ ʾuB.>pXA= Kj3h %c }4eDwP XTlFi;lĻO֘{Dn,G =Ȗ3r:;D[Bpr#__ʨXD Q;;#](n>͇!Eso#?:-Ep2Zh72N:A \0"K IDAT@BÛw#۶:D W/5 S< 9Ӝɘ)|4챽mY!}//̪*;G^;&F"F:Q[lDq~rLɷ?F@,DkPՍO2x&dVoSnDzAH2Ke_u2'3?1;r^ " h; ͝ >*6wt:uy7;+/ ;^tƄ[ҟCnDv ؀)RʥԤR v}Kv{!Y';*2t1"[NG= ity.m?^=\Sc,apCM^e(">yg:27*@UiVt$yW˟3 gy@Eǵ߃ y+(3-Db{(+w,g7Kiލ=0.W@r˥0f`}? R o1 w}Yh;kRdH1z5$J˖!buYg0.I{Ȭ5|4[L՝)͞_s=Z@:rb4i?$=$*(@˞|jil 傖?&{}u{[LOϴUl Dʇm,"g̥ |H*SPse[N П:Go4|FPK-3lbqk{mb-6SM5ڡh; dV$2y~p8:; Zo1$澇 U䁰&txSڐV6x1rCYgB{]jȮ!ttx`0~z*v#r{ohWmGjGu#7 p"lZO3`l,2|jG35'y%dl@pR \z<D?4={~;|NFT0;j+`aRd;^7ja[3+ 47VϮހGz*T -1KаV_}wA 1h=+ĕh>K@@Nr~ y܆0(D ioGhNX`i@$C߅w#7o~i<,CxvwUzNYlG`%!bӚz=p9ӰK7bB9&MG>DٿѓXI?F(|l"4d Urqt/9dkz37Z&?@nK)4424')?;:WEf{s HCtBݺG9Ҡ q2+z٠EB)zl7NsA˷FhFr3΢1ث;qL'YSc1#l"yX5H/Lj2`*ȓccp^^ݦlCwު{}_|0~}60tquON#Og 9wƴG/G˱A <n>D W܏ Bnpk6<39~c: 7 #y"`_6I!beG]w6!?2[Gg#8h0ArgocW5Y7tt|&~ 'XkgU?ZHHq8쉀Y]h:Re*M_'Zuyo9 1Njf3j[?َI~ N;>& jv/>|1U$Oa7"ڒMo1$Cn]k`u( X\eDY>e}ȟ^DU݅hNtw>cAjry7bƳנ쨽"njF/D{K_IfE/zN}#ULJ{Ow1%oĶ|g_rr}us;(=pY 1@SN6ǵ\4}v1?Bxn5L{geh;kMxFy뎹Z^縉i *Nͦh8`Ve;~C9LdJ.CӚM5zV<5IꟌ|QVg?Ah܊,?<51r|z&nH:SoI#<#=lm|{L(h 5*$A0̛{u>H:+U>Z(/wGl]gDW܏W{6&Ï)'Y" B' t4y⏪(9ǀXrpZC*s!EJXO;rOĭŸ;F3W,2VKun{7dܯ?Z߽]>ݿ!. BegtJ;\dFKעa4 !=RI*%ތ};4~}W+A9ğ|C?ygUrǫ4^,hPe;~}9Ld(OWP}113/)%ڈΫNDnۢ^wV{m5m]7[8O!j_OYF@s g#lgKD.ѿCw5΍=D[h$x޸,ie-vV+ܨ7¥`\D z˾oӔ`q lL˥ 7ƱꀋT@|^rQ} <2ҏ\կ_@ſa͝G_ sz~}hw OC\uUb_&2@r9 7;TZd0S.GW?_ڏ?62~u^ ?TI7r>uDm8El`L񫟗*S. [>^QNt^w'_>k@sp&(e1t߰П!pYrՏzYΫC|aIC>+0`0)'Hb#|Ly"fχ1Iu֟q F~b{퀦}wA×v@NPIgcϽ3o!J@_.B7vOI\0zξ > ]>UA5|aL\+J"`0@Nswgľ=[LRHkϾo~5*ϑtz+S{0oKvZsyfy*=ak@͏&?Cnޗqvan?'ߏ[ ei0"[@ӳv&"V3|f?O"J>ԏ̇+^c+K rL`@ͳ 05O=ƃ3NT EU|xߏ)i>(87dH_B9,Mc-@ӾAY"Ռ8:}1xS6ށˑ`"[LEӳv&Bx~uH~c邡OV"OuӰc/&Y|ЯgRh9b/<Y4ksCW?x4 -G^;s"GzZE v!ތ`[#mMښlkBQBXZ$h1 G NƐY;ߊ3dz3v=px0 )y?@@$_};뻡̯#4,Jhdtu.Fx 7D/H|OLr4RV(jv9>u`H-G-]}/NF|^l F~# | ͇?7>~jGmg!`} lbϢ%2>0n swX2D_ Kd0gE]յQ`qPx$vҋ֨/; b$Y q|nhBAD?ɋӿh~k xm!νu"MNXH?o0Ғ?Ɗ}{Xk5LrNuJ,,H6'^+0 #:C2ğE _<;i4 } k z#GvC|hz`琱$Jt}>s$0(LcJyUb`Kccql7cT8YhhndGY׏܊^dV!ٕȬAvr?\ɈϪ@9adՠ(_f=]rNfwS08F@dOf@l!6Ǵ/eqbb\׭V3~sDvf&h'lqR H< 2B>ύ'@2Yh @ iH[+uJkn{Kr(6rl:j@:%cUΏ,Gz:D樞Jp/r{VxOz`-Yl9"[Ϩ M Ҋ~]R"H̝ +pPT.?"?..W[OpH.ǫx5Fi "[@x7j7ZhiyT$Ie@R^kYٞAo/iU]m?o6vm"<>_L:[@Y-r =u+ h+^Ӎz:uܟ=^.(36'rCHC4챝 qϠ=ǻ GB7S%9t;l^cIo䋿t]B!5~n V v6`ys@S;3 ńE`%! [-"@=}RuynJcwKӁChP(\Ibsvܢ~.3dY"@Iz1lMY{uWw ;dC;BI{B) ˸=ޥzZIqEd+*Qb4t_B<@QAzvAuD/6 xP94 'mN݆q¬zjc 4 זYՋ䛋\U57`7!׷hkBr5 E}c_ε9bS}0@EZEsֳb6!N ~}K-zPG~A4/[ m;h 7r""7HF0#։}yGd֟rR[дh:APHB}#EH|KvPLo߯uMW9úCxnP(j2?xrQϛ4b\iسo#ߵܴ_Lj.O_P؄YyNZ*)lFt_@ ncSeJLb)c]Y]h{G+ ß)J<7jm]bj F*ڿ(MB9JI_E%(5ak %8g0pq9ّbr}]>I0N[iߝ]ӧP(|x4=A+j iF"1颣S(6Qh q>%,}w < >Bao}8xj'Ud\C>P(~{_H0au -&äswMBֿ BԉM#n Ţލ> r\ӧP(pqLX>X(YS%KWGgu+5H{>b{dA:=kф;: VcK*IYH;/@8>AރV4t?d2r;) A$)uP_qK.Hi>|4yWu* Pxrl׷ovuEt\yBݓ\թP(e0~W>&9y̲ye5팃]թP(ժ-to hy`f)W'eVa\ӧP((pmGk-ߛ{-LrmD@nhCw<-%;9VG\ѧP(@mX_Pe刲),'h9fgv:.,[B]V܏Ik;VVh P38TRe!Glߝ:GP~5d'8`!JW$SnZF6qq6:GP0pc>/VVo>pHmĮ {*\Y6t\vףu~خHuqxzTYعH2%/CJfL7v BQz¶I ")ScS cz nz$ԐF۩_s\B@="‘ѨqVoz*ʹ hqT]T ̺l?~7$e B= S(&B =Z-ip:X(˭yXy sgr^vh@!t{'#K 4_ e99esN4Yx>@$ӡe)S1[e7aGx.ТWt*u9f. GzQ7V !#$Ek 6WG}JgL +0 )(umnNT+*>f rL8,һ"]{I^h;@G+uUeƳ^~V,Jo"R`0|\4boJϭ:1 /lˏ*˺~O (Dh7X^v}Xr:.=&k:Q'V)`Joɳ;x_ @\8"W5<2;7҂|&po > 9xBlyK h9bO4 :!¹ Ȕ"(ܑW( "A\Kxvsx(h9CPHBdDR=`?F7 /Xf\7#Ao9 ١)D+.٭ױvX>_6?66dakS@ XQAY>lˑzxZ~V: w]D>Aut !">S _A_ڼ='+zwϤ%7FEl{!єrJ皪 g<{scyZY]2iS{PGMC,Ngn#> kR?xqtZȄ|[nQ R.#Ba{=vL;R(m2ZWYJF N#ږR,q&'eؽ5yBvmMAl::Y]o!L(5LI`N Uh* 8+&0(Qu;H:c];-@?B9˯:Kj럘嬛MyMG)[NvD;PLf1iek9̙4h b{lf7[LNʓ,'eT TuC`AJ +~i(JfH "4NO9cwTBKTN%߁}V@c0GʘY>"&|n6R'&0ڇ)b{mȖաPg.4=q`aQBϹ2Bn/v%FgfK& d[D \{%@Py4b'ĢdNgwE$֟z3K:"_kV_CRס(1Odo%0/+m'Ͻ~$@C8 5ib-/Uiܫrq;Fd 8"?h>bOGd+ [)_@PNV!ٝ p BQ}FzXaX^d߬;F1NjvDBQ{0ʵzæ 굀QE>σ20d8| '#G x-֟z H"%]vpR3:~vt Ema`4ֺ_ˢHz||4N?I!ob/Yt Ignr%X ٜt͇*]Bo ʬlm'AٓF@"{Z%z oFr"K<c/ދ~ǿkOFzJ*WzòWi^-kPADCM2ħ=5 @?)uL^PU /&81k!r* 46@EV(C-a Y\>{@V S޹'p*Dwo{RZ#"[@/N.WpK7P_[gzwnVyBpV&mf N,GҘAg#VrjH{}U(`%1/$D/h?}sj AD3I#"$.dDzk[ 4 t^u9Wv D A!l#ZGp2ƩE(W*|Oז2⍟2B40l";kO/"?@׍߃ JE˜t԰uye)z q6c|Y2N#FA#W>C1wlauݛ-89CLٔ&OfBe!P ]`[e; ҪrIx?̇69sFui}{4f&fpxɐw'͈LN~ʇxu=cf#y UPG5?_FDxU,]R,`8 Ƨ| fPfTILEWnz{ }-r#iqP(<Ţ'Uźʴ rcٙ@ e+1YWt}q+EN=S~r: ױX6Ys[%o|BdY.FimD`UUeJdYd#ƴ.Fxvcz<$:CpEdVP>!j~jvTϤsC/O ;G<U/ֺS$rZ'nSky8;:KQ8[(F_8HD22м6K/4aω_xLdb<e".}}rVqro8? #]eZ82eZ[ W416? Xu|hf̂W DAf (KobMaI|l!$/GͧAEH }_G1`f@G?o7mrMgi]? Vx˔vxq]^ynt/Vv%26HcI9BB2Kƃ@Fs;RX腐 Ox?P28 0ė6 h'.@U\"We"|3>&`%Vtu8O*pIkhKL"U O*&^^PcA/4nB-y'V)@j ?8^[l oޅN ]מϤBCe?$oKZ %g'f 0SՊ#`0fxM.îDzZ)"1f=w6{ PPl{J"Q=\ē? 0N8Rwxl;|s`v @PZ2*)?"\pR>ʯCw=&S1r4=Zb`p'(\9h}/"G,>1N+xv˅@Bh#`Õ#Ffy Ұz*}wT; cSLi,AU1 )3*5-Ъʽ(ygu D !1v~;QMRa&2◝&4T*v)Ls [Oxȏ{0̛4ۥnog43R>G_C>B cƟ/o@~,)M TyT 2.!2QC2,J #>aɪ`.t\Gk*1o_{_ ]mgS{F' TlŃhe?ccR)|'墴lCgʯu,giZ @c )V`ʈws$3_EmYhn22xNk PNr] &Uk/N[иǶ( hn@ [#bUO.WV?(; ֽp Zt={9shl0ɑ-HOCD=ˈɴ8@8㚋)/>m;LeXsXwMH/Y'_iߝmi!|]:RNe۪^BSUxafdc: GݑVXm8 ^'gHCT@9\01}~KsUPa ,Α'1kqj @()܅SP_0tXiGt.@`( ʔSŌ'$)>p,tRNA:aG@R 1p V|Gy 86@ợS:/ʀ[ŃYV91OQ#ճT_H",6b"oO3w$83Z2w`!W`"j[3z ZVPՖ~ez0Z>?h 8gcF&pjEw68F_EXk{R/)m3W <˳GQcȭ`|>dULRzvk=#o"NjA~pM̒arUQsCe"csڣÊ.? $)>2g*f>{%>y:ZFmZdv !2)ŗ'<Ot5eDp `F5) 7vVAb.gc"׷ѶVlOqQbQ Tr'0&7 @ȁ>U'wH`<@ƚQ* cXssH-\eK co܄CwK/0-?,.1 ^e%]$P{&h[kSmuo6 Q2ΞZtd ~R l4OMhmD7wG7wry$sCz1P< GB;kw,cz@99=D #{\5щn;MCwCdd``"bDJb."9lL+l_H<(|ܫ\9 ds`ga&8ұa@9 DvCW?7 K!9R/Gjr_ܠ_:TZ%#ʰ%s (`LtfH]Q͎#5ϥ Ek0pQZb b{n=Ct-?!j:"[Mstv>\ԇpFjDڡ+R-Y%sc& L#PՄH!<@1خ۠aиl5)b@hFإDAzyR W"H!R=u RoQ(ٚnKZ231H\i]rJu],x G_856SczށдI4Ǡ5ق"s"2g*Zł$7>D|wZtlTN7^FH4p'n1{@ZZ0eCV ;de=H/1B!!+@EE"@\CZC|M_+|w Õ[`P&1t}:"go@iTjt'V&*vJ/ IScë" &Tߧ;Qn9Uz-iAQ(wdUX~a.<]zZ@sV82vLy R7d}DW|v+WN$dߏB /͓ o+ܴ>[}-': *@?87{e}i z!;I5 DYr pضC4}CݬY Z)B؇BpeRyg Aj20ӎ°eL'-쁕-gRP#`#_ J9 ʍfɢlia4M}@>I V`JÓC!#Q'@ e(6p,\mjwK#]L";}&4 ѓ34)8HtXbYxů\C]#Aܠ'@[/HjHyU`KDʧఅR|IHIDAT4()) WMW) l#S@rUK֐4M~|PnD|RGzm C%i c/QJM\tx5~|>iTWI AҬkwJEBXSP(l%OxvZ8Ȝ%$|v:FR7"̧ sL881G4Ӧ)ӁXa"/R@3\ʾ0708O%8y hGr󙹙u}URKl da8WYYr 98R*{fQگvqY =,o;)~‹;`y*0@( eFZpO.۟cc&?Z[I[U/ak&"m2Cށ!FLyrGj܄C"΀, + a|Ukmǘ;R dd@olc7 5O02XȐ=.Tb/oJ  |[93㋼ұbsd7>Z[I364[t.j>BiYaJfZP2 ri_|No,xy/6jlˑ鬿 )Ц]EŨ@, ńF`@3ψf(c$aشyOk$tH4 K؇sD{2-C4( >%.{s~+ FF4h0>)&W GOdV(j*O54hnSah!g\:Cʼn|}m]%4XXI t$6oxZ3ԠEBĢ4FEBBcbr(ne.o׬iЂ~4Fߕ\vBS}㝩'0s̴5ZE]EC'C 5~zЍtYYŸ D֌1|j"S.8`{F#hB'7YƄV󦧤aAv`ؑYnGu> `|F~J.&BXu(AͨncPG F9[vȌiiX,hY1xRU#u! g_]Xaׁn̋Cdj]s0Xt I y Vbf䭉+/g^.4Id$g? [ṚxxJL}qi9"e߬W!گ8TǑ^ϼ[݆f$[d^bIVz=qFЗ?=] מMnMDn>a$c, X*'fOl}E@ٔ!۠A(՛]gƶ3-X)JF)?Ǧ[\6ǦUٸt1h>KIZZk2h%I?ak ƵǞ+3"ReXޔksHtǟ'Rv0D WE0@i[\Xϕicw&~R_) A)OXQyKuQ]h'J DN-MAq6h5l;Ćm6oD"ԚPabDI/= TfoJq/2Aw[6 5V'CB]d$22&Z"Ǫ3;gu"caWZ\}=&ؒ`'Ty>;|Gxr(N!hd$| z<Z~J!BJ0z`tC`KP!Z8ѵE<6*cfO@e~4P1"@usy*s*]܀M02f@+Դ |@i<OJ9 @iK5w$%`)kş Iàϭ8A}}x`&Uki gj7›w޹}hr[HJ:'PRnM:?#r 9\pP/Õq1t`P'<?94?<1,!ف/=Y޷"7ځzZ?RHG3u0hHp@Ӵ&-JЇhLfGfcc 9yv9E;@Cd-'b"9?4<)U@8)J<$I|"՛OVG/V 4Cﺧ-+2`9#W!6'@Yf@"-Yt{ ]) B^3@xVъA/ѐiQ ޚ h[@w*6o>,@P( d6Bﶧ}d' 4#k@ ! Gp!M@Y}d{B'9EZlCBP(2 }c}h2`ugOMC7 !+` @zKf[|1}ʀb-͈L# Ba}ӞA_ys ЖZ<=mM+ECA{i ` F=}AqBp(@P(7MC./k_9`-,~hvW| -}E")3P(&U_4V)~Ψ`As0[p z| yg0"}P( /}?^0,(c '@7δ{]FZu7Cw?BP(to74>@@GSsAfנy֘? aH^kK BPC7C[<W u' ri8}nj02"/>d yEg@MS( / E?}%9`7 WoC?<8ig~m06:LQ Roq")P(I&G` Fދ`0!gɡ-\q6%{f BpA~3gkh|zOϘ#b OP( 'CAi-V@z˟w۸oi*<]Z!nK0P( UЍ{J qeyRpW4CgncAwZדP( 4ɀ#0__ )ďxN@ z`\Šy#Н,B%V bbBXgBD x+x>*SzBP4q|7s+|#{ #P( ?cs]σ/`|!BwF? Bpا =? ]dH7@a3r B!M?=o;Han@#-@w꓿ B!},?[zo";F_4 .[< B1 %Gķ.%[YAw?BP41~_"5)l"/OP(^?f>v9„(6mP( 7Jmb5RA}BP؃@P4Zk鳨FCCc BxƑ+nMP( BAG}GP( r B(@P( r B1P'GIENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-fedora.png000066400000000000000000001276711364015200300273160ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxwyN`A9dJV^I++Wʒ~{k[u컖lږei-2E r &tTsNUWwU|?>ML:$: X](aW  CAAB  UA !  V!dA* X@A2 bBAAB  UA !  V!dA* X@A2 bBAAB  UA ]fc03` Xo@u]%lwX|6C0j̢OA~JAc W/ h6vev@٭CM ",J0<v.h1-AmK@AR!@Yh_'A?6%5z' 4O+#҆@ \y1PϞ 5C@"\D(/KӼ  o n*R^Η>P ' @@rlnSp>4@A c ܀P X;}? /ñXlMr߭]TAЁl~in~~k/?񧟃B!$ߚ@y */;0yOw7&W  «__/1yy?MEOlًDZ *>=۶7Whb A2x~ܷOG0 0l:䙖]QbN=x@G?^\*KXM D6W>C_S0e8 0<N"i e' P~'ݳc*M' "@IG F~FOI`s aMhP[ +Q|O7COAD g> "YE`xxh0t)/Mcz,]6m& "0F~{ӶݕpB)RycN<ӿD?lڶ|񞀫HA+/;ǎUzK0Da (7M1\vKA&/<큾A֯(t4YADQUaƿArW޳罦 9dךc`ˌ  `&Ae̿VrEJDADUaHt'cI% 1+/B`+5ʓPauX|⺽{?L!ADLAO*ޮqÇm @<90 X rR`ГEU6QW~{E Y\^rğ Man9 +Ҕ{G?{MA m/~`(ǫ\+ FϿnN}^6)t.屸ڡJA*;blW p }Aw㟽?-[JS 9' Zc@}ԟ>}` ^11he@`CA NA,Ȧ[m{L:.ϓACׁ9,椎-۶odUq0H?+<Ѱկh||##c sytY䚾U2AANLAfw?KW_8[J[@c0ㆻ28::FJ "tVr%\Π$o-6i{ Ľ} ^%)̯s a/gbAD3@u > M\#> {5Ov2,ќ+ԁ5knp+lj!LzADKZad@YTW:Pm, ·g￟Sx\׍^>5\e~ADdY\)`~ 1]Rx-ep7W#$/@#Cw1?ArfrAAby^f%M mKgȪ):旨_dT?Hc( iED4T=%(o8 *X8LL,&xdc vf%[D]jc1KH/C+®R蚆B.B.L?EQߏu7b]X7!D k]ش}g:qP*O V[ZXgXhj bt9,PU=k1a܅-AP51tA;ˋ8w8._32ے3߾1&j,n a0QV*ains38u b8֬Mؾk7% ! r05Wj¯A` J%LM\$)nن=7ߎ8dbH/{No{PegGqOAd'/r,Ο: N[vn U Īcix gk6a LAQbLkt^oÁ1LUݥk%hZVjб_‰C`:\&l՟ )US8{xtL{" Q)*TEƀ91i Og Ġ*1@׫^lfۇ_57܄koeu&09vN^8K8s \Ơ(q05R}/~`j[;=nujÄ <_@ӊD)8uU=q[7y/bNDGɵL+)&ښ|6?K@sݴԄD5NtO]e\ PĠRz IXGpqlܶzia [>t /A+,++j2V_W(zSƒR ꠕJx$.;q=o@WWWKK!Ժ ȫ/<^~5E1 EhH'9:0ƠƒV*@ 4 Οsرzx׽-'AA be+¯}dV$E p4ZA% n%gCჸpn{O`t39@H~EL/=#LO\t5Ev~"| / \٤1/~{(B@ %%CT` ٕe<`d ejZ jo Ϝ| |x#EC%Q}}[p$yZ;)Pچ>*^7 5p \T^x<v=J%`aa i,,,!qh7@Q` Qc{RbB's5F>M]߼>g m=ap2UU6b-ر}ڼ[7ntbJr\AC:[rLYq9L\D!l4yX6$] J8|q w6(J[C \&}%ShTXL?nk`h[Lpm Tvuxa زzSaɸd[¾C˓83>4Ʊ43],o2.h\mπPeowa (DU"K$]<~/=Hy_,f5SŏW\PQUߎ{oڂdƭqou=zz4gnAX<+h7KF@S?6n0y[e hG.:=YvQ]*xR8@C.$~{N୯ۉ>=ډ[{KywNb"vn L 0yreV;%X\E5~oe#@+? w݇h D'/(ɻ[-,(>c ?z\ǥԋ/|&ᇯ\FWU{q-節(tC=\ft[E ڋC/dҼQL;t(qĮ)Zoɾ(U<~-{];6;×: ʹPtK>wKӗ)6/ aP̕/Cc'#]o@#: <Ç0;y &[)W0Am!/<N w~#ah/=tv-շ[nxr >{ W58c(rFH|}[ІA"Ͳ (0iTN IDAT"wlǜ)5ܭdm!@w~=~ދZVv'nK+|wo)L؏Jc+FSbP bw7B4V:iV%|1'$\._]zėd˳'\53 >DGvvnt38|+FvvPyb];J70Ǿ52nz""uD$rx_̲(Pݶe~-PwPD*61oǿgTb*'z`yзnCCeV%IT]ƕ\f @д"|[Ż]}Au}̞; _}ɝF{puRqnll׿f0]`Zw≇ź9 € Ɠ?X3_ln?87gP>WسmmUiK~Po-[w2Yi : ް{=w4w11c|uKGD_4O__o@"zж $q~OiX[n|F{h oVb+y<Co_U HңpcV#| JXnRtc}oExr{m}ɔy/-M J7e'&$z|3bI(DݕxʛhjZ m0z:,.;YZXew?zoOȆ> =qtgGޅGV>As׵vŎ;ތ;қE5i\xۇzU^"QwqoV s|(J־{ҥy~)udR)~ݴ)t=quǰocwa:L<(h$0ԚOС NEWW7vHg Z$Z3'quS&a=tKzxjQRK@OĿ p櫱| Ē`X-TqW0vXu$&TMS.YbxlTn] DUlW?Ԅ;}|+Ûļ/Ǭ*/4Bu]k/<ɋcԓhO"`Do\d5pe/!t P sf6|OV}@al q~ne#@ʾG0?=LE uaOX"{!hRPcf9HRBMz} k|z^!LTyO#T+ayi!vi5&th-\|;(k(q7ooO{$$zLF@692xEyF>k+E&[^vC23Ey1r^7esRS_9j?k˟8 Є0x\KI^y:#@QƒP]Ǒb"|eH7g0:£Cr(+o 7t7HY?N:ǖaf t)8R_ZYLشN_qտxJxi]@3%x1d?jwJ[&,Je~cRj>AG??qeo57yNg~5Vn\zNgfVgOai 3}AjwNT?C އnhsxL(en+3X;%k5[85>'`=AUsMM#@/[2_;nG_}t,&~[=/>quuG^~t0dt(/<ڸ?yr;e'QbCe7ebKӄ_/T)Ä^=jX|TT; ?0СD8Y*7F8:|Ɂ֊P~k7"$٣Gh2*k$ۑKNcr2z7]!a<*߿Tzf/Le(_|ܳ> Oq(tgw!+؇މ-#!s'ҷnm̄t 8U'szVv5A finΝ9$Ndn^|%ً{q{n P0Ǖ fW03%-݅.t7$zNГRћ;_īg\^ig ]7cyv V1@'N]e'g7r 7oKX"%o}ꃫxLϿ6?w\ f$zz?P )${?6a78cC/;.`!]R>^1}75D1z 3\u%ZOOAzaN8-{H6A,Amt ƍ2xj{U*RkлnÛ^a n؁'G&3b5{K&j2L+CF9Q*r}{Y];\'̤=t)lA Y܅MsXDvyy\; 1Z#l=#ބqOfơkEZZO=;~'kGQM |1?3{%VX/- ~Ϣׄ7ȱ)e|\.o^׺Xb #3躎Kgx7;1b= :`5| 㘝TB(sW%>]}'1KrG\ҊW}[ћj{=_;>}|vrspфV*bfξ/z'ڙdj(#u]h8N':#^20nɟK:}^rnB#%Oыco_~&]k/_# PpA̍WM޾K- д[Y/ڛiWJw)h7{^ɎSz0p_ۗ}pmjXk?ϡ](t?}jX0i Nji(Va>cp5ޱ!۾IEz,_aPKwE;jZC7^}c~`Gs9iw]_ IG'bwRt|N{  |x㯽Ttem@1R z,|A@13ysX]_J؇jx A(xCFL/ ӳ&N#fyvcC>vUгvWZƒ97=K 27*#/矪W)}8Eɥ=sk/>;Q9t ԟZĿ!/X# YWܴcnni 厈s5DP"`KubUz6 K#q</\!I̜E(|1}#6ߺ>*~t: `ᰀq1t4tT,¡g17~F/ ĆG7@e(2Mυ^+d7Wr Ο-4y gr7b|+~mGe\&}|:uO2,[}>$OA.7:h3YDLK,A??>'fR-Ӈg/<@C@ #^yt1L>a20L*N J%y&N@71r0o[QC7 FyW+2kd!9=jw jjԬgOߦJE` 猸i /QZc/|+z/>hT s >#;xf/رCO4bq+j1^: [㦫bN\~1Di9;b)+@#k+,:W d\N "B7#gg:s<1ť/7vd)cf/iX~8l<@M%# IkAF΅LWڦk9z v6X|_tpwZԅsȭ,ƹ / \+NKetp[Z~+v V23A9߸.ٛ'9DQ}㖎]s̳-^9D]a`%T^7p$,cHtAaaJQ(i+ȯ19v[v[yH?'tapTy)aLA/[ X;=/S>{ k巹eܠFGx.O^{st2}*|<WK2 s*ZM@ pkC4F<EIkЊ,]1S @Ã{K<&S/bfl߱ ttco6O|z>xG ^?=Cٙ!Lk`=b0EE<ՍBv{t(j%-+(DUBrDq\Ռ8M/LhKR7{'+۳;W+C*=m3[ pՖǏ/#f{|Gowֻ'%AD+nJ/B kڪ\E F者4H8~zm+&TEg[7 dfVp3ijoY߃Ϥ#'@: @gÊ)J79v: ߴzT񬠨XG ^Bfi` };>mue=/Y&t`qz{Amus.yuNSklҰܔuq XjAz|2蘕 m $>= ƐI/#|=Q􋴐cP%N_8Αwqurz.c;ry=J7DSѧxZӧ|d#yŏA%:>g&̯h&i(]ʿ툠օorf.Uz]Dv~m?璧=_f8Zz:Xn ůs8y꼜Y199 > _k`. .,dn㏠[X}Dx ꋈul'Ϯc\\ sqzd )T=9LHP[`JXZ^Aŝ^flWm!Cz{N< 0 p :i(PW<$OVLA18G7D!ҥ D.0zZ/Z+FmD# {n8ı@Ws7I^ w_ {lyraQEQ="XؤL<ϭQ 6)j0~2B~-r-> |FGCčǿ:#@&ij`.iHk,)J •ˀ?!K@EQ*˽M5Mн[hOX( + +p!P?>`*3H]$h/@5@+q iu4V[P|ĭWq' IDATc['):t§WwvM(Hb4wzk̥M>!HčWleyjߩo)=gY6]ĉ}yLT Kg >Q5ͣXaynl{ =LB2i~)aΝZs>_-^=Iaa33'ڴ9]@xˋXvfaum *s8!!Igj{PE~C45-@gsaV ̶@=o\Ȅor#s8D="."zpʢ˴! m}F qR&..sY (Q(1蚆Љ@ 9Ӽ ֿLhi{}#:Qve RfNn Sz_#]ձ;v h++@=prfΟ2w:]XT,%J6[`Y.7>MSe2ȥ1U.Q .ڈ R5v٩{V;`_Kǫ龳U-]/Dfhϩ~:FeǸeX}<[0뫝rV:Ε72Q= OJ@^ ~ fZ@Eneetw$q%=t{Y󧡣6&+O&_I$T|*VSF!ڽQtV~ K [7jKUPMH 78De]f ۆ[sDt}R) Aeة6ALpãxWqnኤ@8G{Ï͹. XS4ʣwi㾍>U[/[TNUtyf!ǿKFwGXhn[dɋ-7lZ'WCD,-V7ޛVROK׹_ 9X5wG?!`IpW6=. p[Fkcz2DTk&u"C9ҵ9[R/̌rnVH-^b.sF Z`óy&'cQr:/}9؜!@|Tw5h!c*9 .=껦%XXşkL4$^HQ,ٸ ?3זԒy0,p.xFEwi5oP^K6N?!M[0?= s{J;%uuxj{P۞Y#Ԡ}D*Dh@gʖ6S075BJ^-H~Ν/[cK7"lث3D9C@ (y^ƔК=ۣ(*K4JBT,JԸ1Ѷ낄Tl[D=jW2#'qTn\-E7Nj`)VnΉ'f܄`n,KuNv̥]/STQAdWt.2Rmik1 Ź+[3;(UM#@0vcWQ/AT^p_?/^8=]~uI5NSfUDqߏWEE"hIP͞o0Zmi=#U/`ɯ,{KVcȤ7JY[,b:D4=˺uVys:8m%ܶ24τU\.u >Y4 As5w#__N8{<\9gKMi-n r@- g"i5G;+S0JD!hiηƔA͡0_(&cp:% my47A3{\dwF=Z^+s3aW)&@-%Y|6rC_Lk`] C͙ռ߅8[#6Ou1P/>Uȣuh"&<0m'DTZm029$2E TKf dZJMP?>n~ jz4G=50V2?S0ȥ#)*07% 4u }@g3u@(m=!.?vn$_Q+Z\:g^p^o6>ožc2kհԥ^5K`Ա.'lSEֻE'*rٕсjZ4:b!_XUd{/q[\"={k+-6 s<=F ך =?vL00P*4dN@,C>5֔ye2F7?~M"#~-h#@TjDť~:cm:be^=+tNϞj3[?Zbd{uƹgbNro.po ≆8T\wpk7]X"-Q:* ,LOwM h!34œ\OW&=4V9df {~\7&wX#/@p )k]?,M_ DŽ)XN]A.ΝYwO˫[,=G:_, wVe_%vQxMaLUsa5*D?@t9ꂲ$pn}juq5ƆݷX7T?\k^7c 2K-OsntLBm/,^Gj(MjNz[ }yUˇs]؅|W=uhYxbawGZ'u(j j</*,M_V* 3)eQմЇ" B.+j[7Uv[s剫sNmǯ[ ^g?PZg-TpGPN?Ȍ1s (j]@ 4e #$h҂ij!|?l6@R|0w˖m9H9d~-zG' B.4wćXQ MbavЄ!~;4~{5*y[dձ.Ѡp_!c8~pz'zG6c%ˋ/ioaahd4+ 7R7R+1iM&QUŷ ſ{E/@#DQ7b$+&:yϘŹZ 1BةC+}1cV i3<K5]e.Iz=k @s&9;7Շ]1eTI^nN1æpŜJACۮ#Ffq9^~ܦ)ٶ׃jZ Z'dW <[{bĿ=ߌ][ -r" X1EQ1f af=P+q 8Y*.@ܕ4{:`$c~MyϋBmJxAVZvE1'JM.a5wMrRWsA`!#;@$†=#Fd|%2i!i{=nDiy׉䘳_vvADO Ks%sQFy>O+oׄm;L*. 起:޿&}Em_"1B7"xOAsoK1}#]W8/IN|Of\Q_GM ^".1ahOD.y__+@oW3N(\7qn"nrn=]%)n@ތZۆ;4m"ꖭpdеj0B7/&t@zݷ>M~0x>8(oHMΓ;`Z `)_aTZ۶8@~eQ9EDzvE+Pb!\s O3O=%<_rf3uU&֏Tbl v| 3*@%҂&T,XKkkvFP _MoknpITLJz8Er˦ {*j.t(B9$7+LeOvk9kn/7뙋<}ve:[MsaW+tb''UjQJB{߂;V64R1[{ dH wh=lLv/_.Ԟu3_1S}k1e'z֎H ߿ W6'؁/~dUiOaif"ךF ֫&D*ik`C[AP˾w:dB3o:<N/]YӇîFSd7}=;p9B,م oE㪂_k%ڛy[U&oR(VwB&*A7^c nځ-ׄz[nmCxcϖ~r?=vU!4#/A=tݝؽzDc\=\zcD>y+܈;v@ E<م{nComU2-h?,\/Th 5*Y ]ѵ۱&E=C3tem^#'`j7_vE\iƛ O哇J5N4 7Pa ނuî AUt̜;+;wb/$&d{] Qauw{M]cPW ژ٥hYL{+3d-hJ 2h6~lQX"7܍d@k!de =7cTO (Soͣ,:oD%2+3+3k/I*IbYFxtӞi7}X0Á^3072fC7 ` @%K֒T^Y̷G#|/ވۋ^>:z"^.a`f΁Pu`yr?}iW%0*7Dw=XV My*` aoJҮv+%\}i^=vUDuUm =Dn`>5Dh :^U`Xu^:JiWFY􌦬Wۡ!.#\L ߣ~AY+TqΦR"xܚ(oWQ.bFF^ u`/,s܍L ߃L7&= D(zL3CbkX&6n^54뇥E:O=@w㯞D.Bm޼ꫀ7n_ˠN`U.I[qQ =ZhD}R[]OXU!?˩z+WFykc6oD<F:baph1p֜ 3'_ó82"Yz}\[CaVCe;I~*JP_c !>@$!_L[ zU!r )Y6n]ͷ^Q Md|m!|=_^o~->CHrgQc֪X~_xϽZہI@HCrRpkt#'qe/㧾( #fDPϽ[kDg6no RmR5> IDAT`qЛ7~mfN܏Z"wFN`x [P[keRy k\MXvy^GS;FCCY(ZN2u7fbdz>|.o+ޅ341p/u/r$H:AJIT-(iI4BXPZ—/={#9k؁j{Z]4Aqr} N߽~ILwo}z-D;ns sD"qj sk. D]3/nk;tG.-Ћ#L=# PĪװv۫Q\~A貖# +7}N\6;*{,{,>oc?.Tjrw΀wp+[GLMVN qEw~H T^`ZݿPWӔ49Ʀ02D^-c{u ۫Q.*H=lɓخ ʃ7%ܙ#b`ИBo?++,[S'J @3Iث N4]c0+(,De{KJdv>h0>[?8foKNK=3T@rAQHaR؆? ;7rmԫ sl^Z\v,f7^F4ܼq#.]r',s؇:ЋF72\!yq7IivKD^Yv^kp ͡i-F@KxxҲS]wK{r@/=!|dp`7_182Yo?p'&U,_:g<4; m8CN1@p4'*A$&3GH.jFJ^vp;.jtǒZX;_ 4BJvCzZ;|B{;ppͶ$F@{]8Ɖ7:WOoj "clw,Lo/]>65zմ ŒD&qJ 7|b\vߋpz풙 y2h[?wP\i0Q.K8h>kݿeq󭗰{)t }#Zxǝ1E('^x"+ ztΏ ;=O4aÀk!҅Qh s'.j o{hP4~u~cw|T#ǧg{(I,ZȻ CUF&Xb뷯9UW^k䷮94NYٿߑhƂtv>pAq8zWGȤ 5 QBӀɹyniBS˹Fs[|`l_W/ XN맵k;g߬ڬ q5;ja8s0s{7OryYÅ^{# l?]. %EU<>s(iuٳ!oq mU8~=x^}9'ܯW pi ļs$)R=6 `Y R@AKP$CO1tUpDJ&m^O#+cpsQvP~r".usˋ>%2ǧ z_EKMrT% Jޑbj)3\O#G"xYK"~q % EQ㤳 'ʁ#w{Pp.k &niWCmx@ %w pSrE 00hK)x)87^Gz-qe9`!\N͎q?v Q|#w::# !yjd~ Qѓ6`W{1`۽k~3[NO`zZ3+D): ^%Po&w$bzNQ%MN B?W]x;?zARQoQʑA0w.UZn%`ī)ll-j֥y.C˿}UP@k%x{w(@\p']KQ"C^'ş_7~P%2["{KTr,{_R}O};89Ý64Nۉ杢{~q;‹$Jp=ؿp .gV $J'HYcZYj.H]!5%~Ζ[=^Ǯ/h?/w`O Jo:: @LC0fMӐw:*%#鬗3dC@#ϝG<>Қemݰ;bp҄0B!䲐 QF''*U#V੒ [Ѝm2j(ת鑹yʥ=NznѢU)T)%q?>ێJ~AA,J&xK9Dm鮈y$3ٴ ^@ty7?68*6LO%~C-^ C³ (= {4#;٫xPn,bA훅iZ0:Vܳ/3=5){3.^@fe9jO΋-[ B^6X~& Th/4VGU#|{w@G@RCp7xeS@ءϳ *ĝ{OUH>L-!uqɳD3 8 "G:> FZX| 9{&BKz f>.hP7 PQR7vTp#,$F@o@aYC ̀$* Ϝ! 1wGft{ r.̡hJ$e[ץi;r'&A;dX7@r0 v]R$y bԴ '0d2掞P`{2{O=ĊYhm$mzN(|<' 19f\A#$_'V1vf ]clOOSUR7Խ4`괟@8z|c;Q5mNvMք[@\zQ,1خ=ԫwHqQe%E/RN{FG;Fwڽ|"3KቑpU0lo'~~g/2s ,A'ʇ3 BN?#c1 媺^V(Cbo$ xԍk:ƆruM ьm /׵m9T%ˋ*SW䁎{EEI1νZu|/GGNa?}' R3_u ?Ͷүc'N<Ja?46ogzA NZV}cjTm޶k:WĿd{~10wXc( t8=聑u83P-mW oh@w]a'wPt79sp /A8H"t{m:T5<R.]OFhF@N7 B+N.C>:NpBsXP~C I1ŏ 'g0<P7-Tjz. 7Ms^m^ 1)^`yԫeܼ.Z?Rs /4\GߑGq`߀wgN GZ<",Z \$"z ElZg zh>.z=](ŵ;iW00uƷ}*n17;8io/?7e/'KJ/NsDe΍M&17Bp=02v\eHxx[wP+S86k\u]t9Hnh.o #2js5wchޯ2ʹeXP%/IEk?&-,|cIlzeaL/=Xɶ!@=0ۊ{i>roOf)x G57 Hqԣ<>6d**Ͽv;%GQ%'tBs$5 G{d{&u-d(Lq]q΀cn5|2 vz`P_N7_y;P( ?_% -fPK[+"G5B&u-d㸽&KLihAᥬ]f>pB(jx.*oԈ?7Zyvej?0C>X5 IYEKJФd#/5"CA6'>lFwv&sq6=E n?4tݏ|wmxD]bx6 wGϤ̜x!K$#oޠ!`}'yA|acxFpuByAS oOJCJHUSS#!ΩߺE]F&^54^w)CzxS|xϻO~>Aj/M1[:N#LV- dHA ױDD6&fwNTEC]>n'^@yK]pwCk]f7.SwNW㋊lbep\Xo!ctɗT*q7;q(@ADlD8rzwZBN  sT)?v=0N# }pp?l2RϿ|@ IDAT=z(jpDKzcئ=Co+?ps2y GrMҏp|S`e9#8*ѕ"^*12^޵3:.jT391)@\ Vochr&uCX@q}y7kоVdwl<`lT~`SkϜk4H.|;H?ԲYs :,)M-`bv_j)h Q7ou0`O=;\4a{BB@38\?iHk sx^wgHW< vM -]Հ^V wnUadsIL=_;.%mXoDWo}}}_QAu(+8%PP' 6pSK?<0z0 x;qc(h^<Z&VK.4?vsw?Mwn5~>yW?BI]T>(z[4߳f DLz&Ct&3 kϢ^-hm5tG,<hO#6*> F&w7qOT7e ~)hF@V0{ MA7vZݢ~zbp[Rin{z|Չ$) WϘK ^!uV) GA@fYL5}>pTسX)_g8;: @p]="#8 "P(㿌/ZU"!͸oP@T[qE$*&̓ZL{igQsz f"jG?@>1{̿ٲ650k7D 6\JS6n_'Y!!ݽ|y7f*bV䄿S8ovO=1[MT5?fR!wgPڈ:cK{'_O~B}_*4 ;"{1y1<վ ($PB&`Y&.y r1*-{➪HLV%6ߖC,т&,8g@XCݤ-~ >WʸS/֗o?(iWşD{;?Z?@8|!Ll_,Br@&P)p[CsgBUн~50 Qϴ{ܚB^HuwX\W MK#lk}7oI*n:,I~!KG V!Ql6CmLPk}G7DU8Zofz㳇BZS 90%{qb; 8\Kn2l>="s7AC#.eƛ/vw+*.Ƒi 7+|?=vU!=I 2x).`Rg piec$뷮Sx 1Obee=HD]V2br|c=ٯRCSg =@=/O`FOBېF羅OOK~aHady\ UB5K_S(`RJ§"~_@ZK:>?1*x`p>Q#$urHfo0{׻?ƿB,^WoW>Y\z+H\PJL&#wcan|d(,="0z ]נ.ހRa ^xs1}^ֿ8>7OyZ-_kʹ+-_vk* ,~F˟H2 Czϐ \#'16{v3wk?vs!{+u|w\߆Y?hp-U1 3e O |C\Dn^ tVټ^[/ΥwCӍ8_:ىе6kWWQNݳa.z0 :DŽYCH. CߩH_UZo򥳘X8Ggv_ Y/k~$ܻ:+|/^ħ>=oX[[SjƦc`dCdj tIAwOgr`+jϿoab&4[51<||o Z§Y|Oٟ< =) ?`18Okna\E=x@/R  z;1<5E MVgcyy?.R3W>'<,^زK_ ߚ0 ?xӇ5-r^C(EP@vs^x67H j5 gfؼ}"ct"FgՋ{f1ʹe|o$T w"gׯ&aI\rdHIW@204>"IC V ?4EܸO?q~j 8{\y-|KWm=C7Sy>)\&6 bb@RCd >*n{پ\|e8EId r/y|m<˜}yOz)b &"C7V_%w?8j=^Zadj24M&Y\KXy x_aLԉkqG熰KMJo\X³/SϜC$nSRttfswy\5QІ>żq0;R/L0`xt+iW' 7IX gqYMב)>yN=GCY %M beK\זqMp._i&DC[g1pS}K'hq"ΡKкI8bnn1aͿ@CH, M\/[i# G02k`ck]Grs"&5dΟ h(0Cu crFƯv@{L09L.+iW)"ͤ\"AX:~;# JUUJ(6M2$ٍF.ciV\*] (-ɝ vbdo<Ʀg19DzjBbUy5QO4zrD.#=g]IހfrDB"JmXd021}00:)ZPaj5'Zco] pidr}8t.]3ހ,DDل^W8-_A[*RPr>"~; @{zP,Ց c;o\8 \05hOX {'Q|VV?û1Tj&JU'Ji@Cf"_z31pܛ= {~OyoN c`dSC&ZPHT51}k&z'揢oh^}G4 h$'ׇ$'F& ԜDVmWjVDwd@ b b ٝ)CS=:Ib)j/sl#g2Ffe1Tjvo b2:DݴUcgs}8Wrj?3,+*Dc/uðl#S c@ni>AInvgF{r̈́nhc=X^1פ@kq":-HT:{iL.\AG?2ȵ-TjuSV9$0(e40l Li7~[k]*a?5D3;"Ca oxc>77ܪ5 >5*`=:>oalVw 44Mס:t]ndm74(t* lꦅ:mCI`ͨ`rńi2 gB Z,ZZZzp2׼(k AK%Uj4 ndt# =A&@7r02Y,lF6'/1< @ïmKt- KV]`YCFn ,h&&qV"5Hbܮ!1ЗoD0-t0>At 0/ÞpQ[lUg@v+0z D ]b _jR55t:r{AN = J@ht6-l0tmu A@  1[X IN'J^zgA&vL+wmAуhj\C{k#@ b/,  qj*d 1υJ. H5O:=; SF" Ph92 dA$u ڗ@+AA@K} o6A4v'XGAE5ˠ" xhZrԞ k(}4M+CA1)iA,jpG$ "::YhYA0H6oțȦ:vc  Y!-NX@/ =@s U4[`׾7`kF@ 14ǟ(q6-gzu{|~Р!1pIDATA2nkxϿ]/pRF:K,N@@З2Y9 9̾_}?'Ӻ?9wr^DAk ,˴^ү掏m{A &DMf;.[e{U2l6 " :K6#@nWzق-ۜ(0{ٯ~❙?odsL M1P7-Abuhm%dǬUͳ_Ds?6:uyy. 4 ˩0܆GX~؃ҳ4`0!@ANz)1W~ vDzoíePgLo%\LA4v*wyjszĊ&feYCDA "f~>k @kjץܝwj߁uNx3Eb(r—? Og{  CЗї8]AMgg3veud vh;r҅'ӭ[X5II}<BZ@4[=igLrL_>?Fد:u͝ڷL/8J۵|-w|{*h,xA #kkl{fɸy$D=]K~q%ȍ t+%S,s}cpjqYō3#ϴpmA-= j!h6>`yc =;>5^"<~YMiȈ GeI΋{|?'~^,w?,KtnwDjwx67~"S4P~wqV)֞TZlz^G_:'pNS2c'aЉ\a{Äm¯4ԫEO7xC K{rC Ɋ | pčs¿#:~Z)KE˂ KX~JJO7_բ3+_ Cީz4_? {!)}iz|"+_^z^Ec⻃&B! HUwp /c^gnh-n'>pkm4Q4\3]S");O!Saϝ{s{q` d%EW P>Kx6. [^+~/r-6dk~rrM>flŏt?v [x C ю |]"$ɀp ? @s,$ܛGn;kO~"^2A 7 k.ǻ '5 PF(}+I~^r7Y{zIj~\M"`El\&euf"`&?,ӴnW~ߝQ ;2B%5ITg ^ lCIl# p =ӧ/ܳӍ% >M4):/`nY2 zޢ,L)M]i%0j,ǣ<@b\ (1Ɩ/<>fco'eo37a?6lfU=:c,7<xzp|pTPTqlw0?4љcLΛ r Fcc K,^xT6UsE^3q׿7*[IwCRM ܺH 0.8o [\#oc>G3̒+G|ԿW!Wn2,,q @w~6j) wtZyv}3jMnQ/}gt<1( 4Mw=DcELJ?:;LP.F0ГqmgY=lx~ WY ׅL,j+zU+ŕkۗk[ 랷*Kc!uc9`{x< `o+2L¿]# 8a:7;[EǼVG,hJ`!ؽyPQq?{X;7  bfkZ׿&8^jp׀w 4mz'M!K+xo2`A-lī@䦯Ǜ,h~1 "n }vכ=[L{voߏ\q   `oڳP=_R=g5᥵lDJh/Z"|,iȋz ؛X{uX=o6c,Q+ dWOAHo~o3Hv :lA Tae݄=ޟ0a(Hf_= ~ N`}'3y~OIU"Mހ=| @d A°+( {?^+MsnY_!`4+ M؂fXe &ea^o7>7mA{gk"V` CyIM# Ԥ {_io5@&ByЮADaOh1a0  "iUT6Zix-h| =')nGCF˟&# Lg6:NAA|E8A!  dA ؃΂<IENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-gentoo.png000066400000000000000000002235171364015200300273450ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxY,ye->q/ IIbmGˌbF801#ff Q!ɋBmْfdDZ$s$/޵eS8.D0d^e|?H_(((((,مwֆwa r AA: a_l_G~׀²!|iBJcV*BLRO1 B;;V˕jaM5Lc[#Z@ ƙ^ܶyڶN_c&s\A`osy stkkl6VL~o/aBH#ZB(:XGא/ sʕOjk% UAAAAA`Y7x~_=lb1@!\K% : /Oݼu Co4T<^|_ʛo![`daK!fk0{%/~w>ctRPH8__~G~~B/ !ER_ObL@@/nշ.q 0 !th+.f4r"}% l>̇5VH2j?O7w!44!*u$H_ `؇{u歟$ *կڀ^|}=Ec($0^/Chi+WU~9TAAAAA$<?M$$k(+Bz6D^V'l5oG~^򧔡`4r2"W {Ϣz v{nwS@z#1l%'% 藆,~?" ?}_[d=7c Z٦JDVAAaC?(AfU=0 _{{Ha HOZ`)0KMIAIK`_Ok[dQmŠoao,dd.©LWBAqX#ѬZͶK}uϯo}~~3T\I N2JeJ,3lo \pxs_J:3?p`c4ʻ;$PA1l5 fTDZx=?xpp'w 0d38R6KLoW6 ?Jn2Ge)#А7'!s _6䘀]Sw?_Y0Zz"1N5a)(nySlPʄ%`?0Zg5n U7}.y-II 7iW^}j} 7K!}E .+ qPA 8Y_C(I޺\b?Ξ?d⟻#eWPPȍr掳"p]Rmܹ{n> $`'RPTH$-ciB4C;/ K!~E eb9, ǹM9o'ޙ-$>ڵ힍%s"}e`, ]isI^$oijFX"$RP`]Rf'>3r,㛷c>A,L_&BR-N y]n7@AILN uA2 4]Yi 9O†!<'eԂV9Ps)8ifs1hQJ ocAdfF ?Iiom;y*(((5nyŀm{J{ A,ۨ`` r\p )0!)_&B&j-OeB3Fg*1B# @pFq%o1g4n O׮s=m[ 89hR J1E+z+((dDLX JO1`EdsP&t]5] k?׼U=3,1CXعsb]8- Uj 5g5i%4)ْz j@)R @ ^\ ℁1 /L?5@ 2WIg}2djgR*b&>LNa3h]ӡ\x`"߷rSnrA Er`=Őb̞T4=7|yq$ kQDv`/Js070 vzye CƳ;];sQJ` -Pw탃>8pqBpD,P'uҎM870`jUYE GE>x~!}T*Uj59: ]aS/d~ֲ>kN#}9|3ܲeoҕ +òM-/ R*?q؎qZQ &>8?'p@&[ eaMcRr(@*,͹ }èsÓ*umTj=}覝ZpalۂVkVk.E|A c !tcbA7P:V9RppӬ]i)iBH+8=>50Mq@-,\Hke Yg^,2=Z$]8՟af?-DT)\;6sP=s/51ڐv=BɤO@"}x赌ԙ(օu1Vg 0oU DF *ll@Hjz}+*(M)4M`d'^2M+ &)RE8Gyҍ+-ˎ$-ΥKe3kEG|}0G۰(dIm0T`ρ{W|y!Q1u,wBAtD;:8ۂ;|C7>aζ pNaYCXj V"3Fa#@\ L~a?"|r56!`ᆂ3n %keaYmXٳ?5m[|1 5ypG`x=7A~"HBe@t+t! 9jdXJz#E`pRB@BAq|Q |!`uʟ6O0YOyP!>,zQF:xpu29p` /]F|ir -O7 05a4v?uL.Hpt@G 8kӋuaC~ ͝Cwm۰mp $isX Msπk`H1b?nF]|^FޢαhvQ\XUm_ 5ˊ$X3>+7y\ux | Id>Pv_h*hjh}S ӗ$9*`}dB؂ =r !}kkBg6hF3LeA YY>xD~cz9NZ A'wPc % B*G{Z2OA>yOsMa4q?pD1@w6 fV*ȾYnOV\! TkyUā889p|lFhjg=`aEb. K]57w|#邀m[Z4Jur+% _`QV^2tB>ks̺_eCﶍ[LsSvs]A?S۶]5z&Zwn]__OB]þY!kzWW1P|!^{y`*c:@BA=I `cA` ~ 7-*ϡwZŅsFX[[[jC!.^E|BR,e<))q*PDz^U~p:sSc~$ql:5pе*aj8Ѭ[+ϋ,tn7#r$[*{U'(2"}h IEb ȬG­7s#|%\HvH @B*;:x;;$5/X1PTh4$(54͙K۸дQ*{MI[9nj7O:,ETZvb %9Gǔ1 ?n8 {P@3^E/e"[_렾H  :2%N8j5 =[7_ 6lMZ& k1A®]e[/Po^ߒO]-BJHY(s$SB(âPU"SdHeח3vg<~of_۶Pݨ[+zP!33'V <%< dFbmgICuw^q,X# ن|tCsh4뉁}i6f[mIN W'ˠ4&9JM);R-䟡ʬ@hp^> 4@%dN>:oZܾ٩ L t{{Ms:ӂ@F>rN0h.,18~ {-̑n;5SHCfaTA`B@#Z2fЬ> _2&sWc7 )"EKM@M#WEK$V:߽zӆYrKp >noy7=|%"!?.gD# {=8FRW X~FAuQyJK37YNnRB(C-yqvTy@^>/>Oi=;%m A?5ǿ8t(LrzumGM~Cow v, i;+J*a[ U&H&{L O~sJLēXf)Me~k@_[ r4+@^A %JN繑izZ&h3./RvB~~9^j~9z.F: 4 sdxK'qA JreM*^ 0w$Omqllm5o 0 :[3㜊Ol@<ْ#`.swk-hAIգmۉz&}z0 ]f~ |mk?s7_r:TJBw&s/>e2R 8_B3=8f M)Պ`+2B(ݮo˲Ft0 ""|pww;HҺRF_v,Nrm,-xpC =gYvl4-\xCRM*b)  fagqr`m{g1t3Ef? 99t۰u =}޹>[g?z! PM2@HD(%†7`xyo"/^MǛ`gp| n%N' `al #Q/[9B@yQW@A+@ުg 4[eћ-d~-<󟵇`?˲0]uq>QW@Яz>7rU! ]>h1J"a7qLp| e lr_mO?a{JL,mͦy(+[)PY!fdu@ѡhGO[55+䝵%̣m.m0q=Az.nCgn@7^TVts_@P1O9ա@?Qpw0ЛЌ7Ȣ)+u|cWl)H izBybJ .\8wW*[ @z^ǁylZj+8N5u@51QXJZyO yaAmK֗Rn1l >NC3D7Skl` ݌nc87@Ӂp\p|@fj2@$4 -` ͶO%0FaYa0r?5`~! rW, 0ܰRla,e%,; <ȟkiG=t](8>9s/dnA&) t|cq.o^d3mR]?MH-wrxA,d|*zoӹ'[1M:k@\e P<.?"Bue 'EvYV/1=RH nf1?Bn  8c t 8GIһ^g z[j3{9`x&IGncca < sqo0? 4ó (Qt^Ĩ5yu霠lI Xd`ṠDRst+]d3'-M\@&>eZH; FLH!p8@ۆw *;ޥ/ ʬY}d veocB-x i9O9eYim[`4a'4)}9p|<*UwQH=*BARI#*G_e˚~M78Jv/y;fbހ @lJ3 2F1F.0@0&Ggߜ |&P)@&ş'o/#%?I}:Ac?:0k 8'\ixx/A|T$0TcI瘮KdQNԗ++O8P=7 # v+TkB]}i*l.ďn<_4v.;jk[6Nj0emRh40_0''ǰm= A6Z?6A?x)*O#w-FSsw D/pv(^[~qar49F#bH_~ ;~}\ ]0FQJY% V3נʴC[ &<<"~}i7dV{۶@Y|-,?>>h0G*gnỽk?|;{+ S!rI㉌07GEsZdp-awk%< &Z8e #k % HHۢ[9Vy%ԛ,Q:fhdy,p0>/c LJ LhšxHK+_@LF8) $瘫;ά_? `j ܜt]7^'mC)!c鈷9t#\ >U[ z]*`I梑q-.ޅP;ݼԃ:3x?s)hhh}D ?L?to{L_Yiy`m:V>ȋU!Ek7JH Yc.s:pz lNp'D:lN\nH/,v&7yk0X8<12%1KR*vGp\$4~кh\j+]!R7 33dB;+D cˣw&)GNoH(YcHiW39 8zcv;ӦJ>ysrr q`֙i۟7;VTOݲ3wr̷ۙwC/p~ ?KWo 機tބ:un%[; 0+72/m4V@7@19q;x=X8`gAᦛFI "ݛhH\(u[JA/q2tY#aA |??4km݊$A H.L'з3nLaKwQ4vظ XM9B` ` *v_?M% qBbM"wed5G$8)Ҹb8@WadhcAsB4<۸%0QȆlZl]|Vy+rH v3_bЂ?LHZؿH0ë Svo(Uv;RA\5)f(^Y9YTm3A8\p4c8?LA.C/#h⾛7I]Pķ TLd⋭mqB>$IЇa!4ﭠ 8 ;;F΂k9AKR͖@N ,} 3mbuL&Y,,s8vE/_cm0`"}@paEY;ۗ=ew"̴剁 ]b_.@ijG9h9nIYRn[1aI1.gv^Y6jr @®djW;W%\@β,06]9G)8>  4?x "`R㙴9ϗ7V_v3 ÞD'"Y&TPLpry{V+p籷Èp*d q1@5,ۦHT"VDO62j'q*O`n_r잣M.%|^a!aIػ>_|''ԽFab auS3*Da*lH^+22<7uMm?NEoGjѧ$H:`l3Tk<^8>ӰwA``xϠn':2K,k~+Xܱ9wųJ9lZ\3[:׉=Dm¶-]L$ѩ%~3_V8ȡNg:]z\<ۮZX4ͱdOm5 _0[ݧa R7<Ήt)$80a3zu MPj_cx*4ǿyB`@90aJ]2qդ!UX Vն hysrUe1VmMK&A*ŻtTj4Cg߇ã7%l$%?al 4!R`uh-s. , +c0Π#y >Z ` `1zrP eOc@6-g:.:xQ3*rL4+09;Xr@sU.d֬ubߴO)`Пџth},X%I?UŊV)GF!\'¥8(<σzIPh [tcrRCE&GLe`P,PJӁ&5 `!`1JYy9w8>3=4 <F3Zql0FQT3~ȷ$0_s-Փ_s^4B`I㺮4 HߓBmc8 =oL4@2掊 IDATuC~#_\+0ha ҧ`xNk&zp=kDqxkM j-pt=Ӓ:  Pkp#J` ݡ  i:kcg{W:۱AEQ<0QT9졬WKMJ!4 r}M~u:''4 Ed`7ɘΡ +b'u9NN(޼RVq񒎋 aeȨq *"mNԄ¶E0~_[M F6ql5wݶ>xzf²-T+UiS?ڒj jرY\eC7` ? $$x&AZ!T*kVĆ5I^A`,W_qgGx9dJ < R0qXFpصo OD:4g ܵz 9 ah4g*"H)E#y]?4-D:c"%03moxmfSÕk:.]ՐjU3o 0Xx@&Ap.Gw<;"@g-~{{{R>GEcZ>}T* Q,J0khYde@!P~y<񅃑­Mp7(C` ďqpD0\.1_%|M{4l5ϓՇ>K0> be2},\Rߤ tv?o?agG`8F!Lպ.h:.]'b@@! b {-$z xҗ(@(i?H7 }SrsSy2ǖ<_I?v(wF)l%(zǰ^\h 縱9'>> ׅP07ޢdMh ||DIS:'#<> ]@چ0f:cԍ2GC+x5QaĠ,<^Ka ్e- j-r`ns]TykM9 v 6sEףޔ?(c% tP(bF̉x2+ S?Ccɛ8 |c/w0R,"&1<-;:WمQ81A#k+ad[x/3O`3:s'RWוO0zpd {q :pa%(zL6&>N{8>J"`~,cĭ0m4\ mT$ 8iK "ḁeb.H%aA TկW{z+CĚhա*D@ޜ:R|/_/.mE'P1E%㗯# ~K艠*kk!AbA +,?Ia w|Ds G fG>3)j: W/~+5+t d$; G†=(^u{nVaVE \a"~ۍYcd ƅ!TIt' V}8I'A]ŇpgDkWocЗ |e ~f9`DT@ *W7nPkj'@j`ٖtl1!VE> A RD~8@h25)wD!-j8.^o|!"nH&w),qt ao 4[d/_&D@,<5ǯ@ ppq1~_.~S߇v =c0w7 #>y8z| D?Fʌ| AOqt&CXjnOǷT+Ui_,sa4ӵM0[eha9QXҎ#I\eK([3h"=hck*2;4=:/>/;8>dH{v5v w54ɘYhC`b$/swur 7`a_GVC˝ xT0-!c~3AT0LjK6= T'`{Cǧxqulr˱P10P5Ǣm35QcXWeA?@.  Z.]Uiij}a"u82+_p&.m? ~@>t^~_7iӄ/5u~K .ﬡBdPCۖN4W\:jtvح{S?Y+,&nh13}忘%d?A\7)nAw ?F h+ ~dK\SFn[m}G>l ~ U5T@p||$sNI)#׭.dWe^%31v]Wv4bCccT=bNAe E@ zp1z8|°Q_}A Kc1w~o):/ G8#x Wڍ` UU,mV <\cFAk%R yqk~79Jq7@D.]➷Qh.p=:mòl߱ōTT3\#pukj}~Iʼn')tqh(J"w3P*loeY&l>&AM!(p No Ҽ0| {n}9gS``và0 \e+k$dGV In@ b{ZJ{JՊ 0_ lC3 [p/ 8-[%Ӗ% lIH/HIW`[d?_ '.Bbn^8tswЉ)߉Gpkk АQ^D[[2LTQA6]0 Z) I~Q߫ iL0h_$:p^Y1C'c~rH)\hԙ f+a)ƁʳKxt.o?=>(u8AO0U# 6p Z̼?d e7Q(LD\8NkO>X!][@i;Q)"DsI?ì+u6ű2ˢaOצ^I=)]VW[f`u,p=Max3C /O@lp0yť0]P7hݷ|X w u<\ά4dCp'۲PV1u4NX_J"+G{&} 8{:шC oL7(h+-{?7Q*Nqg,,--Y~O""N#y]8unq mUQLq 3.B1K0S(ς jœӼih4 q_ӿnIV蒔iu7 иj-¶hGo?1PMq(epwzE*HD|}^.P,݆e3c?Wo+3)oŕ.cZ*:>X($BV{bi[3<{7L˂IoA81D@0)')}gah0S]Z2 lR'M|[)HGS@.Olw l@?.We3'a?o=UTA\R2n9[en,Q0אJO=j{ ry=Q"K/]44e/ZAӺM,,,2l@S4 躧&'Fw/vZJ7\W*Wlo5Wu΍}_R ۏ/i7R.?-0 LR nIR1;qCS ²c3m(K5ePh)& ^ُxY G'X)PsWvJܴ٧@q] ._2we,<=Kq{նwEzя=q&fdpp/.#Z"yZNj:f ?RW0 r sfӀ!+ ۲ KB0n7ÄʴI8tp}UP *mf)@xug96iF}EԻYCVAҢ>>E3q7ɿlzih}oҍk8./ e*S#fCКsj43?_e8@cfPHDCNA^C[8P1M4F_F~ D*i6]TJp@87_Yu~D<]&8.9TJZg>R*{pk]B.G&AJOAUTȒܗ0Kv/mFY*'}A|y~?.aiNǦ8E_l.='g$ls߫arPaJ6zWae}_Љ/KXUAtzeۆ"3RfѫU?&yNÁcJ<,f_]a8X Z@D[sV`ߞh%4mXNu]ضzZzBɃ4qQ@pG"p&Gp1 F _bq`yFg4M«__m8EZ?-r]"3d;*r`G:lPy#ɕiv 6g>.U4w;Χ} MYs.pC6CL1{ Khk]ATKkpLfjrJf1"\+ zϘ$I4ؿ NxG=+( =wk_0rvH y?C >09l}0Mֺ`O /xփ˶P=iiy",ˊEggNQJ IЌiկ+̒p@ة7qrŋH=rcut:^AUT bdQ @EN %9I>EVQ_8(-LEiܢO{r]7mlwSx0ï./QyQ] JiK~u5`{]ddr:EjMBJ Dـ J 65~[dY O^&zG$\ K{eABIx_d#uNxF@*MD*%%pr1n:V`p Y UUQ]A::l DC C(#L g'uRދ8x8?[/ў]Ж; О"t`nt1D-Om_?8w@||o~W@1})w[y?/gcw˛$hy^AZԙuy}fivUo`\Z-sۭkr@)e[ɮ}oW4K\ޝ8s $8BH КiUkEUu{_7#(64fa,(N@4mp玍{l:1:IBp~Or9$I˯<|-y5?*)3DEHW.8~N=UBgaBQG*f)ת ~: WRԸהo2Qu Cкg9_Wm ܼa {)<]fP:>>}??:͍^{J ^}E ldHݟb1#PN0~dׂgw\O>_l\2A\$J3pժo@AZ`B{UK``ReVN[޴8g/}Um@Tf0&/k6c+eDJ@;T8}}uzEX2SAql7nH8~B48(2^xQ<#yc ?n`~^:ݭvX݃{[j&|r> ccJ7ɤ>}e,-/zn\!t0 !t:)/lƿv#"y8~?rV:>GRMl6 .AaUQTU\<ʥ",Hq(\k`qEB8 k{s_ױw4R~'W\ ׷^׋XFZku t q $Y[L?̠a ՎG$⑸ō.  q!x%폽 .ðñm45d2@h6eAɁWD/i3b}סi:M6Z:avʤ^|ew17p9D)~ ncYI$q?kk6m?!mb`%!+`f'6\}O᯹Ok^\NLD{쩈?98rif먔Ma:o?t*t:\{1;Fi<!$)_ɲMJj<0|&I^0?izKOn`=p)H/A"_ZS<@5 6-ԛ_:d9$< i6]5=,Q r1v+ʨX(AMV `?]t(ƔfJe@mFv#}4je%;_Tpq񢁏O76UFBvXJ@ί++{)P/nŃ1t! W.>r|&LV:c\^l68p)ex'Cqc G_Q gP[^i?L+Yx뺸{LIPGef}7 WL||wi@~e|{^N,E;m!Q>b"Z~>Go\L,z9eIzPZ”N{`$.v'+ KRW/0ī#tt Ӫ?E?H? Zq*;_')gY7{b_ÖSiIpOw4Ǔ3/3tUQ`c} ?ר6i4qұi^ĕc(^8~͏qߗ!dB\es\NLvuV{ z?(&.R[8Kqp]۷-|InYq?;= ՓޯTpTЈxJJϮ7F~*hVh6{)F(h4%Ҷc^0`⭂=_NոaBeff@!hx {]56ϴ$b4\\Xŵ UXغO8x=čLAzFDĠS3Š "~Ab#qϣ`}Q^r91 );J{i[qz'0 Ee}: 3'pQk܍l7H+E ϔpV#4-Ϻ+H@ذ&_7@4D ιӯC}{O|~<4yop!pN-w;]ov tvrIRF&:bV,KA/'!FWV[mH;ʁ8wDy+装=G_ u[MҖٗuTHk%@Qh}-5q횉Fc-lQ>,i:GЅeՇer\!կvba}E瑅MI"$jceFSgAHw`2ҩWaTVL`fnC$Q o&oR W:Mw"nn(nYRK7L\l\~xGF|iF },K @+28u$A"EW~ӣ.(ܚ5\2^ S`!Iwl&dYfkL!ؿ``s!1 5AT {6qe;SBjE ^}8a}&$,9w\ladc#Ey/unA9AD{1 @Q${46'ߙfE[hի*G VBUaS x—olq> mg?ƅ3EԪ@}x 8ZEf[]rt}aALz"zkdc]@o:ےSIS+P7P4E;/8~u>0/ƮL-2,;@@0;3 o+#%[qOpDž2ه[8Yow J8榍W llذ&{ f\\.pdnQ~]y{vH@b 9n1{fnr@xY 786; ѳ7 7gd,3Y<[aʲ9l:Lz GqnY7:;-ptAM l$[?f zrAW$":= IDATu$m'n1VWЭCQJI`[6ghiMu7C&WbBiEdr]5MGfgKܽah}TkM:z i?@R$'R|^wq{68Pg#ܒ;]IӇO .BB$n&#!,o"A(%eqIj- :x=)u'@RLIMcR4W9i\}|65; | .& (Rwu٨w"o6ˣhQžr.DREo:x,~[.BIN/\766k1AcK]fz@+0tioL60WQ=Yqه>5Z-3TUCp-T}׷΢RlA8Kƪ?xtH1׳/)Wo]/4Y޾e# ,^P|yD!:v2Quyj2~WӥСt;:&DMa&=?ҩ4YF= $L3F -etB_¾ 諛ӧF`;1E#FExI*RC}5qc-H@<R EY-&` !ńCѹwK&@U2wNd UHYBf P ˲`ee an\ps9&wtD¾հv9GO`gEq@8uBH)g> =exla,XJQL%+(8ט7b~q8iظ|qCXRʸ .$$?#0`~( w Bhz8cvLs) 8r yìܥ3_ j],(N6NM\>_m̈́:ZScg Tދw6D ,F` D(q, D,E CxGg| N Ia}t\)bSQ(_Ix}<- 3$IFQ%T*%4s6&ڷo Xo*7}WI6JKFOplp nZdu;{1ƍOk_ 2B )moh Xh.O{`0W'jU}rWl Sԃ4Tl3 [0qƂ+iw-]* !P)= YQUoD$7STJp3s2ظ:jR;k9}^, /naDvpB W/Gf$TYǎ/jJ P!E5^XuPXI-8Hi8G›ц:{:=pz9 ?0eиeW8 9,еTK #UPϢVpe\.´rI}wP1q{ VW^ʞg I= N{Lwn.b>(# C6@qRptz2G iT)ۑ$?=w9,R<"xz*!*aøW3mmS\K]C+q5IxGw#i_|PP_; τ4]etVdg2Y躎rL:B>W@.A9[of~ë_©^owqoSx}|py(ʄb)!ZMl 1"z{_T3أ<[C:=gT{C)PoTj, pJ*3F5G<œ8`ׂ@t*:W,t0-]La.r!qI@//??]&XHhGbpotF F<U%дXTʅcF QVXJ@3s89Ɩ=Q3@Oޤ\~xnӏnA޶h4D87a{DJ;8Ox_!+*$ô :'L^ z* MOZ-Ѩ (EUo u`R[Ùs8: }L_`==[Gأ5ड़w:gLż.cj?!M߿bĿ {Ѭ0e@b~~a˟IiTON *' )- 2KTEE!?zd6A ʠ\)oK*jUll ˷?1Y*Ea)I/ND`IkӼ/10uf@3muuCq$\"R;D?~ݫ҅`14$J)l(@kqd1|=S AdXɼxdyz&|1E"uQ6C6CRL\ll_,/># lKxػ' a_Y|?MTg%8Թ'/DXI"ehX;_J)XFӨAVHLMJύb2IȲ IJLo׬4M׫,6ptTz͛>Ph3 (ؼMOLA%oʞ'qh4N o@a]L| kG4Ρ|B%c=SA^OQ O <#: /ʁф};MX)F )OV@M02m ޗAtض$!+0ޜ9j`|#X ffP(b{{[[ |){ؿi"TEΘ=iuG;e-я#hdv4+^^b8Vy:;?I<}*^_? ?{>E@^; z}L0Dt=Mha⥀$uvV+ޜ@!`~~s&7ڸy#Y Á*(~)+_[h_ X+_H1;*5PL5rpBV1+}} Ŋh:KEEMI^~MűSR h6ۉFW8[ު 'vZ-he$! i4iYV033CZ ve,.67TfixYn_OCnea<zn߅4Y¿QEDX'mQ:9UQs"WQ N2"˳/)x8 ܹR\|K= c=i`an")moe倒$!ò,4561U0Lj9X:%K V07~V7/ae {^, fO`ew_'Sg6m&p,ף>V8e;exy퇚f*{ϢX{5 CǟRso^#m ,//v˴ɒC<jy~xdZo]bN#86,>BUga7]ӡ/٬Zհo-bcc۸Ӭ•pde]~T)o}{{;FGZZ7PqiJشA^'9g _S Z9ΏS)\N<@MĉGeJ{k.`om߆"H5ΣVrX1~`b"4C1HoeY,+,h69v6Jei5kU!4ulllF_^zo#='gfu|Ufsd?h??. 1?[1Ѩ#zHlB<ŀ_/BB2ԇC'q s# ps6w[.(*YB!abd]P&2ر~tUU( ,NBQDA6C:A^E=bu) A^:([ءW0wg۫S]kmw[1vD&i)G NVsX1|Qh|o8@G[#RFzڛH~~`}DsyHWa({̛"B=&9R)?n/1drիh4jX&GQUf Gj+ҿwr݇( h}񔉄4ʥ;W(K뜊H ТxsIr*:İ ($-malm߆i5i9U(xyydžUYȲ YN{lʲ|~el*hkn$!+ y9HE #{Y7 ,~{x'ԳKX! yṶW&,_Bz_Ƥ_,maeZȵ}L_o`;ͷc'pC2{+:QIEk[O,E$Jw7 0t~xI=(c{.Rz05M&K7c ŗIe0eЄʴ+&2p$EQ4lr$IF.?t#l7ju39]:ۿyKkw$J ?hbVV,;-Z8t?t=c-C^h-ϣ~g .{sL&njDƟp5IIy#G!'QM0EoP>F.lMӠiL@وTdYF0l6Z*TU>8ah$PB> cUT_ʡu#zDO=8x#m9.կig•K&(F kuMTU'Q֍]ߖqK>?{_uBu턴$hua@Yx 2,j f;`6N79U=/xRč]kd_G)O hX?b*:DQBd=|TK_bv_e =RҕEA*fx5Mgh!Pb0 IZӁ1~$?@+>L0)T(?fA>($Ip]4D޲rz 3ቧx(>\!k$c3(壡'mXlgxzt!V=rL9S }q¿Tl,F 7Cؼmo<ć +c0UH Ik2ׁmл)z j9rZ abiQʾ~rЯߕ4HV :Ǿ xu|[1ܑe"y_p_}?=a^7?Zx>8\; {<ík)[,("`3Qd1mwWD!Y!r7 f)J ,/>8Ǟ\b'Z:}1W!eIU:_U | xrc_d=>_#Go:~ 67*} zT[szfgP2gI@dY=3F[OL0eD>Ī7Fa۶aY&J)j*bi 2}ˏÞ}tzxaH !x4_@9T(It|zB$V^SJsܾU vB˧4DZp{fL\qbdnv\', n>E]T9pؿ[G0;Ǿ2vZ?X"&#л= Jqʡq ,}V Ak+2aǠM&Q^T{*M ;#%|y>o6nuKA~l^ q#{0U J)L.jY8FJу;Gj_o_ֿߒLb$#KZ)0ǩ mŀ2|r1*~y?\O-=EwF ן>=V/ڍO;l6\ #%I#0v+0(oD5|}E(;P X@!eV ^CxU|5^먬޸W Uh<% b1EC}>~۫~xmH\ͷuds;#_tۡAW\!Fs23,dYaZ@etrX$7y @)&pNugo4\REw1!<`?79FSqk@+4BR{r1EHU 1h <%K/͈ffRu)?ץ}+ν 3ceً Vǃ>~زrP0\8 2QViyp]4u oƑcs#dbX}FZuP `#B41}C\r@?k0c7AwFomV?w4˚R۸vM4,/~ 7_OIO@@8U)hi^;vf>]`mqw;a?X1/C, d< :Y=ܵdDh-E5ObXtS"Yۼu543;ttƁ}AeYX?ybt*t:Ԟ2La%1ktNIy- ;KwG5 PJqp c'կWTB7| +yO(~U :"y iN cǖA |q=:.=GD@2>.v r=i(ԧ}Siyn$pNrn jgi(c3?Hp]NK9454GzAʮ`ru|oBbtD穷hTXBȝTr-s 1 e2T0 woX.hy''zƭĉSHRR]byi!Jg۲{7m85p7w!)1@41`iyN\:Q,+ Ro0 \^z}i'yD:̄Sܖ CEGARTkڣ|vQ)ލRWwi!4>FsC\?yDㅹV hu*dU8|B0?Oo'B 렟ȧWk[\22Shx@6a#5 dAi" 7pPկo[}(VW߹g,+eo7"/AYfdWHaH YxOb[zIKv//z 8>’f  ʬ*X(,8l&|)'NIy(j1N8n)%b7I?Js06=CSYGzQ*.߿'N ~:_d2ܵxb P^$u&i BeNnT >8}'5ocVLJ*n?z ]w9fXYYWBPbWi"w61_D$;M8tN c^C^è񝿹Kz*爛pD"Oxr C%ijʘ lL&f:OWed >bw?⬒أ<>yv}JqiBWe[6E1aqlLԓrիh48*JeykƁ/=e6&_SAl)HmEy!!<?~R=RFY_λۥ%<NTDO.Z}wfY6Pnk&J=@.W8#cJav WAZ݇G5_;@cXZZzaUDgUJ %o>O=o?yA>%Px`_~kg4Rnƻ+f퀐9cc`0tcm4/p_VtݿyW,P |yQ̛K$>\FzvB"o"P4P7)K%W^?>^J* @)Ƶpka"!8~FyAT t*,gY6;;:ʈWW~؋|'myz_젺nc8uja;:ׅTNCayy7Lc/eU$6ĵJ(PY0H!ywۗ"%{W 7o}¥D6Uͅb e+l癆 ۲C=Ҹ*'}q^@1T&#njm_E<8؃>3w.߻yc|?TD.U IUϪ?䭼ԩ˔zIO܎v |͑Ӭ'qiEV\zi ߴA1bCn.V[i: 6:(Z<:`D?qbacLJB:C9!D'̽N~ }70/QSE3/|}x4[76MA*g\^TA|7tV !| N T8{e+;f7nWDmctP@`a}!c}/@O|:K~WPG5py]ںȥrPʧ +0w8J x8<.0N߱#?PT?U:D}B%w_2an QqyB$BW_(^m (|k'O^3~p;ܾĥe2Y=~4 5oQ 7 ȿi#J̝w_D.' +KtP=BEaު}޿h4I޿x# t6KexlgQw޺_,_1(.]y[;L6g^/2ʿ%r ,1)}(So[׫Z!N>3w.a_G2"Q)]4FaD/\2 7C_|B:[T,z@p6y̆f?^~Ce^tQ+\j6|(Y Äm9qqg{DNA byQӘӕ_cW[&Zo'ƙ;>;{" j*F&6_䏊4Iv0})B)Z,4J^oуi3"]/J}Q=.]yۻ\z63f~*VgT3&$8 h[|؟jDQS{ øe|<1S.})/%p zt55"2x:Q+? }Aq?_*7"lv˗Bɇ%ܥٽr"ۯvR|! $"v@53/@cH^/nRWj6h62 b80s5B p4)pqfx@zϒh8UAߡ(^ n6!)MS*Q5Dd1[Ǐ%lm(dL*>ݽk\OzCDJro>G>O\l[`,={ SHp{b?=j󰻷 XX_ywܵoPk]"e@q0_)OJŊr_ 4F?B9nv@3v@VJ&%qvu}:6y1l>;^9@?/:x֤/K8NJ;QpZBzIA" y 36]Eۖһ.jbwwc@&0R $$)G!t)2 014U7P"xUTPY _;o]]f.^y …| 'A@ ڙi"+*V Q8G~G!JP_ytZ<ځ<;[<ՇR1+9T"dUq0dAYN^P(„ysaDYŪ~O I+&egg7>noqBkPuɄ(L3M |P,tlO/9|m8?er?EzzX П.VCe'!,C|X!{s?fMg3uD/QM<' :z=w?VxL^gA'bՓJ 1P*-'"T#ؖ/C'`h B[UiL-ßj~0-א\bDM0Ža932؍~X4ut:d@իh6ȤX܉o|~8;$ow@IYRˍ Jds0 $e00-G~x۟ Ң"zЁvqiM;\ze %=[\\)+ R?cPI=C1ӆRzDKֿl^݇mgxwm@TOpF4K:B+WIFEE6T2/?俿WǏ%\ȝE=N]~V(TN ,R[\63o[!;Pb##Fٖ\3b7 n֟7;mT`6V#;Z{x 2ɿ#PdIZ!X^P}9F& Bq6|xgē?|z ꧛E=.^yn˫,g&Ae9~ \wLEs.jL?K񵿸g> >$IjoTÐA3w@4zJG)_7oLa5R۸g~| )Ƴ?o~'?q^'Zwy|԰RYo;PEہ)F`oyhw~z0(_fwozXZOypG& ȟuS>_g ?xdH1l乗&h, e e2pxy|zXxwkblW7o͌E֧R2 DTGG/Of7YN$}MZb2 [FoA IDAT _3G_I?t\~ҠqFK b*agFSQ\ igg21~kxG/0ђ=aC}VQ, B''\6/Җ/Y?Jo{ qO (g5t;>WWv/++kq +~V~J<־:4e(T:K$]>٫ +a{O;VQi&GAd0Xc,;>x}<^3 \ge~a)dyef T;N ٌ\v@r!<-;6]fD.1~[Ljd/<即2T*gv{]945G&!Chk着uz"q NV?gw;8~iXVRB&glV/AIb r2ٳ˵_?a ^- C;[o)~贻R^Td/&Wk[ظ/i-ێRsR42G@8NO}`2LkTnR#v@8\Gh@rhԔ<(22r%<OGR:?d;(_bY_$Qȗ !KпP +.4?&xso:Sw.ΌUzvbuB %mR ҙG@`;)fBc(I:J nnm6G{\ic|Jͧ ګyӐ肠CЋ3)oD ]Ate {wQG;a6767hCL^p9ɟ#3[T"t0v tGk?!=ƷxO#${õwpK'`T/!ht:tJ?1F!ʋFRf ukIhYn"~KQ({njmP,qʿ+FFy*s>"wNp-Xf@Tg0/@T:MST1tMD\Uga$ݯ*"Su.2 R\ i|ǸjRXۏ>?~ js5ѫ;&\} ^gJ<rJ'q:6uh[Jjur!o=0& :_󝱊E"jy`t':S$NfRXS(o@( W+pLL\v2f1RoA'C@|B{e2t&\es fED8pQn(B1Q*xw0f.,ס~ OAJ9DN"s>@"_.UЋԶC+-M\Wފ[WFM#;&xO?r!(+ee4cH7L8p]_T{Ov'_(0,r) [\LBk zW:~>u?J~K+̳qwO?/3mA{՚_ąW$%+os<4LJ-@.[,~ =qHB{5;T6[ :N30 ,3lJ)>3G8?c>1u@Y&'n^5A4v(sHϔ߻L`噿.ȗ(MnVXq\})o4S7?QΆYxQmJyfJP,|Be~"y}I6b7z&o|4ҩ"(xpR LI_QnT_* YnW'>K(ߍ(B@hGRb߿~2f(^D]ǵݑ2OheYEQT[&GHC0T>c$.Quؿy}XyJ)!@Cw(_(u{: /͈!Gס{v6/Ǽ<[48P;ޱ~gI*Z} 76KK(2e|2RNLoS0ii8-w( , OiNfRC;m4j±C~W (W߮:Rp>(#D)_F#H`ɟ)z6qJ`Sd{^8D{ ;{~Ӵv Q? ed@ jf+z*Z+G#@$vAvS*y*ϟϭ {`mfA޾ Akʏ* ?#n.GIC{k]uw u'}Ԧ>Q= 懨7t*{ bXNd2@n[g*(Dj;JF 9 4OX>fS49c`[Arlf$%qFKzr#sWXp@_&jߑ)NqϽ,+*޿%%ًhw~tX`yi-pR< e9w >Rph=.b7{ ^4Vi^%_ox'`yiJ{I4ӿwjf!z&7NYC_H,(Ɗ ■9^0,SAgrϽGAGC{EyQ<9s^s?!W^|wNu{2^gߐ-ijX3 v3KL2nj q\9. n$q\I>󀪟 "}1?hhO̵DHy _QNmp䯎NDGRF<}|&R1qNk4xg(gMy8޿+IeYC U!(R=MUNoP_4)e,X2sƍ#>sZ~"B@@1du%t:f@?'L\>`Zwk~Fn\bNVy-QGJ"ٕzP mHMpϽNx? Cnp}<]yroUgnN֬0- gpbB8&_7)gg8v11KdCEF,?$~6?/Ǒ7#+zj22ɫȟ5tc '5mɗ9^=/>Wb`e T|Ik3<'mGtH:6!(f̓ySn2dR<&kۘ!H_6:2?R^O~dCCQ?GCC?$!ȿP=d0,J&R!*&Ybg"Ire71z#sD!VN<@OS%w'+c6GgH) |lhor?s6,FO$(_V!P "Q 6No'?߯韽w4Pnd/qk})/au6/`[v[_Ǟ2 ^"+5$)ICa۸'=CKax3,`)3uf?d0/QCE id^] ' eGҾMlꪅ;r81Q_VI::WVƭ#|R^⪶I~eҩ`(C =e~APaMRt2cD@(ì7m繽4f{V2(U>cIAy߻Lk W.橌 w(h 7޿ (_h?OTb=82śxG/ƖFbda{"%90֣# /^Bl"ߘ#Lkt,8+}'7[obnW%hYd3NnT8cpt @#`L_"iQFK ][f@R+$w<Jqxz_>^{W s^!ȟ%S.BSm: `5A)kUjyMl~q?oܳzR;0  #mLr煣i3OL'w@ ~}D^?>4i#H'Io2ԌX2$bބu{i|;y!{A`Yca^(/rtgEk:ZVCj/oGv!Tߓd }nu~ݟ8:uE]ŭ[w[Օd!L:|]Ek6` no@ CN3y-aIe]u=x~3D*$!}p{B 9=CaPd&y`(WϾ`Cƻ{3<ӗS\d/b*v(VaY|tFۏp?,6N;z}$] !4]Z6oHHy..?W }/nc S'(NQ⽆{1ԭ+ǥul3:<m(ߍ֠ 'Vm>cǺlmg?~޿3q_3%@d :mca!~to6My6=0OJ*=&v.B?`ud)'B xB ضs.G!+& 4 ;PjW>]"}LG#kDL kU)L=~\WxFY7NFA]%#H__EE4h >F ߲Nf# ^ΗEvkMx^.ӴP(aY.߼O!cY`.d\|)G՗̬̘휤 S2B}A(O6ӧڎM|ڤuD]|G`Z(݌K\}*"~ #Pg,LFk'l`Ƽ]O,닗EWc`CYO6! +z N`I|5伸0 6B *U ~MS !>Z@`ZN2}pa҂}NXvRgr 锌A0>K+Na^ 5y麎k IDAT{jhNE]݋h$YBVPW| $" /d"8muDu­dQc@{ ρqmw߻6vtUWG<޾p C2'H_K37C=`XoC+6NX V},Z-JyV+J۶ $v˲Q(v02q?:Lt̑ENQ2 ]'yzl '40`2_AjOaIYWꋂky]X^.l2 >Y$XYfž{ky/^8ɞ붱B5y&C.W㤐Ig!pwo ͸Ppcg <0lI>;'ns0ϖڣ~l~0j ZYf@4"}90pl;%*6ociqac 6OCb^Ty4ZA?H1"zJV GCfaiJĬ~|^JD/ʻn{j+'a۶I/W˘l60gw4äbֆO\8\ lpq;_ BA泬J)tݮ' TyU))OgP0PXW0 ת~~;~b3"X[hƾ//sԩwLBP)/R^ҏj]Jc`pŽ_^ r|ճ|k5b F(,٫yn`y&y}g\3W/LP>kt(_C06XkQ^_(X^1WF?Yf9qzc P^?|/XL60xp0$I)8'j \=G7>L'VSMԐ=2ĴQP^!UiQ.-Lȿ[&¢% T_!Y<;h6t{ϒtmcov + /S:a^vfC= H L(T:oi J>J c6nkvԑ&O& T/"]Q˛!%XEp'fs,T*Zmwn{"*`ߴl zb~_#cl;u$~b|a o^7N;H!Ȱ=A.A]ǀ4^޿=J&:] W^$'Q)<"iuR9MS2 Ld24ܳo㏿f[)7ٳpԻ _t/5 B(D2htNJ:1yۘP'4Gm#>Jݾ!'N3T 2 (LA#~O*/_ՐJN"[=/'R?olDŐ*m`iBr_o&^{h͒] '^?z^z&ӲPȗ`Y!@&F[o&c=?tUfTm>$d| A v].4"~elp( .7,r c}{5Wo?NGA/?ٳ^?sI|# ) OY&0ƯND@%E}A%0 4[Ud3]t/ÿ_ ldK?AŴФ?avpҀr*R &Je&Ҽ}٩gK/nO#{fXׯkjRvfYHc?~GAzfqc=ō ՌXԷF l<0y.Wض4Ud6s_b,Q,J7\W&|UbUe ,+%~V'@\\ K&%#1[ǯyܮLI"{ިL&v:LԛFD!DV>%([mlPp'V @P.}-ϧne,.Zsh Py0FAF7-+>lï~:$V x7P]!{jضLi¶n?1?i”c0378LF|צ0 ÄiY@-"Bwo1cDemN`042SFtD>)J2D/96A>o 7'?_=OՏ԰_@P^ tןE6$Q^Q3gEso+LRc$ycN acĄ 0,I2m4[u}5BJ)~w&k٬T`gEHѤXGT]ngd^h?3/pd>",νiVkK]uqPFS} !%i2MU{Y"F ؎C-5E~3)3P_ryO 8P 4iWl}Sq.|xݿJ> L`iBVj _PE{( 1=LHRz />;ܓHbZP!~D6ƦYlيX{#, f <0Ƅc;? 4ĎU'Pl@6gӦV=^&@TNS DB"j0R錁L@:CNc&~w+kt~Aulz"Qb">+:ѿcW$#  ( BnmiY:.lB:l>m~ <>lٙN)lnnӡ;m:ܱ2@7)ӡh(:vy!0?mq!1Q8ࠎ~ݭ&Fs պ>o&*('-LNe? dJ5G?< Ӫ^/wpʔvai&ip=~"%1{7/7Hf6m/ۥhw<꓂Q0~8`4|'a0 mO$'Owϝś_ЬOُӭvU VFκpGslU4f:sގz)uR[ ۇi0[²lضXg?k{Z>}{k? w<,"Ȇ؛|RtH+]o~m\ }ҕgQ54[;zRNKx5MB>_5rׯh!75t[6/G ƤEgszFzaHmU bZ`(!p}?/>q0?4ݩϿ?]Td/0F~C' /hz:9W^@aL>fH~vrI˳Ç(ǔXpxWM$l{{@1BŻg?믞ǟTsOWG]78),- :1Ng|?= n˲50?=O6o h2 (B3 0 m#xR5l{ܾS'V -NZ Lj?]믞ٷ>FarW?fk x@b2rOt:\6]߇eYdc!t@Va 4~;f[d˲v(@X]fʃ2a'6-_/y>믜[o\@Q!{]j*:A\!6p?!t)G gQĹCБq3D;,'yidRY4uln}'?hj!ࠉ~?| T*z})BzFjm _c_[?tM"nlWWc֐ ^vQonry qt*l g`XLSU?ڹ"TXcd=i0i}AolֹrlF%d3Xx˧_+o÷8G{ Ԩy>}p ovW6*T>=z=RrVaYnC[kF}RL.I^jD{mSG+#!ûv жM~`lօ(ʥEڼv+X(⛧y!c&xogxKć/†0OH,_:w Y[{ק/܃_9|;<˗nsCo6#vP4{77yzc 0t*l0r}b0~ 렣A4G1S0ee1afo̟ij5ĩ.Ȥs4`7RGA0P@_[yg%(|>FUk'WNZ&n3P.ͣpқݟ 7-ӹ@0 ض=rȐeRD`c85OhVqU+gD>ןn`a˕M9i ]*%\wL->jkxWoveeZm`-lllMl\ĵ[1ۉY@q n=P>bT*<j\? G7&~#pdyV Ӹ$e':7( m&W&0 7PE BeNnY6G.CQ>ɵFM{\K/|^ E<;}ôZŵ۸Mlllb]ɞPkB#W"Jc<TeZ劁p4ҽpNxi$ϛ8/_ >&Y4-˵)ʠiKs<[ZZCsE]5'㓙^~{k,~Yx4ܱSWp2Ju=Tsm}lƍܺ;8LGj^BA\0^B d3',g1C?,=&mSB@ ScL1 HAa6훦/ lYôH H9,ɯh[[ ,NZByb1B1b1b1T(\σQx>= ~vv>^m${Fk ֮dh, 2 6BNeOeepmt1TDPyHh, m!J4=ɡnuuع|>꽡ð,WLi;nI#{qFkN&>i\^D0ugrv3~{8j^P4w~z&≙ PANV! J>sZ<^76?C0 v4IE#~&>1P*-T\zMN9)dPp6PV J?O5cOƇ?Zo204GW6\RS :6\-F&E?+d* +ܼmGXY;&q6;A Ő^l6?z^f2L_<"WP紡iꞇ7" ` ALwPHLv{@OU>_BvF :gXE "$}P{Xo[GN7xV?PȗQ./1{aY6|O."8_l qFxG6QnsDxX GLA>_zpꧽ`r$1GQ/EsF{̨Jy9Ԯ|i"q!ؖt:w?1C? $g0-NūGԈzLN1ú!Lv \?Co•S~RQ|#bA}dܞ8ꝤNJ]:{hw2L2R!0eHnB(__~L~9I8:رzli|&C/ d9;-..`yn޺ _9QoR]LFv7x|L:Jy){0eZH#Lj c}fMS#4$PMho^!T &RNF:-U{eh4vE?BA'VMB]Wl_5R@*|OA:CfL/a"q!ePt$ʱK:&P8˗tQcQ)tCJuB>_AuWU./Z=;_G93;iuqy!qlH^ C y^b "w%A2dXQeX+9ٝ9gf9<$ٷ[0p/1w-- 'tuZ8pr2@͠SO#mkq]׍ // laZ>BʊqNں>\2b/1\__FU .Ξ'×9Hc#\gN'V?ض;K#1?kp\U4 y"apOֶg^xm‹J5#MƲlFCEך_,MoԐ}赒V댮6SMǴyPֹP lb1{C7d Y4J: cC\O?+7mhzS,q׫?9:^bս|,ӆts=y#3kYli9PH['ހA``òl,ŚGsCDO>+pO aݳ1?ư"8FmF~̱pXxcdė1~omb nñ;-sӰtsWL^z Z6js򯒥mkq ; T2e܀aZEiHEpxBhY{ gS_oz{86d3Cgp [yPk@JT^a(Ee8x|@6!Ο|dUlw1a2;eqNX#0؋<߿Ab|`L,;WtM8a۝}Yǭf3S"dE%ɤs[@] g]y~X@z?Zj870V5 zrOl7Mp>it`=F.VЯ>B|o ?)֏ᜣp D\wU~yW'Wi'D?_:'E S.☀Daa6Foqt2$ #\@T( zSjp`]fу.}9`ߟ#WYa1'vHP m:30?jVQ@|¨0) Z77@F,BB1s<gz #b ۅ~33]L0Ɨs Ha0 P~iMx 뮵ڵ憰ÐuapGkC ?Ȋ ykRGضם/7j8Jnaa>&z/嚯+vi\91ln;Wyg[ֺnPMv`j'ZD ܻd vMe7@4/rhQHdW6Wl˂.F޶lt41j dnnom/|>,J ӄmm9,[]`Z^ /4aYNj]} Nve׎ޡ'Z 6` uQ.0X@BUO@"mJ 0B .D`FACC(GXb]y_!IuK?R]WjZ-ʒ@{%8kladUa38vD~tgDn ˹~bƪ_.h K1mynag 7ap>y˰a6ēxh2Gj2F7eĽl>"ZKLIFvR֩y'`/@/P1"͆eJ~EMr Ѫ[γ7/AE&[Ffo@\"pς +1IQ,'I! ݕ=a-qe ǺDƿ$ mbr jz?;/5;~Vkĕf?vf•ضTџa`Պ%9d [|ݷf{{bDO;$Cb(8 %O>I)A-9_6ӼGqݱ}-8cD@LDe4V%c2'jCmEz4r?ibDh虍l]"V+y/ٝ@ 1I[n םeK_݃_5a yҧS"Hw]u7A2I՟drU+ێfLWsi" 4h S/ɦ@yOFׯ>-Sپosp^_k%5\~j'P<H~y&.fõʖl.i `|}1=f$b5LyN$@ z"Y_11i[ \=@U4|LV4,_ߖ jmb_K~ϢjFU c{*  8țZYl[+.IUrhl 31@N vCh00ɰ}2[)` (|TreXI} udO׮^'g9Y+}ؽЦu*\[L&W*3%qa !g<|dr5SޙHbSH;g}IӳO|6l:a2qǂ8"b)\?pm$3C~dž>]fSU6Y{qmcTUje!);t/:$C%13 hnљʉgEFA; 9X!w(id6MR @e}eS >?4|UV3p~FnntFxG{?z|9^UbvV-BE%d_n yR{D Y1nuQ2ҵ2lECeTld8wb8`l /L.c oNf/F*#XR3IX_3TEqVw "灴]d>.ia#UI@U̽H?o|w?x2թaÌ Qi*#[ h^?&7 Sό^\<~[alV} k" BI IZ=뭯/y0W#tXF7Pv"^ $ju[_{[l iRURz\E U/y_|Ǐ>~J i7 F+P}(k='ֱ{R>؜G?8Wfmid67a2<2Cl6RQNj`_/w~qZc 0 1"]7@} C9i GxM"q30ϼ˙n>6lhz[‚留h_5LAw!L8tƛ0,fɅ:;@4FnY-D1%m3M# >/v|o>|8 ֥X:aT%"Ȃ!y7ߓ6wGQie_ͱM콬`+"]rײ#òa{O&iE>Bl~96`m %Bj #/՟{/ӯ^Z <'0_fI?)FNQ3BydmT6 mը-E֍({|nҜUV=2~ k^V>ì5JH}t(XtĪ^&ߝdBK/Rd)s,xaT57 1cϚ}^՟%6}>|V@UP ] )* _W|4-o?W_Uҵ('!LVDdQ4DZH|lL d}@tkѶ?c׿>|s2G@bt֮|ՓmPXrXkQ ?fڧb- (`{7SG>ź,ފ׷^{lZ"OgdJ eF # bEZ]g}K?|7' |H?|0 wp Vs|0/@z ؜C^G_O|WgY4lsV ̪fA 굸 BƟ*q FyJ h\)(r_I/@1/D@?}Q& knxࡤ?f Џ@-xVƖ *O6mad{qrdYc% 2(O%ŧ!9y>K_«9 {7܊n'4()x+ЂO^n=k%h`cgYh`[z}꿿|tw 1K@t ZiBKM[Z C>"&H9_׾" ?[d攙T2K(bb!l<筐4?_zK#66mɷr-:F&K 3nNgU+F=Ch>F= .V..L$1_½/ ׊bSl>g # @1 a&O_O^U gf}[`E Ez{ En+Lceo@6sPnLNLŷ?DsOCپ DREJ2]lN OAnO|FQ퓍>@3_|>CVPT& ōv9Sנo̴%Q4Q[aO_՟NP{ 995ʎw@tYZVx*Af=6u~EL/9E@ ?ٟowLUFP9}?|>+cj"@l^@t J54QWU;s}W>sbŚI( )"Hh@Ç_G??gOf\UC81WD?PXşƁ"^Ԏ`IDATp[}z;wuG[hx N~d{CDz'@~ZY%6X[O[݇$ҪmPK],tq > Ȁ 5!;*G 2<^(4Sh>D̀l(`#;6$ Y ꡁ0B/!xF݄1E HB^ /BL㫚Q!@\c+~uKCb bl!\ ׁ 8l|V}lgy*q0ȸP1؊Z94y 1wW]GA4%Dsa d V1OFY!\Y}#o@~!( ?o&՟4Bt5<.LsMN!w3#Gd{ bLq[jc H@ VN\b@&Wf}k3ҵ Y ŨV\fep" GAF u} ٓ;VjjĬ~'6"NN:i <v=$OAq`hEc" 0K,^|0r;r;7! b.Aϓ@A*cOF'Ȏ CwnB, 5\PEg&ӟ 8 ZD `so 'aҨ#t1tԪ' S׏B,[{ `9Wzm   , \:݇ur.BA:}"4A Bc?^:{!Ă?ݶT$&m3ĀADX9Mo՟u & ܰ>B/AQ/߇0e=ӦUV0,!r`n6"GEAa$} 8 aˌ 1肂 voM"@ @AMA716H :XMCAuO4o[`_Z$<Ve !bb[K_CF/ /?~z]r  9AA!$ !@AG 8B?{Xto.IENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-linuxmint.png000066400000000000000000001425771364015200300301070ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxy$W]=wwd' (ª /+> "< !d! e&$n]UQKro]]5>>UuW;90 0BaaF; 0 aaa6 0 0aـ0 0vaf0 l@`a ; 0 aaa6 0 0aـ0 0vaf0 l@`a ; 0 aaa6 0 0ۀ,BN@6^3 0k H"Owz7Wuj¢0 0%pu!4{0 dm6ɼY-5 ?0 e>ݺ N@0aEDNކCOKX{mߏ  0$tׂStG"~ah🽊< ?`k 0p!t~#PWߏq 0̨X Natx*诩ހ(3 0EA@sw VkaaF  56 W@a^h1*._gO=嗼ެ)Ks%Ӝ1JL0' 4iZi2 09RʎmwNulݱt,Hylqї}ǰ:+Dd"$~0]z/=ҫMsh6gy 0 D/su?ܗ??@/:5uՌ+=~g]u4;}UZ6SaCG=?:x+  #O~~@sl|럼{J4C2aaؖ5?wVa:&?Eg\.o 0 ӛv}؟"\G`x ` ;`5 H߫_hh W]?j!0 0?}oc? \'`nK@/_ٿ{$20@{_˞~{uݨ RaDZ[7]}=_)5^Fz\u2Ho֯HërݯeaB@Dt}$1QiXp[THN@f"ithVk$@$1Q폾3f>tCafH9|O 5HyHk-p:7:; n&67JfyMda30￿_9p)6\'@EDLf }r 33ì, [$ؒa)fWQ20~#G\|7CIp[D/_L#j;ʷq֞{Z<.>cKbB``a JzRS-oAeS>P(҇ >G jPQ:zʩ&~Փ ^xb3? 0 ZVlj@<,o޿yWJ =8pj, 3(p@ZПWM^~A-[8 1 0LO۰gBTlpWw7 '8t_z zo?`pk>[v{ƒġS@}M0 0! 0;;Gyo{_,܁625Wv/V@L^ZmCNgga2@x~Wph4Tk]g/Xv%W,]yXs @}҇5 MՌs.: ]mw&gLda$݄GnC \tƦjq=Bi)>n xSOk#'/z]}iᎃ7o(0 GN܃;G|-/=6af_z >|-,k_h`k59^?ʈ^1uEx{ͻU^)IGCa°ԙ]~r9fLlW*bWPwkXk @gPIC<7uk-8:Fafj}oaD湩O^}kX*gިj:9x^:u`2 0L:uOދ^ZVo.~枋SPj$2j J? joei?sս")q`hKaɃGnC2fy2W @P V*CV{P9i jFy֖4op<Ɵa)<#<Ӽ)kc)ijV#@u[&Oc8: afjű#&wm=9uus*id]i cP5~/jFbדX0 0q1 + ٯ|uN"9nxz/xF[4WWKIMύ?NaaR?n鹱9ژK+YM7jҚӂUf5+q?0 sKB7u:m-wwPa"9w\c<= VW/l9m,H3PY i 0 S($9XUi`4}@! ?(0Dif;daWTkD⨴4=r -ՌEu0 >Si3JiQ0 04AiszD4Vp0 ^8~*M41B& ͳH zI;mgU+2 0Lp5mMci4(A ZT8IaY/ i8h.Ҍ]Ւ 0 HU77km$.şaaz3~ ;OX=?w0 0dmAZ0 ìw:Vj{ @BZaf=0Yr@XXa+:q/$mUشgRg0 0Cik$aab0 l@rPaf@iC[afe @A 0 z` j/bS 0 ì*60 0N0 "j]@EafOwx1af=SPMaـ 0zZ7j`a\5afR.uZKafpi;.0 ìp0 l@`a H]EaaI.w" 0 3\u;< aYTq2=af=3RM[soA 0zU$ܭQ AA=Fu>7A pF/D ag)(&zb{& |g@΁ALOot~P&Hx0Ãd|5x΁u&'T߾ieZT _C0"P: 4@D)0 A& 8,D h^  "V>sw)Qܢ 4::w0a8aXSM;Ǧ0 ܇Ha$89:w0C#W̿@%2%}@@iX- ê0 F΀ߜkk'x] 0+0$M6;Y=orڋ73: ȍ 5hnWWR ru.x!Ӌy;YM,0.%şa6,8j ZnN5z"qkN>SꡤMc-KH'2 sO "nh`MDnףcS2rHG'JZp,Gp%/] r{KE,wZrg ,wܴ"ZE\<݇CCvW7P76PtF`\֤u|{roȂ}REhsL!4*M*́[j/G푣c`l! mv^Ҡ :;LQY@(j$0Zw<gxb$Hc?mo..Xe km 90Z"I?ìwаuz7Ns/{pہpC5:ܘK#!Pz4G ژ8>G"|n<@4/ 03uf7^Wz^rwpہG K.UvǍ/  JHIF NҬS_d@xЄSU#cYؼ;6^Z:Kĵ~=hFe mQp U AdqФj JSuTJ5\}/~GO7~C:W+s[h0 YYo˞.\s[w|'ۇW "Y`5u%p3FgWaӚ37~e7_]$Yh;}?Y"S'4c5#[Rf 0#{.?YcM_|Fw;̊3X]ʜ  4SD<- Da=7x>v?5|(Y+v $mn 8)`9ٻB>̗BvxCh-;0Âa!s,y7s_-znb-_:nkO^ ⵊ0 ì-ӻ/c[{&.FgY,ߤ$܉KണZ@ 03?{/؊-q%pzQD+0 5Bɯs=sZ H^.IK `eϼx+7;*`aM6w`{|X˽;dz]ŧLROlvYלb;dB)呴y Q1Qr˙a ^gm ɉ) 9.vardf|mX>;.P_3LHh7̻_*r`/GAz qQZ(EeеQ Ew.4Epˁ6` I$;o~ڷR|`Y!gB ^aP2*02Jz%^A:Vzy ˊ^~x x4u~Q0+z^xB>npQ]\yQꛁ>a-r~*SWvJ1L7a1,[1؎ڦ-~ǿ5r=}aM )Q88 Ԕ3B u;̆E,},ѷI,NF2&wb̹9sM8MV%)? KSEޱE Pܞ ;L61}¶!ÞjZdoIozR nCp^t·m m[ǁ ܿGw Ls{l8OKs‡+xׇ_ޗ%np`+tS@O'h]Qpb>fu臓YFyA3'8q`mtZKV0ANķoj8s%hӱcA-'C_w=x3*Mq78Av@2xZW`a:u^&v'+_7Z+@D`HU@3uMz4rݿ~KRg ?w0^.w\܊v$;rS %82Rҳ3\1aZtRh "8  u;N7rDjb|"꠵`ay@q㾯}_n)hQ1}ڕ=R {ミzrR: nLf(ytXeBH)ݾԣYW f tM@7/WIp v7rCiK,la2- رx~|W{cՙ>m]3_sL67HfUP) `8EXIZtRf_>6 M@t7PlˁcK!@34M06]Cea2O`wXv?:~z2\ybOC70ٜG!Db,Q`b #ə~)֍Y)ݾԣYW`Nm-ܖN - L615b/cd qCg]+~ L5L6g'""PkIB1$ e<3r}()ݾԣYW ;O͎.P(W oKMXu8ːRGo=ކs.|5fvx,y/|S@딃r39 ,J2$*`CQ5-w_Tc/Ng/E/hG0LZ,˝W}3[0Gq2H|nõKμ <(mȊ]r?@"TN@k1} .orwQR|Q-}G@D_̒$ݲiٮ#`hm`bS ,eHw?r}p+ys~ 㟿Tzq$YJ,H:G{(/ T?(܂84!mǾGʫږz7AbOjP56o6UBJhWPtIars;ϞL I>O8 ͎_}ṗZpH]D(:6U&#rwH_`_Tn~FOC5 50=ӨMCߍົ? Ij!K+6Vri:*X}70}Rsw\fP/EE?#CNW!PhLTPAzc˞Il18wow\:mG ?($-*aS<P=uGE,B8]E_&jNT"y6+~4U;];`jx?{a-IX'C[Y`ZtL !'W!4jXBB!0=3a %|ƿ1HW'_ &,J}Xm,?Y9}ZtX3/8d~}1^A^B[ϜBc @wpl'=K!^pR;K}0 .PX/EE?#C6-U 4&*0J&Mۚ>ixصyqg IDAT]W?Z󎲶OX!)QpբǢ}!,i 5(K hW)&,Wp忽Eg> ;6vHIF-E1_󹌐 eRJzL+8qY1m *XJl),"kqxw4}>}@b+ݾF9ĎZIWνT1:԰I{t-᫷| x/˦i:~ujX8z %5Ej]@JlPh/[8wl]ЇҏqϡR/EYa_}]Z`m[0>r ,NEXff|y_y5얄nu n<@)б(q3Ӻ9J#Eo>>ek׆$bķWgW=dSw@K9Q]euFڋ஀RT(϶3YVvLz XfXr(UtJAyQƖt}o't垶H<*Üf,=ǎ/XfXA74*AzV–=SMço8Ch/,[R9 u0-2gD:;1%2b [T~ dXW{>W/4]>Vf6e[vO,x>| 9Eճ]Q:^WFmFo (*YLB+q~9E,C*nkܵJUތ/7=?Oؔ: HAWQ8tȎE?70Ls&`4ͻg@ xA hqDO. Tk[wqE?Eʫ>!rsz~A5M% s&߾08gxѕ;jD9( ( ^W%yKZ7;j0٠.aXeGʻZDûY!v:}EF uuЀ yo|oc1 }MC xS甥0 >¢_2t] 675-iZi&/ykdW7)%&E:YRlgg/} j4ojM6>{f>_(P=Qoܔ[\(8{FBϗ;1%sL ǝ8;:;X^젵dݲ^ )e hD?aE`_}G=Ű!h `$=媉u?)u# oGۉGw{p,#QǜUI_+n` $ Z:meo9;l[A_Eb>6J^ئM7L; x n, G(  =~~#'wq'O?rRx8\6&Cļ^M?^JuU(9MmہeٰV)F@am _QO=xmwZ} "$* PD+ ^n!CX+ WA~^ea3¢B%a[6:mٶaYlGB*f,;E SGbC&?~[=~!|sS:-E+NZ,pPP`}~ⅆ)7@Xªh~׀Dž‡!%D.S!Be I7Hm7`FTY\3kMIt^ yom?Q?sɭ iu)J1BB΋"yECIK! 2^wdha@@!+׎H\/sXپ5~J웚k`a=X q9.>Le,yN F;gL^3&/zlc9eZIš}h%!x#nk߈hzH^pž#~FJk!vJb:hƾVm__>]P*m%jkp17/U}+n]jKU-1H @RtwƣZ C+cLŘ9M i;ҏ@}<AyKPN;D8k%O #Bњz7sgysm_f8 `D _x3߾{&pcn+@KTS!V`@Y#3=xٷb[b ,oSr%9(G)kY<-*K|Vaۼ߯}BWg[N_&ڇ}4x P яS1k[=g|཭pX*XQWp$0#咹ᒙh nadt|P SGR얍}B0вY VU>sH؇b~> % ,6 |PM":E=^bK.!.M)J@-PHhC}k$I v3v:,o~H{[ڍTǝ}'j&Jp)f{/|;s̊6S'J $Qyyu?dѻWл% Aˀ*f/},4M(i劆RUˬ`z|}@vZ7xO 0ycB\4|,jC#̗LJ-Ժc2oARp\>|DJwdh`},=+Wn=m,w{^g<ELPM$-gONͬA$~`&ga4 ?!>`/[#F ^L>}UoW#imkb>t' %?z\-x P)ՂbpR6|+@FSSȽ/N%[V7tER](޼?=@"s]Rb(Iܥ v/`f1ŔuD%?  0 m6ia\@dr<랳6яZn_-R W!e߾2509uZQD.}6n>E]BBx $yG(Ծ]!"N\6@Eiqp{д*JzHG;qm'6gOjAjQtƲ}K 8=zk/倕rahQQ3Q5&PҪj9 XOzSfđ~v-ZCO[kqdwXj ? .<ƶl't|/펄Y!:)V6h#cFy3?K'P+'49"گb`J8U _1a'4¯y7kN 3Y-ڹ=DQ. 4i4Pvqp<ھG>@sي>l)_mՋ0^VBT&*z|$_~C֝hc/,E?~Ya[Bc:ws.9v`VV:bj`@lNPEOMi$~*9 M,lˁa1A\St>X3H"ҔQ%"[%ZAi6K8 "ʆ>[z6WBYcO i^[]8ؾ;-% W!uڂKͩ5ٯB 0[; D Yn/^h.Cqǃ9q!?hz /m JORBh UT$M?N-ʡVgbC{ɂ1*ދz1' ڼO$ 0!ZyGQk)n_̴x D{e,Dǡ#I>6sq 0gP02v4.Ǝ8y }!im%g _]'aM存(oŹ]ǿ{K%VˆQҰL'pȏ{^^kn&h՘2@:6|R.g6h *kmIX@e')sE 88'+/ /t/$0tsH¶݉Vn~:`tMg6f^8ة= (Cz|Q۞Y*k}Ww;8n GQ{ S,vN\OC"5X&n(G 8ʶHy?0 -L}!$!  Vjo͟Ɩysl9pˡK|iی/@Eofk:. 8׺ ":tk9  HPk˔_5qOwlR.M e0D6抋m92*T & wkd0VD! F["W2Cs!xKGB:2n?5NmcgϽp#ax{.Y4MkϚz;B9;WΒ2bt5p^d\-Yx :B# H=_yb1j@1iA ɒ1]3iq@H1BB 3yeEK&p{5\({lGF$\'h/E\20*S+㩛{*%,6:;q;0Qޚq>ǕG%ӌr1aC7^W" #se(nM[N@T&e9њou8>`U'՜9Zt֤۱9|Fdppr oV3I"E?M8cpN\8<\%--"8-t)-(鵕3*Yp;1v׊@>Np(#w.W撳bqD~| ٘cJIQID3#,˩^rPQ;w_] U`&B#QjMoLe*6v5/37b:ΫO{U&GAISa!^;~ᢽWDK[8js:}oAx 1>}y|džgh̭P>$n{=ĒXpbVv-v[{DDby"sMo[,\!JC|_q1iFU86O 53A|IuiЂTQѻk>m-;Q2Xit3Ewl('/Cn@mY1o3" @xD' o{'((\ a'@Y*!)J H CKu)  WXS7']C缩gc:N}F)7ꆀ\7q BMgwitȏE]Lq!I^)D 8!K7_(஍+JLg b[K@}Dq Kx_YW޹t0Wy?ۓHu \6ѻ;FޫqL!7&AP` )8?0;!8݇)b)uwn9ѽôrOJB{Om}=jĪ=Wmis,& LNL?pxBkf m7%^J' L)14)`aѧїU/5F.rrR~ ~™{誐t+f_M=C3&{Lw*}Wq}I0ZCC(@]'U o<u|:ePE~(>|{ A MH:{ E@$™jYڄgx3П4EoR 8;N%8O=(fl #{ִ4wFdҖԼ3TtTpzS*>?.cE4рԺ!͐U^E[g iYhhxKV{CLR?M2ͲbdHa~`w$e¹xre3iRʅ$z;E ,C x+@VpedBݚ"|kPZwgw6S̅9<2s-ےh/x31MGh-YPikm﯌t'HUISAmn0{2к5R`(k),UM}&ҧAWw5'·٦֗@du' .j{1!ZLZoXm;Q~ n 1mHkWqi܍H'Z#ETk PuR *0$|xcwPz =pM?Vǯ]8“f_:;cySD ekjmn(8e TGzüZ@i?~#%O7̲\rV x^$CՅsP]5o43 aP-Sa Qh^L÷@btMrH+,%kOY:o/ѱs[xGJzO <_D29;na߿by۽SGpj9v/|B`xĮMJK7_=!mzTo . @KG2p4L b# dfoXJ򱗪:˒(PP HD3dC}~)ǻN@hRi @gI`ēuS؎o9|/rڑG{ [a˸"ԹC/F_8]A75 8c`b"?Ǯ{/r)^p0آȐָ^EkyR?k0.իDFL\L dVH[T`IPHL[02`>4"B%Dn4i֎7]' @w6 YCtZ'Oc<|nE^BmZ{̎-8ҩ6ZXN ߺsmxiz"o/)(U T%Ԛ%*ꢌhaTK-Κ$35cg `w%~85ϤkZ') SvL(=:NoRlCB~4@C/"<i,ks 2bĐG_@K&,m? >ߘ`YӘ5dzhxXrhdư)4&*>vw3c7| ]r&6ՔܢK&19[ %|Rs W4ߢ' WbR*-"&J$õdu]|.)`wm0+LRRUJ7~M?"@ZW >3ܗ>߃/:7ցP V ɠA 7|t7IZ'ߏS(U lkz.`:`$ _05lkbl#Q|M[?~rEM+pW!8 !06]Acp/hVpfbEЅcu#*u~yKPF++H[aL3 +@C=EMQ:xBߴx{!=0msnC Yu4%_lّ D?[ :v4 @ a4+شT6`VPpLHi&REǖ=8PD[݋oږ !6S|[Ba%6ͻ>d3[A2u326< M(ݾuB06Ul&ǟY1wx aZxfYGm _*@c;>tǞ`G N~WߞWsdPQgkzfZ7(;D74c('l' ֧OC7ѯ/ukՀ@٣&8J23|Nv@f_DQ\2fIG}Y6Xf=5@aL%oG ؼs jc%]U*R7)Bnbjǎ\}lo\A7P&h]vP&cҾf4@M%.`}GExn{RE \݌.X2TŲ+@UH E8u 03oYH̶rMG{@n\3iȰ;m8p{flscX3zoWguArcdu Z$4AJtd9Qt Gث(b?P[y G>H-9t؏~D߮}x7нÐ/r qϩhcC'[ZoajMs R,t%oApkSu`KkskP+R3]3}u 'Xj?~;ΚF{ٍ ~^̲#`[MGn[.]-$X{߇tWG?e2?v"?N,[D?[|"&z_3Rݚ~bttk4=&Yh*C?1rnew. 8]}_/`bSX(MM \'I(?_$+61Sݏ| Z ۗ^_&?mNU2>Z 6Ww- ߳#)ZחNfeSZRHOU_'LC^bB)?ѡpnM: >ŧ[Ct2 9iK Dn}#o4kYx)ƯFICEuDjIT+jpl庉G.R\76K h-v ` X̣Vjnx8LyׅBO$+bC1(9Ti/ލ )"*qFݢ*Z?QL`z>Q0k`vy΅rhK_h^dr"+r'Hc=e=x~>?^3Pjw=F/uWm5K8tt(aUy{,PS )(n$`z1ڢ;ep`(.2@ +"Phޗ)АLjKԌG_ 8jR c.frOTЬ8\pF"K1^7mm<݀(iRu@?◮^1iFf鈜sTwD{wAjI>ctW:Â_xArI#}8ґI(uHRfvpmw¨)Spbr `̼URRO^^685KԢ a?3b"$%k®DcqlD&*Q{"kS& CuG/ă(.3-lpRB4t$4s⅋tZh i8M7g/|& 3?f.+()ާP IQ'MPx٭{B縭"RD8sdB0Q̠{;1}qG۞9Mq Kw !PUo]M94 B3}$Iy\aY O%Ђ#~q'Eմb> M)kXsۮ>$3@5"N\A}B%z@؉E=la n9xau$',_V/ᔴa'M~k\~,;LA n}PȤq*{~>^=Db$A:r™ Ab5_c^Đ yAKU#x3ǝ9^%V}X#h*Eɟ6lthWg}xAMy"\oMW HysqÃ%3ڵ-ȽzAiFzRya~k&ݦ~Xy1qu;J6$LHOԐP8 !B3(k[Ĩ1}R}D@ZKp6פmݏ{rJY>szJpVf`Q1w~;Q1O&HN;g"f+ (}T4BT[?68 N<&*>LJ[5gߗ4tjq(26JJ5˚(e2}Vʉ\UeUk#k#w)(8ēhT=z=LT t[$ ԴQ? eG)Q~nj{?K )"EVvY/8\ϧZFM+z_0 ̌˧EFVQ9XZqD! "H+E<{yD&P7=Dxp+[փvO=2P8_">a& EDiTZ Hd1Ӟ䥐D-AQ;>8F]}}'H[#I~Rw gwEdiE.iWn$w;{& ) HߓUFEdP&EZ IIkIH 7_lB|QsZF&tQƏאN5j);(Ys>H@ b| PR#hAs400Qn!H&B{K^ ԝ&d(BDMA(\}׋^Eѧ"^p@FI@ 0&IBtt7S5a:|ߩSYwjk荒k9qd> zjU), Kp{<Jj8ISh-kgоgP(5|Up\ v cۤJu΄`vDd(|&'a2Hn5'}}m?B.8pmr / x80ҡ;">ȶl7_/Z |~7 HT>T=mo n(l -5$(괈ưG.@*n7htҴp"L; Xכ;ʣwA5-!&@Nz1#7o5>c_L>]@.A=h3h\ƒVITy~U:,gnkJ4HPvx)#I*5iuHlJ :H_+mVrf }EKyme3D$Wq5u}z;Ô *|LaBkDAV|ӌi $bxq\fy$Q\K3#o_qYZaagnb=iL*>7Bݨw ?kRv[eGdve04Bdx73һ4Efʂ&ęB#ʮ}͠}Ϡ 0,dhlk$Ci`'i"SNjS.9s!^Y[LDS>bQl b6lMz&RW@-M3kG4vq%fg/5 _=oz@w#OfPC9NF0<qJY ^ .]ܠLykqB@P9;ҟQ@]_i=km +m!0˴lm{3ݝ?^>t{_8La+HYmUlL힀r&8WlƖQ9~x{ YPilw4IV5D N`܅ mF\&?^w U,wvvYS: ߷ S׹.2gPu3wMAc8@`PW`]UהHs!AǜW6q% ܌gL7 LLks,tH<?;ꬶl/UwUH d(aW0MLd/l)jp,eT=_Y4pmSYWOD2XKK3WX/ء T᎗늋At Ͻx0[ax]rR\0O[<`rfҧmS΍{_0WdA_|+0?4ш.R0sIi)u*5n @~EErug?ō\PFV:5v۾%=ĔWy㡍Ϣq@>.Yd$sg]қ r/cYg 䧴v12̚X,q6v@r9XǘY[oFI=Ss b X&CQƽ+]9AlisVDm;35-c%Fy*AF?AJX[0eu )?PiƋ@M#`bє8Seڏ MF)`θ:j8PgtT1sgR+M͌I.1R;+bdnc?&S40BjAp̫O4ʁ9鯜s׆JG7Ju2 c(/F#c 'cx }Ҧ #e[r] @DPtPOMy_5"Ě3(W$t?OP(,;]"O*b6 {Oݳ<wsv\Kvi8mB4Py @C.]LG]syC&/ Yj@<̤L2$^D'jMHt6K%2YhK4}¯04@BSalNH"%JezordX+h19,:tӖ\OMJ p4@1M3~fuehsi h(ekxT,j>|Ȱgz95#lDIV2*̮og- ny˻9`(zCG*ԏِ~wV:V6r G;q}o2f _=[PʎT,9S C'=_@fFM740]7Sܨ{6wӃ0Rʱ+SԴ,wa&;7E.oˤjDNn,pdIZ ^WP8+`FTWPϔvHe"ݲoBuZEIg>DLN֋MД IDAT^~} Ӷk`h߳h^h_fk&\! s7HuKY*;bn^ ;Lj8=ŜE"+RWl\ESlOx,NFMuhi^`–+PigI1+YT{jw:f_`̓w3+j(rCd|3֟1ΑTb B]H?Cgv/+6v @4LxՓ;ߔ}ZY .t۽o.˚>i (r+zI)YS n4zϩ]̞VB}xMD/nky 'uMq@>?Vc_RH +˟[uv]M?}C7{68w\2@o:a6w_NNgRyRp@rʳjkAۊd=\1P\׸qgeVM0Y& Mh V!uQiȍߜ)pRl*(^((4R%R*YY>7UU_h)5Z฀s$K $h Ӕu:70'+:a*\P;ۨ2fP4~~3tm{-Pm0Če3Vir۲w"'H2w_`LŬɲ:~Ro(bX":H,7{>-PmNެI>׉_3 rj6csb]T7N\3 7KSt8 Ԅd:u"$%@gM'O0wSŜ6 _ϭbl`eOgSB CU]X&0}xgMN8G?+yj@YRIH3-h+@ <yLUkn$yY~i ($م2'p!;Ll8e-+tQF_8Oҟ4M:4q'֡> +ԾդNp{(@R@"LGQ\» n(WO\>?H/ym&Ƞ ɝa0FbSXs f1t *=ޭٳA=Q9 e+f~|eFpT Vq<=3HGm%%:0{/7M_㦝f hf=Ō`S  46v*gki;pf֥V'Zw/;cug-HqJۊPl4| ; =.k춑!պ/P@B&( -Oǐo0ά.ҟ1y\wnzx@ 'T[>c <7DN  wn?q`YUpEC _;es*JS0_kfET'êp]}ּ0Oǡ &yRw4A}nX `9)'$(fe3K?ў&aɬzg[(SPWw5O-γ}_pA."`r2FLeRI[XtNNYF\ ~ 8\6J8Ҧ𛨤ݧ4 /Y:ͷ~h!&B-ڸ@vHguOAgYa2lҷjӟj(Lia>˳6j`+y2F<;'A=s>-l9BjRFfeX2z'aWz6Jlbnڡ]&chXTӧ!x%R]C\hȆ@v>(liDRDip[i5sec5ff Ss'ϴLBA`+r4|XEX>W GGWo~d<ñ;rƎ7>׭ &AB @Dbs9->X.3ܸPF+WY=9)F,'9YGzcs93 Z$YB_Xot% o;N0)Ϯ!Yx8J(ldSc7I0l\X@6떳e1aegJ PaMGAn?އDySD ޅy4r-klKD!` #*nWu.G{F+kAPIHO[8$ Bl'%6_r,f8 &o c [酈cIg< HJVS^(Hh W Iߺ]!nflATD0-{=QTa oS'}bqgN ?3zFY7&k|ظ ۸W@獂Yr2҇ٔ9(K>γQ^#S %CP! h9Y*j+As!hZz0l%Z9(Pw#t!8k?`S(~ƭa9 @qMpߑ~Tg5unvYkMָՑvJ`ʹ.c`ZA>XKioeM (ӥ v B*C܅OՂP-Jgt$/j(,<;hiT,U:qC.F_? ͔Dq@=TDBwP0vDy ?.er~+e7AІe![&x0f9ơȵAr2V*{Q,;<oZ`lo(;WOM]}n[)}+Q(i{ N̻}w2[j{,}ʶȏG)W <6 cc#O<S1*1n0Cү l~RhX]|e=v!YdMГh\,ʌ=<. ޷9vlɰZ~ܦfdz[SL`]0cիl# 2n2KVJ%a HXSpoUG{f`r@[9mR^sw>Ҷav VWDQx*n(El*&(yT?l|'´Kv4d+N6esyo+&en }J3a9] p?e?Km8l!3 ~AWNµcyًKHݻAϊKTuz&7;08.[dǒJR$+_C lC嶥5Ӕ%0N8ʗ)}!_2$$Byv[bڤ'{4>Mӏw>ݚۯ86}@'fpeLnY?۹dnēSaM[9:Lfŵ]5Miq^G@(KzhPb+="0LV棱ufH:T0n硟i+lf=vr uPz9o\y6rw@Ȉ#4&v`pǚ޾ |]t% PP-rIu̇L;#(LNE !& LN;Vgr^ hSh0c>!~g ~sSB+Dž{8 :``-Gn闗yOqsF(n <#=yZ:KbFU"F!8lgِ$`F $[%oG4yQYi 1kq!QPZk[oP>y_045嫜hsCc`&2eZVqh;gJumk#v``y!=@,\C32鑾yTsG:nm h\g(ԫ5x^0b r¼@CyzG>Xn:}S]J XLfXݜ{?79˗-v{ Xg9:G>V ,HnIN lP!Q{H@@md\qd]мFhVuvO,\i&]ˡ$7}ۯ],a:MݫckYzlK 0%;KmHYyr$d _<0ɆEb]x bNbD}ﵭb}+}fG2ݺh!թ?}Ŝ0$Z {HuA?ovjGW 4O*:ƧfMV;gaV;@58}Cv'WW>N 0ZS'*dȩzs:o,):) 闔 P6O%e9RLeτ+ 3Go,N06pݷLW {Z4$퉑é?]T>M{΂RA)N^M>޼2`Hl*:('FXx4bx} #(3?mݐf4|6_&;wt;bM|WȘnaqnkSd|ZdQWf=Or6h\NW8v8]jW٫k-mnGAaCSc*̎t53 [IFlGW>Yp,mxd<zGgz;B;Biz0A)[cQ${v-z|roa@h^4;߀?,rx^W.lwԎ~!-&e놲ZOmN_Gx2@x5 "y KԬ.]--gN .!LbIi'+CZ/אLvڏ;o>L\,C.?ڹXOٿzI>v}\7 @m3ߘ`-ۿ:}!'2#l3%Cup2ʾezj8zC)-'!K>W6uҷh^NH57%+)%VT)_~T}?|Jq0A2LNBI67VA6rJdz$iFPFS<-!"C5({M7cEivd 1NqޞGc0)ɍeR2[ee.u2-I| eߒgٖT9h "dI<\zNK>^jap]鷂8$$P˷Uҗ޴mh5E_x<,ه0RJ pY cNPm Ov bDw4ygiRcIV>qy,첌Z?>rj:7i~Ip8jN hmkŔ$v\׸*Es)f1m?!0TWnqt}R5/^xw_ne2\Yp%U*ܳ쯜̬|e;p]>\Qx,FgUguH`v3*?&"$xc**? [ؿb_SB }\}F 'pOK9q UPEB79yesr \y o&8 HWi3 3ϗ͓yMx'?{萑ա7+aؖj]ǒW$z8&~*b[-b,g僮TZ&_L<y2=F>^P,EmCR|>ګ9ɖ$xfDŽx3y.r0zK 0tEʷ=ҟ޴h#5:P%; x\|ed`2򯲮XY? P[UH4I cێt*m% z!S>>8c{?;7ȗ.ko˷qzKV . $Tl]垎~Y&AyŨ[۟yẦ1k@b]pӅƝw}eʒy4e$1GwKZ kh ((F}2 yOΌ40% _?z&4>h4[8:q=\yb!mǑHK)qzfpHOD۰䳹OSF.~ @|,sfK:vxƁKjgSß0X6ŵno``lgIg^^/ÍBi: >uոGM]oxqMF0-/:̳ƩK)0RN/Dw~Ok?Ӯ <җ;R>~n> r'$/ TԦ|UE[P!~@X4k1V:+?W(C>'gngLz<%\)-Qϱ ݈>JN/(ӆobX>S{/+=(g$ iB @E Te]0XsFjwl|[%^t ֳMD"tz",⶯~Q\ȧMFAA==x }8q?6cALWY=}; 2=b]Qtܶ>t1::aaiMyp籃8aK tNc`{ϧ Ln[p @t\Rz |X[_B.V|[1Ĉz4k)Zf%kZ *P MBXX]O<+ȕAAa˽=81)J|hᩦ Ul?cqwa7 Ax8i8 f6r]|s ;X[_> = gUk>4K1a,;S7`ڱpa,'Wu[g~}Y>@P.iO܎;/P,sZQy.|5H,vw򋞎8I =JF =<W #,y#+uTO 2VO7t N/`f̖Cv8j=7z]f&|3ch~mUע Zvl ɨ$,bc7pj:'La<{;65,H˝~>L$ -h#5J>N@P~SZ<1܈',E&q;̘^QH-)B YW$1&ADwceMvA:GH_b"LxŠ{K0܌]˽@빫i_ 1A®:[?GӮ@0>w#_JU!!B֣y@U|[(͇} ǜ$ 7_SvDq̮[0Ke~݅r}tz!>rUKQ-]@x[ \nS_4cXQsh^ vy@w)ͤV/uY t1U>z|j@c @ 缅"PjyӤO и*EsQ@' :PDb//8%\R\|2 7o:;ajz?dteCw)-Ӱ P H_+J`f8~ڿ! u534Ju ̝i _bHe˪y@g!շw_sP*H^-znm=Bbh_KzӏuDYI;A͸A6OaC4U.ݩvg.ԣH˝I1]=d4\si&?l:<`} r܍vDU29ɻ  zK!t1 ?@,S[?v$I]W~-E-%IF^ԩ1? 3XN? ko⧦*g?X`$ݼczGĹyP])Bo!],be_ { ݸ.VuݥCd_2PGwG\0*OW@ hϽ'1܌;;斻0M%ZW${Elf@,bC;>)O{?8tB-`qOzK}uH_Җ\׸q6 <|H)(f@w1SǗoT x!G6O_AdQbPl]EDQ'@H "Goû]R9"%1\׸ :QW!]4W *]-I>o; K_1Ĵ0 !`Ȱ`y.DX8|"|^eg4}/ V[ED4/Н%/:J`L>p{q۝7OUFgW}}tw@"VMc@!2,P,Ji_%8xiW]p_>6o=|3!(r.ODh1|8/ ;K"SB~߾a" Hl :q_ij~[ i+aCuh[֗ z5vVSpI|3>F|MH wv/F0h72*bfEp:'قz+))߅}R!~䩿H-C J7ﱻ웦9 aD:^_{ˋڛ>iT_CuN\sÿo? F ga>NPfPH_pF[zwJv BCŽB($ybD8X{{iߖ.h76؝k_-?8DwuwF!:+Ž'1Hb9ç(fȼ @aU 5%$rH=uS˯S_{,[ JI);+ך%g}cYo?Y4&5&M^x3K>n,凶JϨ_ ;./ZĮ{,f|s 1ja`s=@>4`"/@(lJp]xyߊ~ Qj!ýϸYV^KFhRH JaxHHe*tz]H](N'@Ӕ,&ByTQn\Uo>ov+dCnqaѼI{&JC=2YHF a'@l-_PP~*uIP*XBB* v; "Z 0X_ y:uG?VٿߞF%A@!tq $%Xy Ј)/~ȹQkD|HoiQfqń/Y"ī`)rbFNJNK>(uϋsu,<4!P Pv{>v%w3Y+9I\qѳ1X7U{PkmAJV:o-_.!mbaWfcQ7% LA\ب#GN%uW\t-.~d T\'x2ҖmHyRjL?T*(}F~BR:-ӷ. c11I ( {'0> ]S9~ 0pހwCZe|/qt'9?bRfyr-۲4pRA!+eb׵#Υ=ʊf򳯫–ű2g;e3coIgUҾX|D'B"\%1 !{ft\X"9`?F6=➲z_X {³M֥UUwndXҶ|(05k4mLNfh}'iE_Yo8g`ir"19XDVVق6LǿAk0bχx7-%()# "i/p+fGiweK vBB6;wk",ˑ`3ZY RWwHծ̩Ӯ!uunIŭfv&dr[,;f!+|9Cƈo.m>upC_;*#ыO#Rmٿ%h|HsI߾ǵMR(`>'_y',R8yHoWƤ8i}2eH⭹%.1g[Zl^bG0>p3'@H?N9P@x|/󟍁=`лx-٭&z鑖F4ܓUHF)[P ae t#ڤ$}nZ #_qMﳔS'0 2.9ڷJIŰ@NK"~?x@Z|^%}?PPKҶ`~3Q*N3g\Oo`Ѭh*)\'}̼qi=uHr}vX*%*鴒 vlBP"S2(UpX< {g9I|~62!Dڳ綠C E岠iʧimg7KZ栕[BqVx9! J"s8Cf~F:vAS g\n& xv(BE]]ef͗|?^ vQEn6;Kb}zSA"^3(Jn\@e^ܽV R:oհ mAIfU1ɕBp`smv @UDy & J׻1x;x3ߌ t}&ms|TT2@Ape$}@ܵ-)x6HV`̋;/$+zxZ+ T B&ٔKBr 66K7j!c%i49bLY>/ VdhM'}qʶDJ5`YWbZ<~X@0các=k@ai~?8q aW? W>v,"N*%FGﰺ;*ux$-7Rg`#(AIv3w(5 M>@W\bsk%r֑/x^}?kIz<! P [@=zp΃x W'x߆_|_K!T]RH| /2@xҷ zz Q͂6R5 `-,A斴ZNz"xSI*2MP6p@);1n=Bw"nuqp*N}+/T~*!S2 +5JU"@ &-]%ϕ.?'SCNb3r.nIA|Z]&Ui2$ hWH>{[ov3\9:C*s/i.x$/=?.9hYrr*ǤK6&H4yTĺ$W##W;2%qoP+{IK|5 qʪo<$lߐ@ߎ44V@(DY {>~cnW.Uxߌ /Jcu~q wHJW6xxඥk̵-88RN"w㍿7X/N.[bw| q#^{OމO}GWL)O9!w%{c()Sy/iNfȃmMYYʸmOFf]Pܦ槗]/۶ț{y2VV ; Q/+h mh3*߆ABϿA備7PX9W_/}W.sgx'~?7>X-{]t$גvityHZLsr3jg0v`Q'eO k +T2em!73dX A.U̕dGL A@N߾ܢ$A] q<VΎpޡaKů|?zLx?R֥G; Yy1\s }/z^K(WBuHTr8̛!X%YWWi+7m]Jre!ICCk1E@0m+y 7ނ: {sl\$8zv<~ UlW}66uP?x;oV !*]v4mtbZi+J ?/l9G{g| dE v_6NA`n-RZT!E73O\s`qOX?i^+wȐ=8oE G ,|z;3@JdQOvX<˵D!7w'!g*<6DK*_, ayt40[/rV"̇~U=޵ `灍i-QM G P ,=aTt:Zz7y nn@ jG D P . Tt`2N|ǯ ~͟ݲ@&/,k+`vJ] ̓VBu(`,~Hxgz/ǷV@ 8s03l@[X>°zRWg?8@(_B]!g_YP Ԍ}3XuUѼ hT :KJ+ +t_?x~/~pm_H|@ LV(.> :X=T\[ s?Bi>O_~~WhIڀV(va' *D]l'C+:Xos'1+rv͓TA #.@6n ŽBPvBg!n B:nKCo|KWK|nҪA;2k hb i@@o%NYay_M@D߃{_}Ʈ@ \Ghf$HQb yQE )4Hmޏ;N\se1!@s"kf@"Ә%0GSpQ . IT@w1ڃ1`koc8g߹g/£/'vxZ x[ W!!ߢ@Hc +a`va]t*[M=-&Hu)ڡhhG8` |!t@W0R K":Aw1~|xݟK|w`m}u]@P/mD- I!PBo9D< l% ϰ7B~"jR96uq7>OixW 5^e.W^\ڲR8qA]1HT #ÍjbB='clN0j൵UG߃{g{oS. O*\ЇWg xp=s+u*zsĂ#H%a0 YaWaN#Pa  6W 7iC?}]s>|UxW˾ {H@uXJG}\9M4p N¸C MBO %N0X' +0B1)I<}[ÿ-/|$.G<XZ\;_@ (6% PG)zHBe$Ta8x@#~u vL$S k;bWbX`ZCQ'71ÃP!XP,ބ)Nd"rD - *.ÝsXSg4aᨉ}YBQ)Ka.(`!u&FV PJilHo(8aRe>(\q-="*Hߓ@xO;Wr20#K<""&@mYo,?#-U T@ʓx 0>40WA~B@ ReM@B(ҕ<#kG'@["! QF'q BHeXE0 w0 J# ЕϿ*i{ % ]2`@GՆ_HmN y'.xr'T3t@[u"uB 4 [+@>Z@8| w@%τh_4 B [b TCMq@ h+T&ޟܯI"Qm8wWd`GBeq`Ryu@ Z(i6okgD10$'Ls^o_~}c" DDAdаB~~;z1DoPEǩ>NZn O[X|/>/VAP@nDa8}.ǩp2IV S qቪƒ B*@ ha"v61=M³c{6(Ds'V0ʝ#OoڽJ0@ x@  L5Ə<}OqdΧ6ؽhIgp)?xhI1 r}7[ a֮Yp!pkpu @'@ h auڒop8qWvxIrU?ɂA*HD.4x$&:x+Lvr@0MDaQ;k57gsܘxnbx,&UB z~ѓ4 Do@ jGD? };?y4;gHs+cc" cGbmf{Gq_UdƱ1 C.Jϐ||6R|R '"` ex%ÙYfO^evw.}z/ӧHw/;-C8**%*haLRLDR%mNwnEO,>tЉ|tf4@2CAr.J6p->fȷ$ZTΥ[?[ (dDS$]@K{{vVD>Si\JJDLRT 009J2JzuzuJw)W={!>.IIГBs "z *+}-D 6>mpRmnw@韓({6 Bw[P@( 9:і,s$;7xl}/_[WAq=n3ۆIGstj!3`P^ciMfdh6RLEm:|O||M\8gY͠o _)6U'Bߖ r,Ggyw`WmIBIMź uh.lj}zGw6Bk'$cr2>ͤ;Y-fR܅uN*Iadhi>Z.Jl<ޣ;ѐ'OޑmKg }~go|~q?UDbn cVrLHr8q'F4OnHx,eA*15^{Na{*#>-I!v)@i z`Sc'xH"3'?ܼirud.cڄ)6]⿌M3Y{v,鼉N׹{a=FODoH  _\ 'ZB5q&ùoek˿_R uAFTWi}_ey└jsZB6m쥾FӬey&fsbB6W~,0[9)@O$ |O2>Գ< 5Vc( dzϿn~L6~N{?5qzH!)<ȳ snݡX^WW@]9V~ Woқksy T ,!*ۓ$]v0 :`6g(6Ok38\n#M`=;t W VЦ{a Bf%νoԖ0^S)03YEaǤl`L1td`6.L ]lv[cwUGtGv͏Õ\ 2Ңe )_ Dִzλn\rurߝ0 0j "9y؜9ıgY2ttpzwⵥRɕ2 62?o#IZ`qZC{E 1iPFZ⚣OE:ȀڷHLٹD${0 (G&i5,IDATDMEr>]vMd9W% q( +fu$ CF0 c86d_f#%*{ 4?snp(<| Ir bj\BF8`atV}/`!EC :Gc{E b*RfJ?0 c YV{[$ Nbȟ4s֒JHh>- _E%a`qfH?-it5h6/RǑLM@~av8P?ѱG b*[hc0 HC6ϒBF r5GEmʁ".+d/!y?0 #+qB`w '"i!_l~0uot']B~2< H6'Z0 l8ǼO4(Bd>ϽYLg \%!{Z$Z0 0fIi>#o3ι %$Ͽ)a4!][d Cс:T8JY,~{>5 0tgq>:7m(_qY &G"}!Jc_&_2".-`a!ZH2.I h"a;q'F iCĉ~s>0 8wIL>/:C?*{GTL ȲhO%:Za"B"=3Ә\6οD La@ gq]cS/# e/!9uD D?a,oF[=,s0~wQ*Eb0 cx~|Y8ذ"a.җE~p0 +bsUD8Dcag? x6  /b"!%aI _hsj@L4Pf3 0Iߜd>p Q`  0 c5-/Z E[7 0#9o~FhD 6 0lx=aaB8aq1`agaq1`agIOFIENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-opensuse.png000066400000000000000000002030021364015200300276760ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxyFyk޵c1lli!$9$o9$s&cs``l|=g瞞nz(I[3|T*U=XGPI!Z ;!OA204&!H!:#>/ A±`X`Ӗ#:/;/#S^AnL^-_I!|]sg` $AD0T &`i>q_CAA ;mt47(YآZ- q /;kos|.*ͭdL_U\z EcV0i3Q+^9Z-_~&/" ؆(_!ꏅx;ɀA:w_\7MQTZ0 sffw=rϯ1n T`^Bk6654m7Y]l2`KzG_#tDD XX7ѣ?|{8݁cpÂhV@"O 8@ʵi6닩D`H }) 2{nW' g(k{r` EQ2GnJL @?ޏ7:ckpD @&k+ |`v8 ̢CiP(R@|?}bl&D 2dM4wO=}0 a{DԎU=@~ {E皪u=AdDbV?C}g~Hp ఍DՄn= `9夓NəF@ey]iX~>q,pΟxϿ$,7  PnQLn5sw~d co&}DzHUiel|gعk ah1|V> Q-aWZd ړlx ?W AVBVBFwQرc?{)}[ݦlo(Kd l@`%r~/J{tccUtp"C y ׂδ[G^EvHu>Sd&e6̀X j_ĊpYP,z핗spwQ{r`b3)0n-O-n^gWn\iU[s8Rz 3#AK4 ? >Aj`nu@^ ,cfO~;l!^,{%"6ҟH2#wv^E~ c8&੹aŵ}2ROi"QUО;K:.+0ҽYE <;o\ pwPk@ewݮW$-/ "SA<6LOD*|ɨmIL!Hj8X,88&0>wǾ~K6`k\Z82/ݑ}s X1ҟ99!ǑG1VɒQ?5RbWOnDO5X`ߠdb]NȤr׮h~;@gh_FTq vwwNM{Ʊ@mMN B2sՑ>7vyKL/;=a d`KߙPc<7]욀x 8HS-s?{ywۉǽd&=Nq`XkDcI9IIQ <1<9{L? vq/~FlڒMp_7k})"8^9C6ܒWܒflN9þ_djܭ#W7EbNI"o^.ƶŃ|mo ]òl;PCfDpLr )[sG`!@ L[>Z+Fo@aڻ=e5u>7hL`!l=ճȺ5Z: G9mS:Xy/&GM0oWH "ē  ,zSp-H$p?+8+S82hƁ˿s=Pw'b Ek|a92(攇qRO[r`јaUQ[C=}݃X0`/\OlmIx] cqY0*wwE_Ȭ1 됓ƈ>Z $bf[iҾ]C \A r7@ \oS月g'U7Aqɥɕn>"h]G'0gLq su嚧nqC5>ؕInNÛCiZ"IR/aE.rVF~=ϽRKR{h%¸5{_ܾBid@ ܭ!ixbtE%?I&# &rc]xNOttihw=N_ʀ8~ V,Do.^htH; ieCO,=%r@T4F6mw[a7 Ip ub {lxZ$Q'1AQ,=O\1bp@Ӻj޵.5-hz>"+'=t@jg?.9V< |8A=#kz1!.`w5M^&;z\0PEKYnyRdlb#vw^T Zw38–yU…{5<%aď sirx nh &'n &C%[Г@,GBsnQ60 b@' <MZ8GrوǠ #@N>=Z.q̄0@{Vkؚ&U3N}Y# u\Z$A}2-4Knx 1 "a'@ hM?El@D'H?-G]nb)}7ңA|xc !XF@9,Q=m6b}R6n"%v$A,>?JwɏPyU-m7DZ<ƂlX .5~QJ Bv/1q„v[$^^ #t{?o,_r{)6:Ư% v -A&iD4:JOPQܶ ]$d";!Rx ކ*R!!.7ӞhFmEkI 0)'CXk(~ T HpC@67 F"@ *q'} $q ,$27:>3^o8Kԓˇ@i%H{OVL>BUsd"' POp*\ddzbWzi[W-A[Sxw";l7ƀw-.ϼWvHXKN޸s$ $N5x=͛?WrD9A,MsKz ?o8$' |2D_eZH=w_[EVCPX[|ng;*m;"[X (SCE@bd?{^1 :0.:n|G ;ih G̷8E;} Akol/>/A,zb?M'"  0b tWnh;]bQ iJ BNrF@V@$1#$?{}B a.BJCB -s_(#?o@,F.gH xI!&q_=OV~Ax" c ! dב>!@+a/Shb~Y}|H=A@ny;wQKIM #Zk]V$'Xt{i8@,@qH#!P@(󠣝X*=?}{7)%@N,qL ^C 7F@{C\tqxݩ[ÓaߣJ/dui@,%RO}jl꜄z- `8{x5h] ܖ4=\N5>^n5{xlTɍ7`G :ٌ)y)g ݜh}FYh8& ӹ` y _V#G\(WXyka1[bC`#& k MH!J\\w(#0ע݂O&WZ?A w7Y3 QGrZIX=Gh&_| A [R+O@ fYO3ygT<\P\ sW=AFj ɹp HBW,'?h=WmScl b1ˆBƿ*$96/@b$ =72vZ?r# '[H3--Mnz@z hIyU;ﻫG~c/#-LMn;8t6Р--jnGs|\(^xkZI 7 1>wۮE,%|-Q 4yK`ugC  D oPCYOvEQ  hǫHW_V(#/_D\W@5<!FLdߗv:H@ (m/Ƹ?Q`O$Nx7N[D#8B߾?./%G#hp5BD:M 5X fJ mu~?0OC"(-jxdB"iҺG-b׈3a0אp9N:ME IIԉN{<)iai0 p{ªL.@"AG+G/ =}IMc }[@,F;sKj!r:qpoO Epp07%;`)ADܵBxܜ !|=$DH~ &ͭ+A%2iqJyh=3:Ď3qXLHR &) |$r /B+\霗7 pk@mAK/t|j B&փN/tnt'_/H 8,СOt:_cs+r_b8$> ~?e5ѿWH4pH "~䞫(O^x#{[)Գ%2#jk$%ҥP`mUZ*M#'A$1VF@JTO F:H/ӣ߻N7'phy* ~mC˞-հQt|mAK++K7<@F»h\[uVw;^s|D ls!zTd"PrES o?8JA$HTݵ=M ş9h-&5\>yKEJoa!'xL5.?#@jS }`gg U*ޥ A{‹#uDD{##S^q{_^3 P|݅q(4?#>R_9ۂɭW2;(x@k[i%3mMޝ̱^ee6ߺa H[;|b[zprW ,~s=˴3|T0{X߱ G.]XR({izfV=탞eZ@!?99#1A,gb&+ [\u^ސB (b'] IDAT!'ybyfQNfRQr G쳗 |IfxxΠ8 )qx~0lmIq :ǜH9pp]$ yC֘.ݣR]kv(]\I5I'IQ@4q IaPK- x3݃I)` ]HZ]vND^$%zyf""m0/tϏvqwXa=nI3\Se)dbk]ݽ_w@H#@Vk3g p{^c t!G#CO%v;e0D9.6LRִ⃗şܓ Z `[&/g#Y=?A(Ixq"H`RH:PHEZ饵;َxGITf ΥPrީ J=t)$'άG8ǙAZׯ}MgB%_03V:n/ s.CЇ$du'ǭnsʽE`44Ti=m%aqm2%k *~C񶜻us,u< JɄ1CZEwLB[dth~7Ljӂi-d HdBFux8wgUE+jhQ(P@QD:(q`Vo ϛǿȥF\SZ:|)g4|wľXy<8%`ʆֈK_ q}%):o,qB%GH6Hüha PU.ް̬Uo@Qu^u)zF T OL'=O{NuD,fZ q`qAPzqXfbn7Qχ`sG;O:Yb@&1s^:҆qt^0Ut(0+ y8 gtr)nuv&79L&uoDX@Q{7F-,`V@$NHL涴yCsl/) TOk#q`چ*Po0*V2"7EEg$ۉ.(30.Za:nDbܯykײЂ xQg8q8R,(UTmA.u`0 QcpXMc @E*"Dz YF1eibzr"8YN|f-XOf䷘om!( USuWf2 0f4)Q %D{D^k;ܳ\*1uΰ”s蔦v~Yd&EpY IVI>g>jFUTƀQi*Ԍ- Ly5tVUZaV2,s!)PQ5֌aOԇlOw] ܠ+Rߔqepa1 V52*4M Xڼ.l˞zX5ܦ r;c byS9j$jpa9 ] %PqeD]U8niشrF`o=~hEUc]pQ:3aY ԡ%Tc86'`rQ2@tWZVUA0;PgPhR20yC!DB%U1@1N5yths VLY؛ 5lڎ+O=zV+۳dE !倬1(0]űލS`<]̹7+t2 2YZF#t൸ϛwAj5@=yu*f%zۇ&Zqm,Zw>H~8j1}1{i003&@on[W¦'cVA_ 2Z.s,q4UCO=~ mvSaf1>w3phI~,sX`OP DfqEoj9dG*v^MFQ$y3ƫy9p;L3B-IN(<}3:o,r5r\?[\s:x,Hiع"ؼj;z*V  r=Li4nCmE5qcqgC;X - D (,5Fu#tˌV3 Nދ-;^S7_-vgh"l7mڡ-8 S8LџhL>=sj!I6c Ip̎TUC_iՉ8{ef?[(w+C jƏcgzo D0 DPE 9I%$yOK$b!1 fnEJ dǎ qgb=ܶx} K's S-mãy݅[&6~Q?fY<2m7t9^05-a1г2qZJBFϘ5 F07q30Wx=x09a:fdr*2Y!\ 3^lF"sR\ j`=vn<WqڍFG vK0ߧg t4S=Ԙ>-μqe[x űmY8ce8n)],땔qzVbg%vl81W'{d@ur*Vu ^(kMyg(Ď gcdžqކby'# p"0bhlq2~Z7`~ܳɹQx셁}q1^bALNeqބukq-B("W$m#Rb_K2_Ùű{ xƍؾL|"O#Ngabz&݋0q*v%a2]ʳ=si:3q wcssùoü^=x/dUa0+ zPTTuЍFG~<}fhhټ=2; LHcйkR`%Z@>`L`+Ysnk6H/^b^( ۮۮ@q` z]x`qAhQvcؼz;8U0 f⑃{t:|!^1Sk{g>ʋ.Dbkf>C2#⒓_ͫO lF5r<¶] #x}ٞoa2nKp&dFAVa~xΩDIG˽w#w#mSK7ƀpVT4\̯ .m|[`0C?Q6Lp?8rqoNGF >GDC(X5|.9:s{aoᡧkgȒGasc9O1Q8ߏ6&ˇ Rl5c אsfqTJ, jVu@H=,pI3*k{֮ ''N'߇gl:t>,f><ԏㇾ: n(Uq5x5exCj D/ټ\w&PҟY[q9B8z*I쫔L*<﬛0г"P;)ORS5lX Vm -s64&bdm.l[ />-8<?}[;fe'qTt B&"׭A i0XG6GXW>Қs1aVZZ4sq3^B-INS*R JwM kNV/acO'ݎXzd0 UA.!פ0G̠E\M3L Qgιx aO/γV9o~DUMcfຳߌӶ],B>v(b!Ϭ֑´{G2%4.YyU ,0t; S]Iߵ\[җq`yw ?z6.:+e !oYfsltbc (eP(`vvFA;eYu`zz\-3Z\ssG/wz ݂e0tg!_X1P7 T*a||}}}FW«EE'gl|G܉l(ac~`b!gAfPwx]36/w >/r^4p >SSS9òFP(`nn+WĊEl7^spԫ/n̘rWR@7GCRXt02ږtPgHSʀ ɝi}L:KsR6Q) S_믾]'' c B\|aV^qJ%]!,#Ûc >uϙ&G7el.YS%-1:n,_嫛_>l<_~\dbxվuS| eT*ۇO`rv093QK`Z\kr` V ;ZBWP(@u_!$F™'< [Fv}Zl*eÐ <=cߧza[&ChگlU{/x<~~X>0 i{qCw~zʕyݍ վgz%=X99t]Ǿ}~z z$WP[-(\8Y)v=Ѝ9Gu?n/-lwG>o,^/ Cϱudoy~X^0\}྇{~.ps3}{݆[ą][΀V>0F&i2+xϭ7`v\Gߊ\mw ' kJsz/2uC>PNvTЦ i⮚s;v/=fqVP3<şYiלG૾7)1AOːq7l𷷾*^N{Çò`w>"ܤ8=nX^*$YG-IS,7 e-B.6Mu{Q Ӕ .>|7qΫz14I bR,155հ~Ǽ=_˩f;nЋ[rc/tw8|o^C*[ A\XdT*alljl K[8(LRqEqN`{__|`ўlDaսrܦP37t oزfw;4 /s5l;?Rsoٜzu3Ze4L0YFeއľ#/|7V Ӄކ5o8;7Gs:W[ΐ1BA6/Y7DEYI[g`}W]nquA3?ORW$A8LMM5$}_~~G]j zrצq'o:u-ۅ\/2ZLs.}w69T :3z~|^tE B# kߞ`1;k=C9+4J ZlQ,"p|"YDi}CL IDATNg_STJ&*_.< 7~wsV~ [H'''kĿ<CN-z.3v( xλSzuAr`hxۮ;I4$A8FFwwNd*l 5W=\~ FFӯ.\-PBQ]9%S'ŏfzc>g}~p~v׆T?2үGO;~נQ}g-EþL¢ xyAFswsA4S(94>i-sFԄ?f=Npoi\.Cu0fߏ\.+~.u- 7b0|ON"Js S}a8q.eJUMD&T o}G^f2Z_h- Ȗe?<~M̞WvĿ6}6/^E\s\şsNSp1Rv[1[dĿ7ߏM)񟘘޽{qaLLL`nncnnطo_m|/@ USzKF^F9c. OŮJspVh͕ɾw%,! _oYFY|5cV+0 wL nx k\˚ 0(ɏ W9Ǟ#׏/5?{ٿEac޽8vXmZB~J%tz7}EdjcBrwo& 7 M`~F>]8 n[`1ne'?/i0Tt :_Xq. VjonNvb>1T*KX lUUAo|zh˲`* FGG1??_K4vVu$I= n vп֎(OCug~fʁؼf8Πղ8*:dD\'*pG,8{ ^|֛| =]r4j )xC84fGEUj姽gpi˱eRDײ,yzOe6FW4->=p}KaaKV4k wXV:qC?sTJՅ}|VMOWžڧW~v߱OD,£=4]~3^+ɨY\q-u]'G~'-&g z[pIm WmC&c Cw}Sa47f{=_3[Zu~X/ՒY8̪+ {\3Q.L(Nxǵxo*^#'"(sh%I[(M~(qAl(x5mOq[ BE8LV'^o~_0s5_y;#,$Ш at zZN\o/#;xbC:W걾›M-+6vl<'1o[jEy5wC=m):o,1Q ~ifq&ʸ`۵xbw-CD {0Ћ6Zo8{l:yw aaߏyՐCFbM u9½s9-5tg_" ^̷׶W\ޱX  pq gۼt[*>oR2ZKT;+>oFOD+%dr*y5]ZQaU8&/zxo'؇\5Ѯ![U ߝКnLǕ\WC]' ;ØgF3  8%+&i/Z_)PG{dJFk=lqۋMT[:?,{_$z/c sssÎ7\y3>c͊߾5ox͋.kffC>9וA>+Bg)bM࢓_XC/id\n`aRH;n̊8i8Jj~`>uV=7}݃-)ODҔytP@mj LV|;xG}5vn>WJ d3vA\nkzz|KzdY2 uuw߲ΐ7͊5,X&G&x z$ Uq;uZ&C ^fsS:;oo֨AD;+e{%BT0;;aTUV~9~=Ъ@U5 U( b:JfK~i-cl9 {J`&ߘɹ=3K˵:-CdkZ 5^f QEOȲi~na~=ϞWsN{% v<8ҳF㪪"BF _t=-O|}}oYxյn neڶT(.Z'.?| )usi/RR2Q(k\[眻Պ "I6=ֻ_112g,TU#p=C٧]Wƹ82ܧHfH䌆?ǿ4gRjnR>/o/}e-;D? `rr@EQpq"2<lA3hS iرc#+.qS㜋s-#ۡ +VsU Sj)/+qM̬G_M_y\K'L&crrm}ͭۃbipQ[Jw Vs?xHއt:BZԶaH A RS87io fqضtMޮ UU111~ZzzCINy]סzm sgQ3g{sn8J{=x* xYijO~a#_^h,"_05n77+ s:NtzG[^X4ŏ 7155]qAcժUd9GNˣb Qq}_΄f`&<ףKP~}im0xG(%ezZ4 \Apڲf{mQ 4ψܞ`&"oK(%s[(Lq .:?A 5$  ߿SSS3_.՛A;j,166K677혜=@8M;߇ޕeY40B=  !?/0 wq-|e x?A^<ϡi*z{{30>>\q9~Нgq6&Q(Bmr8rH,(#Ue[%UkGj1E7ҧu7esMjȈPqu.-/0TiE)A AGvE;',0::an\{Z{2F_sLѣGg=zB+9GXıcǰw^8p-~o`=W9շWkh0|Ru7RYg~:,Q7^Jv2a dbOwv.b|NG/?異*^'->ONBGݻSOᩧݻgٳwÇ133㺰GV=97ܺk۬iW7 4ob1:nE?Ӵ`~9gukj-uJE AAǥg] ϿO?ŞCZcǎҙn^vߊ\|dw>.4ג,˪%yw_0te唆~y]8i /yU9٢=X+, /Y:> ~:^یF Z/{f*_執G+]r}gW_|#zZʗJ%<ʕ+-it׋{?,3CW_)__ApzxC 1Y<=m 4poCMӌ0Ml֝ҰRךm.<: Ep]3Uwy_d8㙧\KOD(65pc#?.p:xcvvX>)Ѳx?ݘȁz}G}]_Dl},>}] #E ߁1Au/,(dLA#.4iC DWs _gQf̽LiX7̛(NWpٮqsEODdYd2`;ޣcqy&g+8Kğ 1M]]]o)s>/zeaarr333nY[@aO/<}X (L4 )yՏfϊRP1WOk@x&~y^J8yq&Xw.j2 e/]̐LK㮆[Bf+X7|VZᵶl85lՓɪX:B4̰T3#`FGG1(`5WYNxv^*{'w rQ<--VН{^d{sNOD8n]ǝ=Xp><` c{<j l4cuZvr] qk/|5i aӊj8Ac3G6ZbLL`gp gd(NWpq/hi_ g,:&h6a͖~d~Ʀm;}arr,eIOG_uڶ5Xm, V P;o'6x+קG1Gѣ Nh?c\ 9f.Z׶x:c17ݏT0 Hcs:8xK>C?AILQ{i~9@W_ bv0,SY;/]_˚?11I bhh6}Pu(M[06VݯW<Lf 0 Se;*ugݗEP~WU]My ΀+}nn F3 rU"X&PU˜>/@ ÝMR9PYq\s*wwu<;Aj$$1A6_lN8|5$cH J(6jjw6LΝr}TWMwש^Nt*yC-B -$t]8g8:E !17C!Cy>i{ '۹YdhD(BUUB8!ιn/z?|k(9D" XS Yi:' $;o=]BGZ[5Mm882M_Ah%.&,kxS*VxJt S oCeZfZhaQJ6xߧsv Ov+Ͽlz "!^! Lz3uo%PN^@`3r" 1. 4+BV,3ԂAM L cCokjZhhI}τା|SQlGJRJfA Ob/Ķc`:-1jyWi'SxvCxi0; JKl`z6 W_J3uXP) t~g~V -d0M\<σD>u‡o=h`%TɍPsXqKM*g|ϖY D -jU8+  [pށ#C_OfOcآ쐐KͩEҴytv;?`#$Ei8o+֯9my?B0L3q O9!X5N C1w\)tG6O)BUM f4{|5{0Q p24g٤ ƞx_?^1ڴZhLA"@ t0NLHB&5䒪7.vy%}^cxk$!h$"uSE23f3\ XĻCEbrf6pAU4 2-dfTBK-pb"QjLk7@z2@۲%Ӱ@`&*sZhLϲ,AJ@ڮ <~·8E;DwJVG.!QTEclfȓv1*<"Ĉ_%>tb}/r.gh6 iZ.R T]z` @J[P{PKcF5oP ͂ZT ~ֿF,Seu[h 4Q(0 AT"߃.Cxb=?+<$G !PH;Ӫ !rS?KXq'qD\nf.+>n!7Qml.4X l Sz0׿iX)e1mۉh>6o - ey9rW2fY躎uN9yc1(i[E e~|@ߴXwn0cYV sJvr x8/eB -,mC4h) r7^7^L>W?/~+ $։X[R`Tcej!b pwΫ~k:/x̹9s7u P~ 8v I/! 04 5`hbiͅؼ"oݙf7 B|BaE,B ]-Z83Ap7]x117ǟL0p<..xD:iCSLS Ӵʦ̶Y 3E+KM}~ u Bb{dJpr8-16/W%pI2m5xwŶbmf]v|48)>}iMOVrjd)DBT }nFoYRd[&<<!ehpj=0LM,`Y߹_q(Z,ScAމdvbOdX[a+\ i ˲;R=ذlZ{6ق]g%2c=E HZ8yK#c\Of2R˺{ ,FX2Y_;KQQPםg18c-iP3Rq3.\ҟmۘ@6U܃#C{18tUk ,ql  }s UM1M333HjnK]VizBre6,3,*FO5;aTJ,SmX 4!ns,+aV6jΤ+u`~5MkhDLԎag4ql `ki^IJ躎QȲU$ZhՁhi!`Yֳ r]IlFMD*B:ʍ-'}ǝE,G 9*[XP,`OXK%e;D B7iht ai1B@_#cב Lg0eöѬwas9!o9fw jI.ˡ[hm,_ReeYBJsm_"J!,8;tbvQ 2KZYuB5_ZPN>)$fff/tr OvynQ B͕X02`g,#U}eölJ5fLvICS; ߎk/q÷eYB6EoooPpz=R)àfxse!K @ nP[5qg+a6؄^BW{3yjömLNN"ͦ'ī?Ʈ#d1"(`GxHb2r hDZay>t0uq~'&&|߶m<ʏW7)IxZdXf(ʣ7 %#Ґhl3ޣ/=o 6( [-&irr5F'¾_`aZc~M@GMP H"4pF)+h+✷#qfHq@a[)W]̫,<$GF>#;B]-,yqm㺏#,[fiZY-&_LV2pN~Go.[*AN6B -p9Y\*BϾ0ҹy$C,~l1 6\n D@+B)U-!v@ZԴ@5gEa]9\累?cjjc_nTĻ$pn)WaW8'։L,]r,b6@: E 2nj˻qb0w'JyY$m,ض MӠ( TUiKr:6}/{ ;=r4IU{qa>tC9S4W5_@(5ªQfJGn#PUe/S;/d+G<TÌvȁsQH%I%d,B(]hNBdh<5ڱ02uw1൛Q[hja^ń\6E}xqc9X,]CSmBt_B uPi Wc44,[9`%VwoRMvz bCZ,G|$s`y,p5L>@8'< ݂V0)f` ^bѽ!\REr[`:59s-zzz r}/|>t:]Vfv1ph˲ mizeTvGNx>Xvm\)!&B(V 3$@ӄq*eZr+:'5??we9Z/58>r?zvm='rbj! 8^@8dyQHBD(* 9G.L K&NOǝQ7uXmqpdY.{lm{ѱIPRPW&KQ8rj/^; &ռa" raD)C*!X[S}ln?+ܒ@ӲUBW2:]^dx&F@Aۚ0" $U^`s~K`K8BwD'cv6Mˀi0L5NMI*0u ^U`X" 9Zx| 39'жmBCপRa1Ä́O fm8=~ O¾/c&5%XNqP5Za[%"H0t:)P6괯߲,x߳_<εGR ~L[6 ̅P:$Nn '`6R|OfwJm"LTa zϊav,\Y<w?1]iL-9`YFGGJ%3fB2=!N ;R** CnU B2NnUeyq 惶 4^X@`?mo[6%iڪsssF*~1W0?#T%ą]+.-ĕ1BvƋ߇.~/fRqI<}>$vU,@N0t T0[ a:#>G~| 9 r+ /2 7̭Oq?(w&0<[p<+-ۄn,/e?&lLj:A8xaM5vJ<If!.yMdeq4o`Ynj4y/X5߿aH&˶pAo=/Z' q^hLCB|?Ew[}.Ft{q۶~ s#xU5ag픠t(Y=7i8 gSO?|?j  EQP(t)PPf)d9I̤'j*tC |PIF8$#"Ee¢$#0$!7wlA6B:D&FN`h8 z) <(Wd#5,pQ/a Bh֯,Me?WJ3u귪53p7+Z LOO{ŋ?c;u,Ϡk켰e]weiqLշ4,D|Y}[W:Bz#7|bUf)C9Z`| H7`&9om>,\333W|Au$I y`YMӐ8ikPpHMk%>6O0>=\4M+`.5>h8h89!CHQC2¢Xp–Z`Y2$2R9ssHegb.3dfI'*ZCQmjotH%B HaGQ%u`pkfM}"$FVm_(s|f0@#ʋlEЊeP>qK7_(,dY5/pyRT+r;>u_a&i|S22,&B+ȧ55a:d̎君Υ'q_Ƨgy'f!L8{&AUUyj²,mjj phHMzqJ197{ Vb&5Q%8!!h$9G4(H0Me4M2aXLSiZ0ma X4ͧ`+-τ ).XP|`yBϒc<4@F\oPfYnQg/ {,XtGFKhu70?CH,0<_^*\e8p;=w~3C/a(s(nr8 sj3o_2HOϛOO~πy ET(077WSB>G>0eD<_f"͖Y\vy?~0/* Zs#u;fëgIwDb+x,[c تY`9h{Š @5X.9L2m*L%' [,Wpn~z. dB2md*y]xuUu6ng]9lw׽@=i.+VRU Eև/~~/blj 1ƒaR*5. wW~o#kT_D"g+P(`bbsı=GEL>L>Ç{yBl;z^P!Hd2hkkC"xCXTSUX+8'v:AA'XV@!EZ>U,Sc 9IOwC]uwXQ]؅s+ΐLA*vQsF}얁mu+@\/^x'4r'fo6Mi_ EQ} uif`Ypj ޶zuwWD+V*'0̩YMC4]>!N0 ΢HToY&S;c|4m8zzpٖ-b]&cddX g2V EQ057=q"~<;aa~,/_-,`jBcBm݇8Ϗ[ @G(m s 2 wj\.ūl'4d0o"\C>A+o5]i:˲fn_ i"ɀ8Ȳ:7w݃0T br mb`@[ofA9dv?M|BRdYnh>Ԕlڿt//+އMQM( =#ITUބYtU2NŰ 8~ލL4}1k< qO/(F\QeJI-cURֿ*4M  nR C2 לwzme @&iaHRD"k#>s8rIZjfF)pѹ6 >:{'+```i}њa||'֜rO b CτؖIcMnPMC3\A81zE=7_~lFWWhnضçpw5 qgvp\ui=VWBoZM_aKNpb$:ӟj|{6W(6 (r(cZ هNpR\R tD!!'ou^@F eqW \Oi6,DQD8.;]JDDdgA'3^OlHt]lӲNNN=_3-9E.ZAXUl FU:`,(Noo woڶmJiJ  M HNpۀ lU X(*m>#:Rea2nx [*g-XѻGCF._ 0-O|Q.^Z\J /gȪօMQiZs\˲eh40)PUa@2:ٵg;p^ź8MMM,|>!~%9vh%a>f~ bmXXDh#WH- p)~-=UMBu"QDKF^XΉ$rxD<1!١M$ Q0,wv 1O9![H00::ZR+dvR }+48^ 9^N}Q9A] 0N>^*o n7$X.DتYNTv^?aVRirg2eR)3LSQ@`HfYdYYf[>u?/WQZ$Ddfh0&Ӱ><.tck4Ma̳{‘SG'2 t)V1+[ǰ 2o#/%>val4M4 b !dN ?eݦIUCsHp,K4,!e/ͅ&{vE4\ ,V@jiZvS,BRpabv6`e0ct ެi_6/(ʲEQD2>8=1!O|$ELnG,q zgqlR 41G"2@N`!o@BH&ZV w'ī?kxB@`H6/mzYc3@ g!N] ]5)F? hw CSt6o•[Q7Wn+2,J!Lwxh+lybEP"ו~fu4϶ۛjZǥ u\A<%vcv *G+,YC\dUmF6r|K,!̏\I rʾ[W~22:#'їuk6s߆7^$1 S;hg2qϢpCȧ5zz C `)b!2:tD*7=݉L&F1C>t+GN.ŔV ae(C3%Xl";"5U(Z<}724n޲17U` /rE),l8$9nK, CPe4qHyRsNJJ]@ aI,Rue"&&'`&JGYh%Yf޺ro}9}uLN?<$pz8~ w?齾~z]?BPt/5aGضXarn;=㵷T(XŸN` wK "zϊQ7x{+xO/П -ֈ;3>{*WO_\7F*-p%feŏq'O`{Y0[:bWJZKճR`\qc\JcGpZ?"$˲p`p kB{lތn׷,rߵ[_Cv}.Qj,BB.!3@Qsy9\}ۑJH$M( ,®#ymqHB?$57J>CpPs\pmgн.T؀>nM0ĹFMӼf T*U\.癀{;{K, h_sves +UU18zBXNN.̂" "!|l.=;u:MOOcڵrDAx|ş BVcNZ+@ sR3\ EQVRxKƴR%@YQ:"9[4E-:$r:@N`ѽ!lYlޣ/""E~krDwww.mX}6O-k[ 5^`Pq3܀6퐕%Bz9٬t @d$Mx3~*ՕiP,xGrPmۆUG~xzϽN|#X,^d!3K(dtpq誆 @4ǾPm}ض|iM1V brz 8ރ/E.[um>ue%qP.3/"c!G9T>ꗹFݎaɘΡyG ɸ;L&eVi]暶[ 'h[jGQI,0eGT2-p{oapCzQXEնt vhhKsWV^ A ,ݮqvL]뢈uJ,rak"XsvrB{%<?sssu. OxS2z`_ vޥl^۶RliH瓘KM#G s`YrmbLH\ r v; ,Ee_F$&?ҏ'[m_{f/s,N,BDs|?taKi }\:|]Y{ 4Z ,)A+/׼sw!T Z7Qi>ݲ,/Fb{f0 G0SPTh ?]R`1j?{-p8>_, !y֐>;JYLCb7:~B,c2A{_NAs^X)Ai%Ǥh\\ kf&^>;TC8. %e6г1Zfm k\T*U`6X ; V !RQ9zf׃N*g6XA/w_# ~v.@&p̾*G.gGBQUc, )EWbzŭTZAȐZ lVWnנt{_ :c*9hi6Wi[k"5W8iah% @PD%ePMuUB}=O vަK ~ $J , ۽@>z7ضOכ#=˲y7Vu8bP CW(GŕqNżZ\n]m^ɩ)<.y|[7`5~ibSyem:$UWdh,Vku*d ?"zJ`4S Bm>J{!-=]SH 7+ȇZܜA:۶,\e 6ʊA pL'8tb,Z57!&CWh> k ,@89/pjxP HU C9 +yv\+4166-B8$,N)h婁*͌aXu 8N`4в,tz}rFf:̦烙 p˶}ֆ_qct-1xKPٵa0MeZfj NOjdB .jT@i`"A p^pH>1RJ4ķ{ZAo^m?FUb9cPf)ej Tyma-m^0 cţ?1eeKM%3 /P}8o@IJaMp̤&ߪBC1C3WJhOi4mߗZVskWEҭB/}n_A ^dԩ 1Z'ݛѩS0h4ML\:,>}_ IDAT…R?-i؁e n'>.,zWt i4,[:2V  wA֋ٌq^7$Onh\.x uI`8mcgjU~f4<a!dUT@ eP9Rn|䪿M͇ o{|pO>jR(ʃ\Ļ)שׂ}!7'9BQs)L%GW%,UtL 0-P\z`0P$k1nlEY aO{miSdT~ Ss9Z>-eMa{4NYkϭO',\&<mB3H`x}^5݂euaxeKy^nxߵ*s`ZD }dov>_"5BqY\"qө1$'XG-϶ڀٲ3#އ`YXlYAQwZg9p+νwxc߆;:A1m_ʁz7h]uR6/urךts.J)!8>r8!eYذ\f4%<4gp[V_hG7HvImX]]67w 01Klh Vwҽa7GҘ%q]poc_{l\ . 4aѬ7 H@U" LAPT,Ne?&,o\dw5d.&aCRYhZIi "mrD!~6 {Ud_8qu;X߾ڶ2MǞ kP>DkwY9;szX[[T}vp:?iUEh6TZ:{0 Uջhi8DQ]c67.>ſ%i=mccc.\JTԅ'{>(*EuHp@p3x'뭫¶\" ; ~ ʜ]KM;@+U"N)^k#23 JkEEB(9]mK+@~73 fȮs{gFz=,,,dni8}4luG?}R@D)5Ozt]W.z^BF41tx_l0ng J*()h48 |GT{Cl4d*h/_y'VfWhaaKKKq]x_;vL*H +~ M¢  5goD(l,2wt7Q'8HUbjhIӧ^|y`aaiX^^ŋ6VkgAMDg\(JA' F˻;u!bX.?Ļx3J*U߲Sg7X *&T (`wý}o~_x+ZpE\|tܨVjx[Z0+X]:p?(="0P#tU03BrȉS_S}6771;;),BEVsn}1Թo9vA{0 5pzN?p]e3`6VVV=~j дZ lnnz}ԗ7,ٖpbCܻoR',~5O[{TOk*SZeHyxgx s{{fF6Vg#rMѳgW)Mշ v T *v, Y?aVa]w4R^XLUN@˲2B׼ԹkBAs8}Jؖkw^XX˗jb0MXZZ… Ÿ1< @#g*2 eo3O=saG fq˱[B;Uh5ud 04g Lo?F'O^~ėWBDg_ZѫWgz @Z m,R h}̟" iLD1eیVl}}a ԽY«~`|5۵՛e*5Hڝ0YFvs… XZZ kzt:hX]]fn%h_<1,]^u- gݔZxMBvDePus6pig*#.?ԟnƒ¤j-tzpBȥ;<պUФ:<=y1'-Ο?U9|ph(vV߱q,*]?q 7y~rF@rU0&e,!`ěa8='paE"'.Φx w`;_S'e܍{a `m*^j0/.pžRBUtX&C/\*A/gjzvxo{>l7.?xGĶm4*32 DMC CA}* _u9W.+}O_unq;wv\w^gM6 6 =?!_NmO@>ug2Εyyc R Y qPAxu' mee%Lca8aN@m iq3)Tm>6>bi, ,._+->6H6TexP63[{րv KgAaJ<3͹j(" 桪ŕ x oᡳ*Xe~UWK@-P8PPueTsV7/}|?aXYYKp9K~ `h.zmsTlnY"c5~\kU°i :}5wq?OײPH(%!$=wF]Vg =y[fc7OcpU5Xڗn8#7}.T(D=ڔ&k~<}J*"$q*`#L\"^g:pf1amzLCh:j)]x;BUp^S |>ۛ‰MM|+$? c4{;0f ID#.&IwubOȟW?g@gFq牏#uLWh!x,Ͼ/CIwJ]ő8~+eT‚P6-Ә.eZ:gG_pYjt]lqCBHBО@Q)jSj:ݭ>czJ] ܱPûJ]J6H_щ|М>4ð,}Ua>(^ď8 Mi&j 6qk,x `B]tbD`={F;ma4F }U Zm vE\e ӨWLb&},aގ۲186pST`dgP3!⻟5k3xx/)^U- &Ǿl$ QjSm1`H4TvÙ9"$/68erpIA7 3:~XPihν3k*TXGmho-J8}# ;}Ln Pu`[6QL/ ->bP@ D*rp%9+?w-0V_ra ~X[[Fm ^fXmmd@64LrX&*fR *h5gJýT+\S0,Nfs~nz4T*7g$u IDAT+z^]d*(ԯ T!64gunf $:dzj3Ĭr FmPw tUg*| wkݦPC=3J18{:^Љ2m)YgAT{,y` h5P*TQAɵO}Fћp&SCaN$ s $ca&T1?gfB[ȝD/ E9'>zjaVVVT ~~ c02k*L^EGVQ1uƬW`6Zk=W~3TQuz**\ XZϜ!𮧆"D.MFT*3 $Rʧ KS403|?h{F#wmH500:Pm4D}VAG>;d PzZU֫ ;&,c4])s:Wy}kkk#|҄eYXYYCPzǎ܀?_ߍ+goN-~mV4M.j32^P/BU:Ʌ'%_jhkk+œ1ڴ&"mODqFUD@)wHFYMlz6}Wڨ F9X #pUXoK춳i,8#L&`XB4- ɟDE%Wj :ڔZsէ*^=w>j`q" wzzfsߊ_~]lJUkb\Șh'{ȝ/YNamz Uߦڴʦ:}bA|Us۰mG4 LMMIQϾx/eYX\;g'Oݐ7ϸaii3c [[[`?{Q0Hwnӫ.C 93kA(-ld-;$*C%e"(EJG)qe) _2l'FM; *ues-wӳ"ErȎx r3X"h- o1 ?jbP* ⊙=($'֝ @j r^U`mOhU:Ruu\=CfuְhׄPisUt_;OBLlHNr'0p3m[Z:jװvX̛ \wdz=\|ǏBEۅih4 ˲jnS_u1Õ| ;5wA M HDCvg0 E uu2CS@ԙ[&4PCv,ÂerGUA o^)0ND@~TXMim څatnƇa"6773۾X4g{>i:R&v۴%9/"!s+;Uś[EЫJdmki6PnUT78ŘFB+z/db4De @2<+udȬoϼ ~{$eY|0oVp/--ayyn3 0n%drK?cus W6f&${Bh_aaDPT+bT>6AR~DŽVQPiI la+J\V$iX wx2e՝*)vtv.jM of`\a:E \\]?hwv7*}FYBŝ;M *%^ ̰R'nOgHt[ꍔIO V^S:AQu"7 '? j>퉭(qEp46".=UVJ1J8 jUE/)T])i)#S}72]֣/X}F E8rc#cw?`q傰0 zG_xԛ3:4/+*AmJWQK Ы_ hYaP+ľ#E#hЪpGn*PO/_w>1M+++XXX$NKKKXXX@ 'Oƌx# Br_"K BXqeAcVzHGw_>_5~Kŕ-onO@㝹>^R;[lƗN~ ^_5s 7 +++X__4ƞ^5(]lwΌU"!$V"U5M -ǃP( wmf9}  < IԿ _ICcN #زm)%MMk /pA*ZzW.o>q|ڴe ᨪFF `Q`&-T33|:Q_B5Q'jow# |jPe9XfJ) DMFDa@ ~hPۀ=AtbM@R2OjCQ`z>?xɳ CҾMlnnBQ(;1vttu| ~.JWT%@0?/g\ A5 ɗPmB;Agv agg@kG o>9|}!9}lY`k\J)j*4MiPUu"1^7~osw$ gt0ijNώR9AGvo@/B@8y#-N&U0zgxs U4(A{1EsjS\a&yK#_2{_Cn62:BTU]%|,˂i03q.~N,WE(:őFМWs(NUT%DT0g/&e8QB7@l4\0O6m¶:E󐎝-kR4ly 1|'g"?iFl$F@m`kg>uxK鴤j:f9{WuƬ?z yfS%=&9o;.;$'8&ϵwǛ`\Js FFߖJP6 FќrV! MW:.Oރf}|7=7{:4Ul]Зٓ;*55?hZuSRmWd'x H*?h&w{`gI GLX&hjSΖh * oj6C#XM@Z;/ជM۞\rݳPw, qxI,:*x И`f++5i9955_վD|6mi,R'K<ܦٷ=FI]8|~r~? 8{! m6C18Qǡà4clvgMlcq<^~8 TN|9-C(AI!R/QD^N܏c?H U(>.UFm W|;5WAej0zzl{M~1;lGF)8èXLئ!>s밎,tZ;DaNf6n,n<"A}JCPE0B>Ay|WF8E TľP'$r"0}(B#`o li@+uGcI ZEVQ`,&,ß(*EcVAcVm3Z&:- 1\$h>oVBܭԹOf+* BUJ_P(~ľ$qF„{>EM+01}ݶ /Q2ڴƀnH ObiP8T] ZmKʵ#Ak7+JqK/?RK>,Nhz`} ! 1 T|ݽ'NuX#0{lm+ 3a3} fv>qɨBhzEbߍ/ h:0 PRנ{ݭSA YDn]y/)ڸ; dh) U42|4 eZGc”(E}Vmr"00X*1z˘rKZiOsW0UT E|_r8`=H7U_D!EVYA DY \2mUX:ѵ.u E}Zm1= FφeJ z% hMWFFǸƄU#zBQm8yڣ d|_KlkvAzfH:d5q*P`pmշBmR&Ckq3B@\ۿo0{vX!j2!\U,>RSX1ZnS08{HBs | d:#65ؖCL!\^U}!x`Z$@(ThќTBaBU K(gaIt0HJ ?kT(*!;Cs]U`t-b @eKM Џo@*?T/Q"{"DەE,- Hqݚ#-ӆe0QOU* ZgCx{hL80,6mbq@R U%J|@B m!Pu((Q"!Ra=S?i~ F EgvE eY) J_1}a;>B!m\a)`# 孢rUf(Q"C5O ibi D?&4f?f<"·$&atR\g@KÛY?O P*"άʷD "UlV!Yț*b A!`!!5kٲed41;@wc(Qb¦:J51Ov3LOPVBjLB2n lM͉Ed-@?w㹁< < AK88XQGuVKmʤQ6|+?7/̋"k㹐Mׯ-Bΐmbm$4Gg@Bݿ8FoPN(qP+ܑc wۗH;pڥHX^RM!T*W[Wx@J @ Fq5?0ߘBzq#Uk&؃H{ջ[ok[WN: "YSiQ+2Ɖh|z,d ę'vIZPkA-{KlKz'??lȩ"pqdd ]0ri2M93v`s k&֊ح5x{`QG5 ! #Yf D! 6Y0rě( (Hd"!w h _ϼZݯZ3sUw%E;cx~|~qX?&gǫ'19w .aW-1e}K(*⾙aߕmŹڌ= phP!UFJ2I lWÓ(f1k/QāG,:Q <D|Q͜Ѳg5 *<|%8{SD$ӆ`eb눔}ϧ 煰q1Ll3zK( mDd-iUfehHVkbSKJJ"PBRa8hQk!I2C!QK@tB,YD=$l N=6s]_$"Zy*[53W'WDh?DSRMg8xӧ2&)=(5痋Ks$}-@G=^?^Fa-QDTD{'+ (?-W~ :Iľa#Ż/%J!;# q-S@|() y1e#mQ\$%JtiRxAy?1s}D1ޞ@wCyDzRˆ#!ؖ/(Qd\%V '4fQlQQÊOF_FGJ 0GoR qnyG%şZZ,QGDl545ą IoA` D amvѠ;fKFV B5'5{\wD=oQ*4TScC"֚˶bpQ @>TIr[_ : ?/p%2Ck-9H*?5Gvgَ!g 4YFI J)jevyшGÖpLdp1L9$[&䖘V'iC(I\6 Yh( Bx83h-K)DS_f/CIJ |OǵGkT7_dWtD @Z$ |qH\\a@coDv[?-Tj.5 Oi2ZUFH DZ]%|//?)"c2`͟0=;  @aT+ ?K(3L 5_#1.xOR@Zdɐo4%s2 z~KRPDv!3OD1;R :0ZWRdEvځqĠD az4|Țө*"&";c$0 ]'m%ΒS[k<6" () 83[ ! v,RXj3bd%  6H@ԣHڀp&h"n/R/D,Y =Ҭ?TN?Es()s VxKCO R\A޷$/cֺ`¿8$-NDIҽ&c ڇ H0 \wpOGAgoj%JaI,S*7omIǭIMZMF"#6º&(QDV{sd!ô!G>:ׁ7w @)`+> @`hD[_Ѵ%JH g˱XK?cH~GTېS& 9-Q^@Jb+DN0 q0Y-@YX$0Zwx}x"0\(Q|Du S]\v9GV HOdIb8F6 \hD`ny$%JL:MP?l;qKWŷ U8u E/^QH@ToK?aZGxR\D"DU4),əmO? 7ΗѵCcև 2*JPb? }aJ18w@`տl#'#qI0IB;0T*u!=?/ZQSKLB3u8  u$EH:o)QkcϘ?bH`1NW܅ H@=xxYՒ$%JdHRֺ?1v(pKKKݳ/dъ\s||p\d@F qJP wisRM? A:ɟTdIGy"H'QIfF" sG*_ iR|K2Q"OLXh'E8?@ Ld`+cR$I9"S cǽu %JLshK1&Bq< DCI1YU>1!H%J$E`_HvD`x$%JdĂ1^K?D!ٌLPq> M7bl'+"0Ԙ$(!GaojHn: ]BD ǤI|sD =g %(Q$d/2򞑲6Y@q@ pn1֐zE_(S<6^W!sr9~O#@bS@iisH+`t%Ji ©UC|Q1H@\ lO,6(Qd!„u&2E}s'˞L8K2PAGB?\њ?¿ @ @ZӤ ݟdh%)(Qb c\MCcB$,o{ו5H!6)L9PTq zjpҕ$D`LNRE#.d}Z""m2n.we RBa"i?ᑀ".Em- D({źPB}"Y) '"I=3'Wm[OIV *D~w: AS~&).mc)]3'W%ЈBD M:FgֺQJtE~1s͋%&~&9!'ζё.@6Kg؍s~,A$ƹ֮mDQhF{{~TD;RK W: ,i\XvB>&$el/H3Uy\"6 qaQEaD`?3{dPhGf" ej,D*c&)OxJux}Z8% aY}^ Ǯ@VY*fb%Ju%οxGJ_X_ڸA2!T Mc$FH"g\ :m8{BT0rU$ʑU#i% ](Q" 2)) 2m cB5g|HWC8fdT:Q2"Jo z:'>vb7Dldj,( }?Ι ,QD\L=N`uBT붌މ.!Qdl$y0 cA^ouΆJx'"e,QDD]B JPy~ײl@(-Hy#ue6mci&6X:gMӇ[(`Q1jSӨ {AkSDQP 7q7{@8V;eh{Y}Ajd"g6ځ I^öa;P-ڥ8J/$=?ty(QTg;宄 *ՠD?ܾpڶmAl%y_HCҨȖ! 8ՋmY(U)PZXU(xOUczyˋOIJQ6\F^H"vfg$}Z뀡)%B٫ODZ%J` F2?G®t ^. #C10Yčsxɖ)lPUz= c$B*` {dD ; ! UW@c^v˺qd"oB4;50nh8 / ĉkp򦧍 bA@  mfXvp.WJ\{_(" >Tq9@G@8f,pp]8r]y ߽=8S.{c̟ ?a\Gcs`d[܂L]jׂv5\'*<|ɩSZEAp49z(`!Pou KZT⣂Jk ٻ\A3dgN,s@,BH/#93#&4TK*O7\Yq=9x(9JdCp!'dEJB*HA~oGp;n-(هM~RF)o7|ė(;`3O-!K `$ď0Qb@[2!vxx$h>N O>gL;7<$H_4e"n4 BnAް7>`MΜ\{@ ͸7A`D)>g_BӨUO2#U(X0^"H898&.D%I$= D\`@ܹ>V e2ɿ|{>~ OQC#yO`p _b 1h"mIC e :E${%Mf6='DŽ"ijv>\fz8* &p*پ ƐEyFsx#fd[AqNZ!Ip</ 側޻x$'{ iGkpvQRlboeo[@\X>@LJ} )/\L 'Ԭ/׋vj8 ./2#疎>Mn[iLOęCEDǃڔej_bct(?6ӳ |J~+%)#ͳ#=Z[oOO^7IO"|^ٺ){:Ԝo[\;} Gh[~@|'+=(n[$B6t>>By6~ D;Q|K`{sg" 0Ě ?qݖ$=󟋤@g|X!ZQX]p"=2UOlP{ޫnxJaT H2%DV6C"iF!"5qf! @cHPw7B[BgJJ|ޔ`B!fA3XGf>dǷUO[!f@[RAJ0@XFcYH{9GF"Pi)X2~vܷ?pɉ?.!c ѱ68Yt`/|5=t7C,g*iJu͈Vj⟺w9!uPzYXb Vq@xM[ryZ yGp )'uǞ) 4g㨀D8 o$@O0<)!pC)O}٣ZPR+iMMzUT]T@F$OX g$Ujmxʔ-'k[Ve:[~{l{^oc9}re2X\u/Zb 9ɱ%ȅ~Ƙ' "\ 3WFL)8Q%J(&@>/GT5(}vcߪ%!롆)Wƨ)=Њg7;̶mi}3VR,X KA)?) *&w~B OmsR{_۞wzJKcwmv.kKAA#w711}rR`ff]OnDֶ%=_Am(f @WG#W%&(e|J&ԫff}Q}CZ;0XK=%cV@si }՛)Jy{XpCuT5p"`ff`J}u'rktPP9!Bɀ713C(?O^D|={*$KՀMGd 'ff[fM|G]'d7ÖMP"p#ls4?G|#pM%JZ'} JF}~͗ع9o5C 9 K5n334Es'gdi+/w33czD6]ЕxlAطgteffzrП=~w.:W\ffv~bwY133;CNΐ333 yY NIENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-ubuntu.png000066400000000000000000002105101364015200300273610ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxw$Gy>=q^NsBYILN2`l1$06`dd0l dd@! P #:'ugvgz:MLn=tWꞪzz*bfhhhhhhh,.F +Zhhhhhh@h +Zhhhhhh@h +Zhhhhhh@h +Zhhhhhh@h +Zhhhhhh@X.@7ADW+\4̬8l.Ä+jI_CCCCC#.lxs o?݆S,]8P7[X"~ ~km``2X~  @~$ R/ğE_2,w@5B CoLh%~ 0hucj!:P!/khhhh$ ƒd1@%m>e|YaA/@CCCCװ^ p=|55ԗTo 54444@ r]>5+*ppE@_}*(`dbdaă&c*lg3r˲&2d 1]!Ю[y !jj5gJ\=1=_z?폿q{L2 Q~k1O} Ro8F%컟~A=n\uv2HoKy7EAX3;?w]~y\|/\3T ^+56z.^y/kXg_W<&'* (2FWFЕn[?J7}{߃`bo$TpN GG 7oY;Rz+ߺ%e$TeY n] l۞WW}}li,9JP؞;Lwd@+L#M9C:N&WZEGݽͨT~s_~֧ `j;gӡ{|X~C\z3OX,l [f7Uc0&{ X2<~(;,,CsA G^=20 sv6MOP`jDG[oBCC#!De$< 8x^gpG3pG~*P0\逮 _j~+2&P =/~zm`fb@~RD.D$}Hfx+}__B 5@WtvM9cT`4>ٻ_uk&rG/ʧI_CC#b *xb7.ydj%샚o@e-pJގn uE{uæ̯.#Cx l`7.!$k5B˳vǸ$ }\߀l"@3-!PoCpGw?nuȟh\:Z*`'Ζtb_'QY(i29P6Xaϡk~O={/R 5@UeŽ2Vjp{^#ca-5WLdgj2\]H^C#:zyZes0E@Ai?ςz0\"֎jf&ff)Qq'W!cbBʮ;hp&w t"HbZ?ʵw63ɇlyڑpP+"cPW_0 l&1w_F~w&D#h\lˉFkzM r1l&[>`cJ2uIXTUo޷n^]8lˆ6llWQL3O)&{ 午=Bk3w ؖkp8lˆy̴ԣ}mC#'<%5dHT//~_uK@߰c ӿ3 if&` kɠs O$VHF,.2&[3bѭ׍8aLA6Y[.'G'J81FUo~ɶTGՑ"#|IZɍ-5yџn~"H~5 A@Y#H ؏- –""O>8 Q(njP,c:k"s>ʢ*B$d A\la"^iAdD34gS{}$ԯץ 4@ r mz`ડxc_^ NlH?bꘚ54:GvuW it>{׎[5|P: @NuqL2h0RȏI*tE7? ⏛)GL$B+A@]8(lC2:~ vdQ\N ˷i $~q,Q*xP>;*{bzWU|Z[S NgԐ>F4}IGaT;|/J\粶.-Au CQKI7Qe,{M&|LR!Ab{H˱&o Α`{ -h[#:oS.y,Zx GU ~/·dەpfta@H70H,!X}$s#4=tNbl*Ʋ{LPJ*.wd@gfC:vp)ϵPL %*dz_cY-m9$4De4 m ◌g:NN+tGHd]~ 0"@8y˦ix.z[HV$jUl„*kv5A?Qdd9tXHiHNĵ_p VONo1{.TvBQtc3H:JWy-BNj*CCCC0VEE({Ys!0"崠r|iV0Vz;ߖvHl?{.)G.b9$hóu{Zt @9MAqBq%P-NXtYpUjO}p9+Tw>ޅ5-??\-qntT]6{F?nI)B~ /0}|Gm]bDz PNԁ5gj)8(Ra@Dx(PΏZFEW?'1RIjh :9n#L9%L pJ )6D4GF7rC(@USe#[Hz޿f?!O|I^C?[x=ugtA*=*?ɑ2ƀQbEPDt@%8JޒQ%=z8KNjyS;B(~[&wSD)*F Е~=n%0% :5Շhh_ş C}{O#Gy$?F5Ҁ~CQvHj 1* Z4 ! ɫ" &§gQ2 N-zˊEqI?-F*%acKTB/$@@ր tW XRv* %G%8 S>F!@Ga@|k@E@jf=n "9!NGIBp@Lt(HJ t(@*(" 驀* V7\S]e `)"Ռ 1n:Bc?D0+EOb !d ަpp9-Gh nHb"RT7u(H8!EThh -U9$D"f! u*WYZ*/0V .I~wdZ!cq@D! te6_R/dy?*Gr >_j$DDHD3ti]D:&B?$Ì,[%YDy.iyFt؏V_P1ͷ#%7O0ր|}D@i T@"mɣ[CIr }+>B"L C)=h?!RQ 44 _ M-m~\R뼺O-}IS@15*!g % Qc:r o3X!?2%p=Tة ރ؂a fX& ,"d wH+bU4Oy^@C)I@S)5z'B@a `ٽ)"]p%@@o_IX )+#I?BM *M&~T2yO&QUr\I :OyT !b ?k@)h" Cf#}I%֗JM y?;0E+ 8 m;_%h;m Mghͨ:@r}"'$d@ 1&(BY'U#)kBЫVD8?GФ9.W  0ɉim Κ~i`BaIQnw| (րF~>~B2" ^  ?l::!|MCU0u긫Fk9l3Y8kbw@Ìy!B2@ /ZF#hiB L[cYB0`@~@(LW}T 1f3EeP( ap}R RY"t.i.RAJ@U6y϶S'aMc(JC`,آujf`&PqipuEn|2&);6$e'a6]Nnۅʾ],*˿` *9aݎiO?G/5X k@ QKD(  IS@Nv¼-|8L}3^WFqթ o_jWU `# . Tr\ B`)ҟa@D!@:+ړKeB'v)Asw_1!(/] Q(V?E TFᏕz*P p(qqbo9omGѩ̣7]+~Ind11Od3,5DXF(BwG>%B~"GV? K *@X*jrb6UjUB':qPB&G\"C hƼ~,3&]ցH596pӌwΞLyѪcZ8ͶRkreFi~y-veŗRI*^hZUĭ P=ÅFOBwi,2II,L$/-nZWdvF Xn`93>_)kNLȿ ?dn};=U&V636޾Z:rvۧHC6m*Cns Y-71t+k0.8 ^H#%ojmDcg IDAT? ◐~$FdG0jͤ)96)Ok'BW^yk(J$v!1]q7<Ð?Hx6Fxrj4wB}*h1Nx=BJ@7/ Ĥ*p/KFcsz.~ПT+w v :xGn ۜ@E>s=a7>j&b)_K4{5~|{O !H1~NSvs8ݟ>AG)~\pfrHuUqҷHD-r5Bڔ+D/ Lei% e _ᣓ$X\?i/  ]c ̐z v翁"fio5eQy,Ƙ @ܿ5%R!$Vċү%7e<3P$aC Yִwr LEZ@SjKաo 2,8BjuB٪@8o67pֲi~ś?Ï|,8'o**o\d-"&?7t%{ϖNӣ߼_zCt?QÂ:uğ"N'F:G;{=b;%jm[իq>+ @p1*(?%Qr\B]hㆭ 7]zIt~jBn.ٌٚC[ӡWɂoifW$?PҊm N%7o?zE,zjH@hj+iSK@OMFԈFq_V `@G^\&'`Lpn\g3Wmk1Uq 󊷘Dˎi3^aqޒCk VKl`8㓁J;8wHMZ[ |SKwMC<>iH4B=t})zAqLeJ҈}4'Px=%"(Ǡ=֩/ۮ`$ "؂6iBpU֗ȓhqg,ּ_T1Uqh"o&`,g̰E8ce)Q.bc ꠽fOzz5F~ޒӋ/my@ $u'0&FYѿ}W))ԥP|ESw+ٌLU``&Pj욤kb jsU㚱=jZc/x[;-3dFq_o Hk I7w\7J6EGk m|/_Y_ٞ6b3-^/{*t`@W0kL%|eO~`vY]r:9(U!`3K īJ%P00[swӌ'=FqrQ0 r'vEW+ I0"@'p>3/:+%L AQit;xE5 LŸm m8 T(H=D(LDl[;z_.>f&@:@mo\ W}탻egmV"E@_!\f]Y}b_/jvQF=#R8h4` w;[Z/ S\2=a1ۨ7} jn`&SWc?F7m]QjSb1K={XLsY}¶\R6_\\* h["vOO|o4⫞n:0EHhRٝnw|HĿg_0P'|wK; C =b]AiDe$K +Ll$i~aAmJ|,.ftҹ+rVaG~˶`ی!58Tٖ5h\W/T%dG|#"Mg:CC{ Ғ($#i<B-68kqCp+'LkIpt w%#@m D"BtW4`h- sVO>}onT4M*&ln0t$& e ! O(:G;? s6h:l}#AV:3e1fk:`w[ג-0[8[1Uu0[2l Y12/XjUX)^l:nd5 [tQ ]r{n6v?c h HlhYI@e2H+􁥥zU!!p ⸣^ BF0-C**biaI+ʍlJ#2:JL1? όL;"[ciI-1ۂ cK n49 B 9FK#| ~dR^$Pe􃝬p &:WdaG`G*,!S!8 08DVafqedXKspfa.)))ͱ(^04J;w03%59x?*6p< 5FKXz-U4 Fhe[mqE$48уiB- 9'ѿr t ;$-*UH hhT|0No:22~F6;' ;wpa#w>̵ș%ENc9ax.EYq3`*UPO^,- CnGSW"#HItLMQ~}ʢ*mhG&FjujsXxa.oh]}P_Ne#ViiG o՗/:pɾ|`\gr+zJ =Щ 4@z< /zVc&Ìe{M!JI7G5 Btfڷ9H*w0 ?7l%2SD#"3<1;HZӍ q[+pN ,2C#=,qPX{at5XWne{k@ @D)binrA{cF?plN_JnO3=qQ MMc`oݷ8> rk6qg6&9 ͱ+rglis&{ bz&S" j^n̨ڌg$'%8!U[[4(%8mmVe%!KfAd`YqN4-M+{@|36D[X:gi5AǞmL{&w`R203zÍlӪŹ&"!jm:xOXdh=/5x)?Jv!, a S@w >.I#' B t,CGse~^(z \#b:,Z{Fnl"ZVa۷OUZ[m[g{%dCef~u>.1LɑsyNr|`͈,01'~AFo!~>o6C\Dώ1g;Xs)dfʻwpEθ#W߅Y(H.|*3S/GHEq\bl9mI.^J ",)RVDe NG~3)/2<# ";5C\8\ߋ{vfFLIA?$dͩIq~>1ЎP($]/}708\ @a N<ȐY5¢0^~&- c%ٽ`t4J<y$#KL2+]/9VZWQ3  NgjH'/ 3g r vjqW-٨'H;0)Ds'+@mL_qs5VnB" l!_*xnU] 'R+Òh](azܴk Ɯ-.amL=e{{g*JhDʟi"o9vV^?ț~_ܗ>_7`4k Ӳڧ$ϒ%eteH n a;%VuSes*3DpoBmID3&!y=bIُLQO5ۏ^|eG /a׺Zer$#Pr$c`fÁ:,ނ`_ņ-f+fg-}V `ى}S˚|c.xXlAYaY2&_}GYᶟ~wy+o}!YR 줍'uIRg_ )*HGP͑vz8Wa`,s tUꚙ-C*ib_w?TAaѦݝmf؂3 5V?/+Q7p> aM`_3#}H`ݯ[}mBe&sR1-9[#S2ّU=H"C}O@j%k$LW~v앵&8o2w\KP0u=w݂S|* f^k tdJ 'cw w'&˔w)RӾ, Wpw r:SJg8ec>+`REa0ly7d䆗ÿ/f.w m{s e Bh_wzس!|u%SD1hO&^cM8lZůU6 :F&۲N|^sf*]\gAr&{DơNگ].>GgԈ^raf`fVI6[]ψv?.gX[Hs!uj?M~;M\"N;_E `j |~࿚[xFۛώWݜMmkВy32VՇ[JW~ kqȳ^a]Ȭ1 O3[[_Y,{40ov(f$@ Eٮ+K/]8V㼏~Z}QiQP/(f u}}7GWzM53UAk2&a}ai4\-ŷ^ noB\|/Vt"¦as} '"gM; tF@@_ѾG2:\ B{0)#b |M4[ 5a-94zS͡ ‰>ôr(ۂj"Rydz&aCZ<{X ,F8d8]df[@ѤVMșk ۷;V^(~yŗZkSi,=gmvizTF{'NI~d)~Z~@tE<+Dя2vcQ`M[J̋/33Pl؟  A,R0߿z* A>(:0]uh$kJ70dq32W.+>.E/uk[՚ЀUyXyiWziXOC\h *l!)9=W VU{ޟH>Ѧ6oUmO>U-%[`<O8?/·UqCBg\ɚ-;N6@ j^ PHXsA #`?eFpG*w IDAT-6Io>?V5, '@(قN$׳&ama8W~sR]po6w:Td lΰw4ݷg_;|D,ãwWmE9v5ΐt?# HE+^;'sUǠ И8Xsty/4`o3烜IțSk 7ek멀AqstWvأ!o[ƫŗiΏRQU{;oxO1c`ۈŲAh伄 !'3"Qh!@ɑ6tP% fk lҙssyq^f#?63rfA?/p΃θ2, co١mo F3,AVگ sm}#﮾eNyF3g!Ɂ?8U福/NV&:| rC &qeLgFpk ;G`D7@20QwrlW~B4nJgRØșm;zF2qwUg?$dTN 4P0 /X}4gy]DAL)rŇg,[?&[i+"6῭ӿW_lAaƜl Q粼?7 'N zҠ]p[L=ǿeż͘ G}G F"f T 2|LyA%WĿx1{+0n3fv}ɼœy` [0j qp@2QO#w}hW[. >y/ȏM,麏3!JZHU0laYٳk`7G鿎 dP:nW }umFIq^f*?T: kE{Tً'T{Mš;x`d:m4KDɲT0CW>VM^[x"gX@4"5 L-n'عO?vuB l8T:{JXޖj~chnhX'Fϧ$}D@OVgzoaT`wr&񪂱t; 6|mzS-{(i],[2w7|)h+>NCzUtɛ<52풐3 ʛ<3ج!x:][O|!_`l}sC `ƨ#{n75h1@1Ҵ0c&Akq>H^׽|_ٵ9یb]Q3l(f*)qc/O<6؍?7^#foN0+`s "MIșVZZssh1md= 7yw;Xy{3YCzD-e#hz-kjI]΄,4̑`3=AFs90%Z6nGlUP49Si{֜#cYU[V0si;n]7]#n˻`-Na:";n!kL1%L6c2֟\cѧPc0 xץ2{ߞ'hMn˘҅G2C@' F@uCw&H1f~̱-!xګxݷtաU~* e 6=s G0n'|u?xj*L k<yWSnFZ.=mpŪî˩ LLrGxW჎GCd){~lg]|[)|`F3g @3Zk,+h00ɸ-J3Z׬*8 A"cӟZ#2pʅ?xK0!7!YTZAXWyw.[qO2s@#88ċ/QFy.LBu.NBunտ3 dFV̑qʌ#;>]۫J>g`\rhPa̓4A(O{~>GPYZi<* P]p""2r!Gi-ش?&6G\EP`hF:u7|ފ[?V(@^2M` FY0Q?ӫn=WÊ/4$fxZ jy7#3jG`|!tk57_gW}\a,c.[ 6 Y@}oiq90WmؔHyp ~w }a8N-,0z-VoڟVoڂB[<: Sw%sΧGoxx`L802&CZq2d@Sb, =:ծЊh>˂H̋ Ƭ-__u5AV3Ɂ!76c_vן *z[YCx)ЮTp.ĿI\$

"XqP+#?Zb*AO{[@~{o}Sk-U?xuo"+ZH}o}#b)g1ij'|t= meB?,o|e֣OpO~oY`1g̸{!+MEjȚ,ɜ]q@\7VHP4C-~!o$~Vls5 +.!ݏt@1ir1]5gLe-Zb M>n-ۇno}+ s:G R\ ?`^LEt-ӿf&` *gBnl01y_u* g& XpM/r`V g,ِ= [$ꊡXl 2L=(~ ~pSKOzNDvNB~CLUZSh^Yq&I4FΎbV>m)qA/H4b'OSjE.y3ڛؚŹ>k͙0dY>t< [*gv3 r&6 Y|X[-#ț0C]7W}?+b, +G쯌jm}/{hIP#G!qk#%.e3P[H3~gU8t1w0[mXU0->Dpa Io:?*+qY 6H`TEE5qs/Jqox ʁRS̘ eSW.@/6 '觹#\ߡR~倡4Wrc_~Ob7'㢬&2 5i[ o;&-A 6 '$L0 a,-0JIϜp-YC[ $פ$4 fv{޹}/3X{:/T$%y,,&&1dA c te?~۪7z=Ÿۂ@R_`ܵx]5i NH#CxJ^פ,ZyG`sFBVq5W_sv˿;$%mp̟]ZM7r_nYi 5$`!O^Ѿ K}=\Ѹ1SgÏ_uykI1c|yG #F 2Rk^#6md-$lۼ߈ͺP·>.wQ Q!f/Z4T&lp@h]XrήcݵjijEԵzŌJ=l5sMWB?0AXND2.&yKhg'Bk# Ԣ!*VT)x!YEد\K7\r]+hqK_+2kփ1b Y+ R5h,d}լ?c&X >Vb;캴i |4=WI<]k&6a~ڴ-9-TKcuic)W62P caw&R1 UqW`"r0br#Ybd `XW}o(8!g۬wNf] i;8@8K~XΣoa3cK.1S mX_h[_t9,#yk%)?p3h2B)x gRiɓi#@Zx" - [`ܕHI"T9T[xJbXG,2]/;H8pմRѧ?v[U& 2&^*X}-W ݵh/HD8IC[.ᇬepEn>~_ö_VoO(L"d.U?JdY/BX!S02)&-yܵjc4(oy'?{y%uXqHa/SLf5!DgqĦI%u$tYhUTꐸgWykyJ+՝*,7#^񏕄䟙1SQTSlJч@JFm1W"o rH"dSe$AF:mJkӒG%pãCxz趛j3kc_)Rq"nU3P-G5ÊME>{f:Ĉ^;2lX1:Mrcg~.P1x HF'B~{yod}]W"cѢ3&"ԇvR,E B ){g,ڌ#Ƃ Y7圈aiv_x=$Kky j&z@IמRP+oJu=Y{yeg: IDAT0խ0Cq1ٯ|j=,lq.sնb[ 6z]GҖ#uDѢPknX04U lU- t+$B.YK`4F`mFpJ]Rp8rW)+}G*F*]=JPM<"t3Ʋ.ܢTk۹Xkv3_@?p%w?>U84% ]j3O1a" LDu#>Bo)PK^":%൹$x{*LJLG;؜Gb27?U'V݊ZRٖLCUd.M4$."d% CGpxWG9F w_&6],KByk_Rv8# D ?"җK ̃- @ ;2Lm-iiCE"`{g&3߯~Y+AjagrR@0cW EH K`ymjMys vA]OP' [h z[R o?%CQJtV - @ΒH v:80Jl֘Qs]Uw%+?1uE_] +#@F6K{H,MK&$NިUW]CdqG}KP ¦!DBt+]M!% < !en!"$$v ))&-jHBK.oXsi77I 3$F̷! |"_V_0 R0'vm89 /K?f/G d "FV;}C([ mĨ+`Yl"I[2ܪ]._^j$6,_$ Rʬ./I !y. jE;b# ?3 Qmǰ#jSR`͖FV0(O+,=%2ӧ[=22$#v_CyW҂$BTY# 2F `ݢj `E|߬1a˙7ȴ! `$%pX];g0=.3PVݿLv*xYc#ٍ/MAiv>l7Ca̕Zݴcڎ lAp%!U=l1O軝'NP;gVhgNFB1,쟷9fw^z%GccOzY <ŤpѺO|H [|7O<(fDTWjR`|-!M3,"IIy)Th:l"ngp8%L0dK#䔥ݬ>p `s洘9]U [zcj 64E\?8H@h.giLݿ-g|P6%z Fna"eE΁pǛwRn+ \r`!말 =v]O˒m[xjWK7 у-<d!.v m`~^ڵaѺК|֟<tЪRWQm ¦Ťٮ[0XrОCQvq$a׸2DsC6mW\:5{acN^fu A9ল6K5<x3}$X+4,n9o/Xu ~6Kn Ҋ cۆ :$r{C5[B?$@'q$asYbsG߫ze־h 81`&c$X$ .ePfy]Y_2\[T*XYg*Tox1ofV6&MAk25Cwo! U.!JB(8Vc 0U 0)j>hOI9gު"yW~zÌ:)+Ҁ~Y-RaIdPp\ӆ s%F:}{筃3 S5Bg0 ~mpӿ!"P`p30}/O6]|~sݯBF`c ezRP36r֪Jߥ|/tKK3QcF#8eM !B* WYK@l3J¨иwοǁu}ĺ9/`X(A l Ycw?ݜ(z 5`ghXMՏ`!?c'RjlE9xHcٜ#8er`! ݆9baXd-B4Qy[F Y\c.>t_Ϭ*j ϭS}}DQIx_Ct:2)3|e },CuD!(Цg{W~FYӖA)|+FbUbLy[Drr%acW)(xJ9uԯgs "?ap%A{滮_ ow!dU` W0΍:ukϯJ{H<uȇS"Z1#VE+7+)t%h_DqoR@Kx|Ee | Ӝ4SQ4UVhg)*!(iDx5`CzŧYЉ 0^&ĿX  a]I(8Q@I2+tss6z"3kZhgLUg\ЬgPG7[QОJDsaF[ew̸cVs{vuR#w¿B7q7jJPpТ!_),XeҝYz{G"W]W\ 7pa蟳䱠a9Ot ' V Pwm{;wlEyV~.V. _/k<Nx sĖcOvg4])ĄP 됱 AB~(pO%*لR@=D❕*ǟ:H!;ß>mԞ)FҸ6vǸ$кht4$c.Xį3f}E32#&N8MLyIm{}vMݼ,Zxm?WLG.g:QX^ VTƏYl<'oz=;֤$Z7ˆfGA &?v$!P gk k[r[)_Y?~}|N`U bEIF b53gƿn'Fnlu%O'H`9v1Z źO1(3_$&O8Mr-_bpYb)gA~MԎ.S~q&r}M =gC\&ܸ'nB95E߳e#(CĎ^9Y!!@@F %#S0l 3²0q0W +z#ax}S 0[Ly˖{n%p xaW&*ņWo~e p c8'U8l 0p%D޻| ?uRa{[/.^ nYKڗncʡ?p%i:adG[D_8)Kqw>k>t:m J3=Q& )(qS Nr-6!L&+y T c?{O18y:_ӄ_&q?fdju&"0En:࢏)PЍ>Gn>r`K2ȬGr+kϥQ[3ok"o7kc`1qQMQJ#Qє1P~~^!N$`uYKe}&gRyk罽7agiNRjF]6dK3ӸoWJ+W%4;;`T7g .?-I+$ EWvR5q%PjHpgh+.>3`ȟ$ˮrX0@۫]O'ӾBg?2~ J7,p1[U%a•0b {59_B Hi2UO c {`o;Ejdӆ7']@{ĀRTEiY `G'drx?-\w\!CzH*:`D 'g%^9޽qg0D-%Q5i3}9J$i;+9nIiDhr`VrPPrc +}WZ{رt_ ߉ZLU(cŭφ*Kz9Ѯr$j ã @EυFe3(8}A~MS>4hX}t=EmڙUu9W;wU!Gf`W)@6ۡ }Uq +Gン+ssYwJD+_c_GW$55c%g fxZz[/i;s `_ʰ*/>=?BYNbDD9z@G10G7`ߓN~\l9%ph9K@>m1p/~٤,@Z Rӟ´ϭ:xw @ [b/ 1KHBn1S1P6lR/ 8QZ85#g*„6˭?N#@/&c0=48 loOx6LL?j~O,pГiU0OT0P̘F<̗jIњjHЙ"zW W2p p%?CΉw9R/z6茇Ga;RHiBlH !f;4kg5-fk#xՕes_Ra8#^M8Ow8̕M[ @8~yC"fևFgc`A/>R SK":5@ZLmۊ{QD@N@o8iIymb5S\ XI[wk<;锝 0)R{t-L=?ISmn.xS"ݔu[aouAlF$R c̎mvO7:W1mW Q׏F1S 6rƪ{'Z)I+cMO=r?mϤ7b` }|EkQwGQ}uu;#1 L@ еJi]i"п;IpS}O:cՒ}O>+9)k1$A 5˜+'{]djf0;Y6CtVuW$M/-*#_=&ajVFݦfVI(H9BcK0DHv~H֬W9rS t/;6:XZ/4%vbp #bmA!P'^oUX-8nR}IL:A[D%B 0 ~""Au:9n?{&?z3O1FSl` ec|aI=OkjRGgO>XG*F~E5-h`1kӠIsf^b?/$''0+rPiXVܑq p͊BޖHI`k6c lx l@i.R$IohX1@-A [cyڢ0% <Hy4_DD(-SdD;B0</?4d& 0B;0l0pJd+ςΈ+s`EnIr$C *fGF׈$Lis2~]]|DYV6KeBv}:~@&]) 2諦e&/Z*5[)`Tffs<;7V-_6=whݳ XDZ)#%,zqR2@ۚ9$ ,e$d9:XS~l[fbZ;oO/mSbWFzd&Z}d7Fhl\!8KTPQ? Cz@BJ`lTf1?c$:CAܮc">F'$@MZ5C@ PYgE镩X JHn֨Į]Ctn1d,dJpJ `*uJQGK H`&G(n1C @ ɦ,`lk rkR7[ƛ.8iҖW4B-";e+ᷮ‚ˤ~.nv5:|W%Zm*-XB{@떋h?ЌJ*-Js[Ԛ0on;)t>B5kJC2 9U66MSXǕwE0Kp- +M.G{UpZxA~fǓo$0c[,`6Sxp3QCݒdTt:CҖ~#A=¯ L$Fb I,iL61ڶuY$LoPLXބT.> ͢XlA\ j BbS@W υ",Y[4EnY0r$HcST,;"hUhi @ {?Ր+^V* !RN:b'鿹| Ș"<- zuYD%6+R՝)+ lvܶ> vjIzc$oSB"j8;&,N*ȵ}# mP gFP5K%/H#7XyvOw^I41bJg9ɕ@Q)PF>e՗FLkӍn< ^qV&)3&ףU1jC/ l0St~Vbˢf>|QDӑW+s,oő6Ҕ4A pAIfLf\zQž'o T?J`lA+U5-tPwo0q]E_@I1f?Bg*QQ9gy5@!kr Xztѣc{6čư"PmpMreo{j s{mts'_q5pQњ~m/+7DbQ ؘ`X10)a_rrBP.oF3h8oT (]hN9EwZuz- Pt_/bW15W_.[u-}N<-07TLv$RC aO`W%_{@_z1']Qڧtml0bSCsDUƸ0]W_Ro3^Ia&4m" ?3-M4Pc 4+ux(W (TQLN6'ƚ"Oj69c&WҴ]P`yە;2޳~{If7] s("v[jcɏ` x@w݂0\jrW09[]ynjwWDl}> )S]htӖD1@NTiZŃb0`c8i8q7޻>X6jNк ~m!Ubo B뭺@)`vN|7 U0!(Lf `]Jb0z&r1똓<G_B1X#2[^wj"|G`k t[,y? @abtvnoc[s·K@uJ{!CZ33.>i%r1r n@oh޹/_MQ+FD&rw eRf֍[n=G@3~V4%z앞7[_k/\!0!>U{Wt$+CX};vH?zJ hfC E~07ɃDo*l ` 7"c$!.BTKi(=ay[ [3c}_h3n7g~cIVʩ v_>P~M+]Eac c @8}I' Bk?[a\BC4 uvh#+` 4y.Wz^/_|ӽ0:GkSQ׿El ”Kz|O;͉g7*._b&C 6HpTM:f=im<Z ؕdSRcwR-N/Jsi |F󃪡mՃ@yvU_d$B>.o9=fWmE,a^*ZO2<2O.s#Wօ n~Y}3GnF53@kb%[G\Ϗ `z[֍E:4p`͍߬AX)\Ke׵W(Ng:74r-_g-1/\J׿5bb?xCKp_WcPGﺝ݈\["MtVzPwfЃ^J8[w h 9;".jv]|K7]>7kk{̑1)ry¶u&²y/wT`_6XUR>t6 9_c뽏;k7h<~P^IK-mb\ɂip_عZm  -́#6흿Ŀ99Qw?䏿Ejc7DؘY-$$l9~+S.c˙;7A:5@@5˄PqA/xU÷Žfo B n#+EDڐ"P0!26k,zүq?SO_{WWn[t wX1%ssU;] 6cӎK >C5@ôTLdžv?x[P]4tJox?)9 `Kp#wv#Ƣ4=K/ R7WzM<;~=G˟fYF w8%(2 mAo;Գ~;?Cgf"@7n~AO&lC~ a" <| 5Zwli.֘ftG)IElݕ[s &\*B<A8og}Vf|ǿ>r9BRX'KD'F5pGzr>Pfڞ/Ixmf"PPctvx@nLD!6٬%J요D:r~~88w:3`7lwE_;oN oFӚs>SbnMkZ iK/,54L*j:(%+$gy|#"Lŀy( o -ҫ>;Pa| `auԸ3%GscOyYQ'o>sG S 4) %`1p(¥ KphbT-D'd'sHHd,ŒoaB=G\kHKB*m0+vum _._lJ1oɥX t_I]M.G@09YUQ [šCă֟-x~耳^j:Ϋ/c y _>!$5"c՝~oP.5)c s-قx60 ۂץf_QgͰ8uiSƮ1E"(n'3`X.NƳqe,!Y4MܤBմZ׈;-)(3&BQ Wr;݂$ 'Oק,<j Bpow{*Credr8In! #0n\`f|@#cj" {oi=۶wކ]' t`OdE| Gu/zw$0K[fh+ RM\)kr wo.(ގBAx Զj9`HP6g`ӡGWQLBxVL#,ӸA6?:\(HW.~")V8¨# p%!m l[bܕLIIGMH[EsC"ƈ#دqO.Q7?r@iwuUg}!z š 9OD _U[ؙ];m7!o _uFH=pehЖ'M*|Iq` )Iu[E/o^%!R_UW_25lzh^*fU@ɴd`VarAUx V0U 6r&׬*"f0 MII d,>qOP=Vv~tHD!x%Ra̕sg-b!,Z\#H|-o9ڱ"uMNM>bcf=Yۍ{,D+S~8o9ʂ|_(,Z>̍($%t,{ %;MFkVH Eĸ#8k.0qV&wˮX8<?%TϊAm0dzy?<];'{ʽu2D-M:% !!DXDZY:hD&-C%PpD9qG#y+yܑuBBVh8hU:Q-Y[`?rUNgah,_fڈ!utJu)`w9/{-WJf"B^㶒2A`]\+7Yi#~#:{CcDX/@1 ?jgSΠ_󗢢TrC&`\?+-g{ùy1}Y)QϔXs;t8;:vJ$V[ftV[v(p& ޴F1bz#?q9NB*LRL!aĿ0Ts³}`Um_No!Ak0Gm;Y'-XUw)0"rVOt{p31`Swk~C,fj8R$"ϑƈSAgTEQWZb?{6^J5-eU|>#֩ΎE\ҺK>+.vE` m2]zcA+E(8Kģ]tQCGݩpBH #b_Dz& 0=f30]Ml0ƝʊF6@}࿄zV~u2Юڸ̪J'cXd HI ruc0ɆyZjSh@+k;|5_lUMk.=W;: g!^L;`LYr\wbLX ,+r( 7WQms'~{m"gcmʇҩ_ > g0w.n[@J v$lZտE&n`k&t XR_|+W* 3_g-Pn0y0pQM3߬W ,XY%Qm?gFHk-a+-WHS?&XV(hԖ0s~ywW}x> S^@苺̍ )}߀{x8_>-L''l}^~U񿥑1uB<<\Һn0ˉY @GIhUo"7hR"`*z)'?Mv2Y_׌/f֮Vc3[P }9Qi/>|!;\gV KhzV%"襈f,b]s^k+_֬r o i2Lk܀ aדz[:7CT;Kz&!u f6TcS(-DI10+ذyСx=jkF<_Ϋ5$1\2(Ϊɤs `w\5}_E  8M'<7ۍ=Rck߰R@!N煮7:AH IծC):=cp(Z01y@on*&OTe3c6~i[u=lApt.UݻD^qzCvCϘ wǬPUjrZ ?#{7O:?'uWto|wtW9Ndmz#nM=(Jnu~yT}4Ū"@g0jQW:Zb48,JyAV(*cP rȪ:|3ij?}9ya)xu/~D ZN ;*65ddeL4{vP惍ֻեE\/h{,;~Q4@Ua`27d uXv\ ^GAy|?!B)VtE폿V~?C!~?<'-cz>Jp|MgnE ι*\ØE3.;l>ƭ0RNQq5aWRYMi71k_cRUisH=Dhmu؃3O~`fŏ3C-7"yֿs/cy"s:}d@ 3B̀z$Gd-8LُjCbép|b5b]a+O?6#,y}mgbh"wmNL_S}UUEu d)^V}k$Cp/tƺ#o?ƈ2i*B;MrJ'A=dL[ -0:Gwǽ8a ؑM!TH]U{10k.!isA5p2> XrX;,V댚{>R\-"5PQ y#>u_Fd8~Z} h5&ۿ-|k x"f9Om>ȹ40hfN.cI B "%!0 Z>olU; g)_=Sixh̦ӗ"%6r1 5+n٣h$a$6G?v }u9K{Qzx?n 0N[A qVQQP +.~ߘ`}{>CJ$,6ήoKRڃ3Oٖꐷwx .b%?H \Ȁ6l,BJ2.XMe[gP]1>qDG=jAؖvKe((_4lu+.:*i,_ ,-ߊJoXE`;v 0.mABT'vAEI`Zx_Uby_(?)ϤR!>iMx̰ĂtJȶ 0ƲÐKBAԎkս1rkkv/xȍ ̌5e_jQn΅?Afh2<ϰzȠ)cՕq:런[PLVh0য় Q3je`_IL{*:X~nsBMQTKX~\6oFoKaW:{L2yhۿt;OПuOž7ibd Үp0U//{C \<)4>n|叿πf[JP(s #4߅n)jJX޶` M΂NL w۟bYU{AOF{Ï>v*/&l )֢o%R ?" k0I^ F`-kg[km+_N u&/`Θݵ~o}滋/gyЦuP)H ;$ +&ꘁ=}W}vWz+)fԀhۙPwCCicATW}B>!OA?f TVAcRx-i]w܊~?ŧ~"5q:qU-tAj ⠳ԃ ?8:9\z yJsm+u WvLRtl(*?4uiG`|Ӂ+$p~/7m_mic<|Piy:{Ώu/ckG#^6RӁM!0eKt8 ŷ:l>GzaDWjZOX b]0e'j>l!86"mtǒeG20g8^(Î!,'t-^x>_|бJ;>.,.&`8')f]Cul88(N + >k :7%G @*Y 6aYK`S⹚C2N? -c+S15o-٣/ uA!/zm&oI;99̊g>w\ubiQ>3w1ˬ||Hq x󂐗H`a4AEnns?ʼ}7cEiE'i2#B` `A_R3dva9;,?9P(V_sn^u(ߘNGدpϟo7\k qm_kMQ sBىNS5{ ,>A#O CL4lA{!pݱW3/7y۵l4t@UBT#ƞ{io~Z^o^T\3T8&/ `=Dzo' 1&W,'FcSaA Yd 6Ź>CyIuK^kl6Z ?1߇s?/T}!SÂaa,+EBXx##bm7c}w쮝۵cn.s{vavvR('P@qbO#¦#Ɩ#Tڳ~Wٶl鼧/qݿdQ`b\UXe .!)6X| `/}'b*'Q68@pO;6zD_g~˖Wտpd&zXş/5t/qeو'v\+wP8ё~"-V]^WN~q-kAHz?%(Z[i?aB6Ia14+$AfBBh `%)PSd@"s QSnxYv+[~Y= J6${dz$ób` З~n7^Kҋx۵WC9Bٝ;p|%쏼7?<Ϡc t]+7Oyok4`xJ>ث_pw\qy 4`.( [s=ق0<[U,5!׿D cQ<7>/XT.f_Xt!=B7vԪ'g_<?݀{tO#Ɠ_OЧ?e}7\raoJ[vv^cO¯"b: h3Rڎ7NcK ]Ȅ? ao AOl2^^Ovi5E[1J-zm| NߞmߜxE{qϟoķy2~/ssŏonshYcL .eݿ .R WXdוxK(eŬ/0%y"'I&s;{c2_y[⋯{zsW]֓ `餿${k uf׾uDk'v~I[mJC1PWL:ˎ6߇|>>#p/;#ݖ4%3yvʖv΀?i Aw)krġz>>%hb0صpMuK:5 z3O/RwDyͺ~MZk][9ɠﬦ5m8)϶},;:Ӎ%P{ IDATAӐ1N-FI >4W-*R1om~-M0v}| P:ouf(HJAR2SvK5h8l Ix?n b`S sܨZo^6dGn^"5s{Fbƅ_B%U8X+TU(1'g4[„%EGm>lqDQ l(X8ܭwmAt&Y6QzWM_JI@^$C`!6Brɫ^:^RF 6c*)()48>smk_HNX)}(}MJ;iFrT?o!X)˟/<˼#9^o$l(X8r"ǛJ{*m9*`%t _ ugbX92& e$u{bZ΀Pgs| I  `^r8-ps[O~U$ ~o􄸡`+u 9At avķqoR k `m8H" H]ʹb@uZLNb:'y`oUQM1ʶy˨("otvEFOHRʒLj q+L' H@ɍ` 0 (Yjf@Fs'>~s߇}Cm*>5,!a_N~yNpUW5gg ڂ%%)8/ yϩx"_@az=žA}q=Pٳ sŰÌQ5vI[b6k;Rl"GFh C8I4B~l'R'_|i7Q u9\US\eXy v5$`i~_y+}iVw6[m ji܎w}9?ⴷRUKl,ZRk$q^ `  SGk*M<*̬C~z=٥rjw;x]_=yma𲣰0*QPA>)H X8djT翤;N>6Z] X`:$@Nu_4]-0[sRIr;݊$d s=*[0Ws00q o^a3+'|M/ A6m)/E&i9"LXkrkr39q)B~~?,nWϑj]!i.V| FEPu&`_a68.1Kr-ԨYmn|M/x&;%VE9 0ԙSx& Skb!.*Wnm^2 . KƌF|ӻe&~ҳ=Mï-)2BtbҟN-=K4Wia! 0bvrvMuLyQAHI:ʩId& 'dX/u$-ym" ka[S`v̿ ?6ف¿ -A[W8^/OeCAX}kgP3miCQOdog!cpN~̥ 4ˏ_jm4 >>9HƤTNW钀Clwa[ `ҊQ:L2cO~hn ~~ATL؂gr{M~'Dz槿gU@K![14X@ OiL?bNl _^0it^ʈ.;8wU% $$@Gq$0&m8&>eo[yP?S5m)Zƭy}db,Mmg}{'<3 ~ Y<1-m@5h7@lARR)# ">"=}DTWG/vD|gz5/JK sDXl9ڼA%;o"ד6/KB5J8Ye%|M'miWkrsD+Grw3>pxUG)٪{S3a<7|eHP/uy&5D *Fá_%{"ic GEڜ4{Mm#&r5~:eX3xܿ7 k'O73A⺤poAm|/@ۛHH<_+|]Qay_CPP/I*12 5WOz}W)gGdQIEf!/X(Yrki(`=GKhQYQ+˕fVWBP=ϋT!, '3 UqAڇUUI3L&6;p >#، ˨ fQHDN '(i#rJn#{ɫ~l W#wa@OD0e +0=_[;˓x$ *0(@]Q GV3ڬTLr%f®$۰IӍJt(Nޖ Sna!\4e ;es Ku鰧ժ L<]u [ra_q. !OЌϯA O%-y]@K KpP޼$K.ZfQks2v7WӞF>\u 0% }a 08)ϣ53eH;Gd0TSq(c +~ycH/FzsjS?",;k=D@qN1kΉx&m<͋}5w;.F W*(š *Hm[svqSGk;1EY[ /8i1GߥJPu(cɩ#C?e EI[ -0n 2 A) ̴-w`\4 $U*'30VctDΛJD!-D\''˖XiiN6eC8]:B;O}wl5߼1ta_q|˓m{5\î:Ll*`,f?oiO ʃ *ڼ>1 F# IGO8n L$f9_ I]]AU?/|edNe;`pc#c6ϒCƇ@q|].4X@:D¿͎h%gN72O2>LYpZY߶t\Gn&/qӥ/.7b+W?˸?a&؞OR%3?9 irN8- ZA @8J/mvEZNey7_Svo މҭ=qGJ~iuNLlŖv޻sm8CS(㏟ʻږRKW7(Ga&'} a"/8qOm>EGɼorne43GEIg1'"Ai}!C? xWS*üc1AɽG& ˛(a@DmKOKpM01)\w:%_sï6~q؟L{v{¦^^ ݩ}l~ o^QDUd] @h3.{3J]?쐀ASo$߷ϦLKE0z 7447 65C*DKڂų5GG kgk +-c"ML'hSyW?^A>KI3W|襙l]vQ\ E)Ѯr}bP!vX#db&If6ԙ7#73qc R*"juH'BŰrL$/^7@&mI[]lq&,9Ιx#iQbGcG+_-T\o`S[Y⚵7ts(&f5 ^UACF +&$ ZPDel@IyQ0 T)ZrTO)[$BÎxH}~Ց -CD P^ˍL`a"-"[+|#3 )ZfTUt퀷̢(j%AI1C; rX hMV ҁ[w M*%{]ɾ;"rucnԝν |wZq]Kmަ}RN{QQ lˈ Y ]zO=Cgp*)%Pj~XlAaЦנu7A7X0_ޅD)1n n`"Uv۵luo;mW_չx i>D(і¿J>$c~1=7 i4QL$ßPs@3)FU!^BA\ ׄ}W1`,4MMp``! iõZ$!MiJnF4Ms^ AŪ'9skX09~H~ʶ@^q K”-&'0eE舦l|,n"#yڋ/;{7rDo/4 ص껗j`|D$ m4㌕bD˜%P `m-һƤ%0XcKLZcR / 1I|A[l]^%d 5I'&\~wʶ{IHa, #5|m3('cdY-Wn4AzJvØsїK>qL>Ќݾ8)CݬI寮E;ǃ6~N31P;6<6-keK`}7twno&2;Nm Jߛal*u!#\  C/NK) `k\X~T $ tEKzKaӟ施ͅ8Hk? !߰ӑ}H96;WjǤM : G<+{yeg;kNvV6Sh׉b (9^#AW a>#EL@T6:I73Tm0݋y=17"Sxf4DECKsnÂ$%d [em%Pr>$!> jG@ Jcv}[UNPkǢcZO?҄ki%3B> SD HAIA%#RЁd L%K XP]i &[k煽{Ժ᧟|33,@ǐ-'L.MAu?^M|5tͿ|i50J$Hd # OM tWqjFHTG:ByR 9 E h$K0Ъ\ }:7h9j~hGAKG~[ teK(_|cw#;{o .$.A;U`h!׉/{ќL3x _DiZeu\$Օb|ԫuBBN~i!:EoB9:Ln(Jm^8[_'SeGmy*w}ɖhc 1sd 3{ ~㷁 d".s2\ 6mz9V3CYb4:t\!Ch|=9=iڽ4 y&uiQq^dmd ЈOTۃN)f4VWhb9X9f9#d uEڋqHQ<ɰݦޝX%ir㿢EGφm+P__?iy6UXԖsg"ni4)ލiZ" PqpȽf + 0/%#B0BO;B7n)i-Gv6Χj0,CtqW)@aӇKlϋi8d=n;W8T+ ֐.C7/d80-}hMj Б҄lF,lAX2D,aP`gsEK:WmߟCޱ~'"&oWSCVv͙Y7R#-O9 aQVߥHݪ0 ^}zB5p24Yo =xAߠYD(Jbo)H–q_ګޱpn|^&'[fAB>,Ʉ{`T3#]k?`9묲@Hxc5Tx( 0m+DTTb(>+=*{’Cn&s ܎ϼ Μ. ؋n,MEԓLM[w{_v_U7A_ HD*+Eڔ08'<\ I rD Ec[ڟwʽ4@!]r} 3]!/Qqfke|o; ;cKa~K_krp`IIa?k" S6غ(# x$2ę|I\v"?@NV2O4C]?Aw"О? [c> 5|z"0[39ɻ#P.Ԩo =o[R P]^¯N2_¼~^]%p]D t!ÿvՍ !zrZt]Fw"Uz{h>Bo hq)`)g=,޾RJʟWc~_MO{͛T/jA9.gwgL3n,پPĦSe2(o˴]tG1ꍤOKgeߎ IdDO9$Mm@#mpdvHaS A1n xQζ P߽WPVq׿~ǝHyʳp̓*p5|e|Eڋ &'h=m"X{ǤI0!=?R|zّ0|А n FH@,oQEöq)Ob(ryC@ei@{O}6g&yx|↋/兹Pa@l.ZMܦj@_"ў"x_5B_O $,;j(~$I@1UfT=;r8u t'HTP\%j0,hHU2D bEV; ` `Ρڀ<wlԔ&Ź @r3 I PxK 2ZHU_y)¿_^#;_H Nҋ62^H}lweofO|%B!D@!DGHZlF6dt_1t`z<~ "0'U474"# 9W]'^`cEPQ#tÓ#;/Ǩd28{ǀI`PD$D̟Q&IOhK2 ohCuGK@_3@Iv_e0prD# P(GDb4muGVg]FIr"W!?/\ cBc&0'ev mdn2!k0ŚxQ }.!-!SWK@T}.6O@?M:b hx3R, dA;0} GHrj̀vӤ wHj9 Փ')#fu0!D6 Ry=#ihV[ 2)*=ЏUq0 M @1z"ОkELb/N]`U$3+ fF b'^A׌##鋼JS(/4H$? cP$ǖ-@hD~x؋Fdv ?`;H">@AhOa j$ D 4QO!ɘ8?Ef]B3I?ym`ӳg@$ z2u%G#95/(+~@dZ< 0PA z")a"G)3m  %ǠI$7VHЎlvFXHQx%ڙCXr|:8K!;.- Tғ~L@{} G#;-$va?Vq3tWl~թl,X!41 E=K@mGϠoD*FV*g } qWޙsTgWJ 4 ]pUU7Q^'Р I@"+ MmhG GHmxU5yH_U7Ňݕzȷ]qn  MA!* @bQ#"?a S ep1t!H< -[LШ {qoHђ,J-ٮ]rK.p̑\e)P;PρERIuҙHQ),`0^OϏݝ57?ׯ+5F~ ߭j0(T\V@o1M}s$/[vaF;^4\d2U;鄉TBCo&cVIq`&X HӞU(J&[1ӕNJyGií^{B"Mm\n-EEY*\ϒiP ̆Txtݧ[BvY<L<ywk Gxy^Y~B֦(u=#.LU[7 gBHχ>鬍:G6'I(:ujpCLL',>zv/qtNJ:;p@JnhӬKW@D]q<,qmN{P-[]7%EST20f赖ZNTHF:H{Gޝ+R4N <]o$CEEYrrz)V^zh Ӕ-p{t8@h tgX9PSAz,LWY U\{ZApRAg>ꯙA@:*1ٜ)vy)Wdpܰ{OGcԈ0vMVLܐ5?]ROJ?5SbTNcBeIDAT.?}}I8Gr1Lb>S6xt݇Ys|b7 II]2*y_ AA.tHG&ӥS $R8dͭ(S vH,_eⶖ]('~_hYZA+[{LCzN E"rCn!ZW{_ c-V۷XvC N 4sU/Vt67j!DuDVW{_ 598%ozVw8<ٿ6.[zxJj^I9FЀǰ\>G,::2"CznZQf@}`4t2w7|GQ4>&5ȸ&iHd6 'ӟF'[돧V AgQt9WM0Bn\` 1V@x:'[i)/C:ڛ"ca R'g{/w?}210 fj3p5fϢ}Mt")~sa-컟>ދAȷ uI8h!wR|{c_UE‹YAYQ EQ#⻭f/GfٯʤHKk,|IobE9Ӏ[yKKR 8ˀ~d&&FE[HOsQz+0~u_\_ʿ]J3BW◔PQ:&$}d1b1Yyk)=UK$[ӕu \0Wyޑ)#dRNVȓ#i\ǥ@]8e%%X,8,% U_ʏk~1K ?Cq[5pbw>~hz1ϐnя|ΤOl幵s$BNvK.soLpL3ZǏ?y |AHYU{CK:cLϖqr<{$͔qyP4ΝTH.ѽ5F냿M{r˂Vy ͙evUe>O1eУP|aܰP?/tZ/wK(M+<3XNP(ƘK.1YWu17@+7/åf8 `.%/')֗.u$ ]$,Ew<ʸc79p/W7}! [Ŗr9eҘܰUbNYkC.cgZ9.VFN<~h<pT]_ ;aG32'w 2I9k |~;~S"ڐXjJ[qp]DvKt՟^X:6zǽ_'atGB_>x/hU/ {/p|k :L^\7o'7ZZ %JȤ %[%Iw չpg]ֿ$  X魜VVw6&,sXDy<-oIy{:kPral8#_>j@H/Qh\M#22$~*fOMcu7g#(|κ jh8{ 9R_s,8Iʣx5b)x augiż3X1x}91/'  7;@Zk8 ./*Lz+uo U5üu.s~֍ڭ;k20Kp:v㨳joA:W>,U_X%7lp)+/Y !ަ&:J.`p7v&\77@|P Z@5@u!^,䉁q׮[+MgwmH ɕ PgJ<0 @C2*ny(_t8-_(ЕepR}N*, R۩bxg;~{p5n >n )si?i` j' &3FpQ-PWBfzW֮R{JJ}4V<j0?v%){%/$WCj)@<d()GX%[t7v`(_jGl=LpG`C8fiw]yk C@EQypRY3YP48u5p_DLff'\f7 %/#̻ 28-ҡ(DLAJ j A#\p"  V<PAd h(K)$91K- 0ƴ!G4:J:Q!h>@@(\L8 Zg^h< "&yarXLkw4d/yEQeZY]ZP%3e171M'Ͽ 28((` ؆ACP_LA-yJ3W1n>~n*(JQ (Pu oA%NF%b_E>U!p~EQE{dž sW 1N*7f]4@ƂkD׌}&k(d j}AEg<EgEZkP8!hO3,zEQ A~| Zw.n's\86k(R WBg:߇Z|P Ad D?(< e?7= Z_[A"Bo@1Ё(JX*M4/v@Dp ņA@#.(a˟~1EK0ƴ@k݀ }F(rm TqF _dܨ?EQ A}VKcDu@("1O vp:Ꮲ(̚hA@3Њm h(R hZj6#l( $@<9((<(\@PEQ (rQ@QEQ.  &PIENDB`pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/images/distribution-unknown.png000066400000000000000000000601771364015200300275520ustar00rootroot00000000000000PNG  IHDRxsBIT|d pHYsIIEtEXtSoftwarewww.inkscape.org< IDATxy|\Wy?sk_vfq$!aKK P.h -o ¯PJ, a$,d#Nƶے]#̽njdYw4#螙_/hj29/Z@'1qׄ),狡R|ODD&Ƞa CO c1PRD~|܀PqŨc7o}7u"eN$iv8mmR(h' =[0?zN x<JDDHӇN_yqqاhL ,i@^~ mMWݏ޶j7ٶTC%""#ˍڻ뿛:6`MH#8iXɁK|>W$0J @>~+nH4U#%""*.39ٷ:c'#xnF~8`!mILx؅@ @o}O$֖|Z{S$"r2Sn)92/CWj7I(G|%XO/ 'E|ez_/yɇ-eK(hZ + -h@{o_~ͷ"߻F_&U@MNzߏ"ߓ@p.I@|j&'P|3lq~9j:cΣ'ZCq @D @c#O~Ϟ'$ .d9?Yu% V`bGOO}rN_/5liL4r"Fd+Alc/2*fs|qSj@P*;V<g3oZN>DW{cc#u1z2ODD3) JYn$)5%@ēYv?Weߘ,^.8+2 @a_ N̘,x_}Ϻwaj2'Z|crQWN2QRQAwʒ-4NJoQ Z?~ym3G1'x%'V:AA; yoЗU~ B<%IeQ*fy%w;߮J@Chsӷ3՟3.>1ojM(,KN~b3)s{|+TE'rrP.QVز˖zjH =>+oYA4?heDv oݒJAЊY+Z h;;Lxz{&ޡJF!f^1cbs"{ᔫ" g#x_ xK}_uama7|=e֗vgdN "Fzc3zlH +մZN;-ݳaX'(~ Wҕe @aߠA3cZf歑HdTo_se9(N2:07_ϸM$Y>kAX)ED (GF~{St_7vZUƽOOx}GDDuOOx)d,X7wWڟ~~mKIq}SWw'yӞg @:O\ODDhK(huTo:O9 'Φ|, \l (3 Ij^c2q=hV/bb!M5` @W("ƵrK"մ811OA ~qJOH[7F"S:bV |JW,7=<*?""2cYSݿmWn^ (|0P p}pQ  h‚?֝s. `}Y'""iYw b^;{MwZXhBIVkK-]kz}c.Ҟ^e@}ct%g]k[zNo}+d#3G}:UA>&}J1-zy"S}9nCDD5婾LZDDmy8p{9w^e'RCP{DTseîd'"2:jn(>}q 'nM 2 @P?hRߦ6 ;FSmAW""U1LCCK46uw.;q 1B~][ -DS |F3P""MCiMku!B>&Ύ1p l/IPN?(A$]7q?,|,oV _ (L p9X3Qo DDDFu}v԰ )qq};n TnXl\; Rb?DDTێ0g9@ĉ]Wgj} [2K&) R`V,ejcZS,kjܔY>ՑNϺ}jnrYJ$A.&3?jg$""17m&bԤ8&SܬJ{|s D }2-Q)1 v-'>>9Nן""~1-7o.h\" njls|1 "z SĠd A ৤9Y. ":QFL ~1\֋ *JL^/ b'"z៣X0{_91B8ߋ U!QZUSʙODDT\9񳜸WLDD@$/dx?ջ2b_,V%W+_TY"pQ)/-.~ZCDDDdr+վK&:?^V""ddPIDDD35QuUss&UJ@ľr@DDԀߞ7 i5+@DDXzYDDT u ""j@Wˉ*X BjLQb@DDԀLL+!""ZSc9[ꄉI""".&DDD Q2wQ01֕TeZߛn'""5KJաW鉈81 ""j@=!>X!""X7^+!9>^?(t'-tt'-NYJYWQ[1[$b b R9j Ku5&r9{(ܦ'<qaW?3όu5{Li'l)ʒ*MleM-IK::X؋Sqlό俎 ?vgs"6u8Q:G7Y%Nr,AO-=-il??яxr6D @cE@+mhUT.\UDG D=Q`xöBR}iIU/1msqBmWWLa l7mI+U%էpmp䫯;8GO] 5#x) ,O߶\]بrG0-okRoؒ;-)uɚW??{2ђX Wܹ+"rK7%Xo@kZm7/svŻg R![ $+0,^)ں22Sߋ筋x8}S9Qk_eyfШLu'Dd̆}u.ִ:M1hNq}tp#I=4> ӓ ' _=D>ۖBs"dTZ1$hMDђIK2d-R19.Y_ktY7MBESpFajLVP'}F@D%\<{B'cϑzv1w߭ b[8.J .>},kMza=[oˤ""ƃѷ<_^d C\<~h^uK+^ݰ7^Z}{n:V A1 2h:۶7=ϻ}z`M7==cU%[bjw>s6ˁΤeAKh5Gd {w[Ş:> &cy KTZkxqhcϑw&'F+DU'w#g%s=ާ v ^}]ו[ԗ~}[=vs  ШZ SJɜut^iĒD~(^gAP$PK"DTaʂԩ+ԉMyhs< )?NXMYQ7]EWU,Uޣ/\fr0xo|pv5lUr>_SĉJuh;lӐ{e/ |zg|/ͤz{>L8BB`.V2x:{W~~Pї~/,UtI[* d蚖Z2ũBO{K̡Mnꀕ0UM_ <1)7{n~&Z5i8,# ?po~WZ^!.>}=1%4}6C21-uP*-ij9o;mvZVh8?Pt&SK Q5^ڲޠε:{d̙T?}_pNw/=G~^ ]u?UzIzxƫ`t"U&jҋmET~ݾֲ"-й "]ܭ'?)^zxwy/ph%gLMH ;9i`*ܖ g,[c>"ޣ%޼oS$-|ߐ+'?^Y9k$L'cbw>9@(֔H IDATŗ0Δ:M'6^EzZP@EPĉOEl*ք3t|Eڡ ,%=7|5$BOhm37.>bM:Lxt(sA!FPfh"YL EY[!yZ֭^%_3Nx'N_2f&C%11օfc^lޣcV˲Eʉ~$Q[U"N /Ցoz6?4sJ:zϺ0RLLȌ(\5<|( *7 /(DWov*;ڼ`؜ePLiF&D5: p9Lc~5"ޢ'"-w@2꠳91Nb΀JDz`o0\m}29GS|Ubm7l;Z [2k +~2.ȏ'ߛ^J;9wae^gpM}nfZzh?jOLL;kk`ہ/!vWH>sY)ZCj. *O{&k68mVOXZPL&џ@O+zN5?ԉ;ۜ{Wv̹Mj[~^n \3 iWR@k`ǫϷwdമw|hn@kVhјF2<DY)fԼ{MdG2v|>s-2 j'ûv̛pY92Z-5έp?Z~MQ s=\sS yGF]?6Q{0@zx!&ƺr%g06X7A2EBw`0<}|$=D,ϵ?6 Sdtauy]P$-.~.rzap~sZ^0EMM;:Vc[I$s79!jwwqoйOW/$hUaDgS(N@'&n@fxFV~%R칥½=(1p7e}8f5xTiNۏ/ސKW3۟n~޿tPJy#5::}⃗%ޮx:\]"#i8cSb4͘*@(`T'C{;p wTmV&2kM,dfSOH3#d_zc{nOŪzGc ;( F ɪ ZnΝ]cRlPzd(/չ6/N/h2K7<]oi]|![c3EV/U/*/A2̕jCO~x[돼"U枝=1ہ_f0 =׷˨=b14.hDYtn竿ru7KJr\| Ub&'{M7?O^kvbyx황}cE|vV|F&Id*m  1@K"瞽Ft5KWs]- jN9?}ClP߱.gi_IײǙ @4bţ3> 0'ԘfdQ#ڠb xӷeC2JOv7;+ծU,Qb{͙;:49\.: 0x_ &T"kKl &j7wh{W.0sXD'{+TYL>BtZ@Eb,/^ 40-?:+R=>ϒSVBOLڤMmГH~g@%Xܕº|m5_R=>Z <M[ϲQr@%Z|6/;u}b]SBz,Uܥ͝pdž;tLpGO.a UlXvm:`tC bG B|Ͼ޺,j`0/^ xLݼ uԘfd@T+ Dݺ esYh7ˊvsہ؎+Z=D!NbelHŸG1'?-@9 ?bT$,:;I3R x*x9[[]3M+z9l|_DՀഄ[Ȁlh= Do<c]ݝHD(f |Uԧ^e[unylGr UNi52=.uяt-edҐ.|E i벨|Eo)n~@g͏BO Q"Yjxl޳I'* P31օ0ս-Q m}YRMӥwV)q>CcZ 5΄%ﻰzYMRUVSVOCDKUqKڭs ~ ׏Ͽ@*X^ojQ83"úbu{J'Zt &xgV*zlۭWnJ?uRml^*.O|Vr+sb4 턉e!l=0PM00օѼf{;?{{.j3|㼛IϾQ7'ni[xs'™u["c/NdFX- K$:6CcZ Q {m^-~BlWTU'&EOfڽޝi=gsH/G01օj3'F+~Gjqa "U3(u#""侧_|wl2[sVg?gϷ%0ՍPg@D4C6c?[]` vK"+7_EPV~=ȡ"j@;~&oہKv ў3Jx>&*dc] Q\OgK͈ެ!Wt%?-)""j ?MCT1͈9[w _,.oźNJ6}":iMfs}nUg̿81NsZu5%~wQχ7͛t b}y׎B"yG55p'?MU"\UR&voDüXg@@DT]=;O\{wlh/J9vKWip @AKܻW{=}*r">KDsIϗo8E ODpX~]UkfT ҥ^GĉVp`&ƺ"JLWrM)t4]jL" ^ݏꂧ5v?e!s翨O7 bsKf'jO׏O!.[ *yXD z`Ն7jS""pVZSҶˁQ㩮BOF9F&2z/P$=kO;G3OB2Q ߽ވUpKQht*Cc?Ddt&ܹ/VT43eYz Qt߼﫿z;>(8=phK K4?c] l#[qۣH:$i7w |]s?$&D5C5ozΝۼL/@Œ,ߠdK$8Q,&A5EI;st@ke]oI^S,ka.+}^?-'p o,4Aqm;اڭ%qD쎕pVkQe4 P}a@TF,oaK1Z8?Ou Q '_>]K뵕l-$uTTiQXFC%ƍ_%:ŲYv["I~\G`h-jt))x3jj/ED  5ED5bn~vr֟[~/\Gś 8?cg o ~gs >IgwD'DbM|vzQt75 p{CI$QZ(*Mr??Q%0 A>w` 0N5韖KO~.."~Qq'\ý}Fe_C޶C}-}Gd c] -u{G[1pǎ?ch޿? w$Zs=}Θ}{vzeOqxS}14^0qiQ86?v<+_78>QYM+Q21L#qioPWO^6}⻹]GN +xp_%<级/Q/VZ~]>38ZZMŗqc%QgmȆ*DT -=;G_}g؁cW즎 5s/VkK$@ߥ XMק_t/۸R>ׯK-.vO{_x^cG{z D5):Q%{5<9}nc"@TD6Di(|ek5r%jWWN>n;>@7&D+Wk!tzkT~k??ӲQ ""}N/Ze/O5A-ٖuKM67D&IdwonS ;3&x*;n!2F"aCu\|~YDTOd[@t+W'/]^a9+箴^  A" dUѿU)捭l{BƓ iGCuZm w@ED64G7Fϸ%bb@.Ԙ Q- j7- qI6T0o"#2Vҋz w -ҋW2g;@*ِI¥ \=(d[;c]7<a5HZOhIJzo%iZ W-VB?Sg*=L~TЙ!0 YW+t6%s9L&^z^zܰ1 YW%-)">KA2aQb@0L8o|LCQ3iZ(5,u A='n.## cQ oQ܎?{p Mg@5ϴc]mDomD6۳}P]*y(QL:ٍFCcm72x@m&ź+hƤN7h7&!c.DeQ Yċ`>Ιm{vwV'QՍPdi43jt{ңsnn vQ0!!j@z1}pA{h{PŬ7h)udL~؞]\snD5ɰW?lƷl `8=oyfY%FR*rԈ}kk w{#c>N&3OA,0L6Ca4_k}w_+ QM0$晑{#WsLq"wpji1.@Ny;{-|;v,9z=_@G|@ń7G2xp+髷>?oug*ڈF$?%~uDs<ԟza2y-}~|[Yvqz0fQL |zxTŝL_,w|^*x-(Q =)Lgiν}ס;gޟ|F/1{3f@5̄OR&&D5W{oғaN~pO{ã( V5~&T]4 gt3ԿyoU9o,kJ}hs<دvCO/ja5^hN%RPqc}{pźe{!/?w$#ۮ}I}|t"iuM D1 Z vI$2ӳ^O6m۶-x6V敝8ke%cU;al?ܯ=oxt}DE_C%at?@cd&6"j킕j;:8}( @:OܩeYKWvY+;q֊NiKƑ:H"HF#>JϺ.&`t2G'`t2LJOo?܏י\57*8=y E3,'YI;@)Иu~ %9C5cAcT"UAAK FEN%*J`wD䍖hlEہb.~,ob51hd_d&m5O,]?e#=_pHtޘJ sJ4M BtfqU {De,D7l*PZ^-o#3J6HV7- sF$DTq"uz8@HG0"m5`40-4-Dbn:WAnў jAV DV(bAEha+z@Dcw,ҡwI|A$CdZ/AA=Adj&V&"*"6k{i=Oǟ$vjn/# T,}|Xh\©xugk{ZxC'8&q gh@MۺU}Q-] #>Qe@S'Ԫ=x1xcMSURh*%!V$Yؤ=#,S0~0ƴTƈXd*!SNY:e=ps=p]hrQeA¿'Z(W"'1),#g"XpsLa( mCN^$q v~%bL?Qe{Z{,dPrzWm&8c"Z"'8!CU&Q!:`(TF2": *y'Ak @Zr(|9YY~VVu,  &5D H Dl*y^}|o},DDhܵX[2sojdֿDDDd6-""0$U1 31hz1Q*sO9s% !ђ1 b{BDDTQ&ƶ'""f5*DDDT}F%&H*gT@DDDKØonb#9;g8$r#e-ˑNC%9//>a@žX ²(6 MZzZrŏr*M{^UWg ]t |^U-3""""ź ""8TL5&bY]EyHh]@J{8BDD4)9cXB%{. &DDDP ySVƏ+SAW,T@?""(Z+T@DDDg @ DDDSĉ03 "H0 MѨ[EDi1/x`'"% ""]@SO +BDD4VE}SOh]@LL!:GWԘ6i&o1=DDDCQcZ!2@wgBDD4r ""zmwmE.!""KiZ웴acuqXi,f\d@ž )# f+b$M=hɂLѐ"Pzұog;mC0DD4"jSb$Il`[m-a""U}cq%mmz:`Qyghte!Z6@c~\ DDDfĦ,O_@Ҵ' I=\FC6xnSُײO`>+&d(BȪ5x8?#]e۱'i1 { @i@7Z;!'Ṟ""q)JF[;7_߈*@wiw Q?;Psl۲|^E&ۅh* OToz϶ a-6v6o*:xjI2QI%'jpCw _ں//><ӧz&Q P2hw-\+ƨl ʑ/>׼/n%Z$6.$+ZcLr2O \N5R\`6x'-.0W2(àյ\4Fd?osho໿##HlbhBO ~ 5M5v;~е6 `q  %ڷP0;kg?GkwB-HKb.tר@H:moa^>M-"T"YDl_L޽'N օĴCE}N}.ښLsMOWy9_O按,NGm^/1$aЎYۜHE~ϛ9z?YMBːAŽ|gޗYd,fma_=R6Eȱ؀m?o} =p?LHg;ϟSa&P͝7^Yiڄһ] 21|K?wlBn̍ ŕKy2o9e zgZu&s~~gڽq\KLJ208"` +,`_Jν5Oе碟G \6{?;/g>IzAm߷V>>T;>vl;Z-"5p hH1~?¶Zvy ad]N/y^M_1 es|BIT$iێ<7Ӡ @ 5'H Y0ղ_?kݑ^f>wni&<}ޣz/۵;7t߾ Яړ$f 0 }-ϔ.z}:R;o|i1 N N?qPnΐ"¸~v:AjnKl@/ t^A{ʠյp;p;|=꿾]}wc탇3?l~G<Ťm[󽸫6m"w/}w_߮?$ipC_ȵ"W Txbw1%3.^esŇO~׾ܓO-p.Js`V "<ygM<|!]``OC/%c1{=9YQy|p刺,wA"!\dEhd~|>էzڭ{׷7;7_[ۻ}ՍcwWzwT6(+2# ]HH"ekdÔf߾3+&|5 㳛`a^'jEINZl/dp @Ff_ok_WHמ;$kvӱj\75Tx{ѿ 0 "@9ǡs+ړ'կ}_Ϟ$L|_3^@ut_0PIz~W,'= #1pW[7_[k܀t߅{N]]?^[7?0im]@>p Uzf~ɥk _JT*~&_vos'ȳ\ۧۦ {E/,&gۙ0Oiۣ?|q?k&ЧuZ0w0W0\\C4п`YC^ %d1WJK_XzR^}R:_ZYEIO1;"'y"O/v&}[M;6v6۵v]j6/^HBZ.5s5u kV˨ !@u%O?) B/ij xsռwӌ$c<}OA浡zI*??PkFlm.a!WUy-!suk@+,c zU=eHQ:@f e7O?$"">da۵ k[?X CZ d @ r%9v5f$}r*GDD4 Y?*CHkyf@ hhH0H<ƒzRc!&DDBo 5fBu-xN\S 21 pw/}%A ZD\O ܽDDtu WITr;_AR0J^Z;.cI@@+_>,#"i؃T[OlS,8)q&gx/t .Cz h!?k%>  C Z@ ?9f[pz7yט?n[(C+~!Q 5UH]@2cuW Zc cMnp9d! uI2?"" &A}Z_{aC}C'0TdUt){2c; =?B@]L]6!Wɟy )% ҋ[籵6dP&'+.){YWeHoDDDDT\g tZh!#Y/0Ɣ =!cH"8t!C[[ wt!W m c"H q)~tx/Ѡڐ_߇Po׆:xXD|cD2z\b:C'ܘEH)AtAЯg> "60AW. import logging import notify2 logger = logging.getLogger('pulseaudio_dlna.notification') def show(title, message, icon=''): try: notice = notify2.Notification(title, message, icon) notice.set_timeout(notify2.EXPIRES_DEFAULT) notice.show() except Exception: logger.info( 'notify2 failed to display: {title} - {message}'.format( title=title, message=message)) try: notify2.init('pulseaudio_dlna') except Exception: logger.error('notify2 could not be initialized! Notifications will ' 'most likely not work.') pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/000077500000000000000000000000001364015200300230315ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/__init__.py000066400000000000000000000031321364015200300251410ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import functools class BasePlugin(object): def __init__(self): self.st_header = None self.holder = None def lookup(self, locations, data): raise NotImplementedError() def discover(self, ttl=None, host=None): raise NotImplementedError() @staticmethod def add_device_after(f, *args): @functools.wraps(f) def wrapper(*args, **kwargs): device = f(*args, **kwargs) self = args[0] if self.holder: self.holder.add_device(device) return device return wrapper @staticmethod def remove_device_after(f, *args): @functools.wraps(f) def wrapper(*args, **kwargs): device_id = f(*args, **kwargs) self = args[0] if self.holder: self.holder.remove_device(device_id) return device_id return wrapper pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/chromecast/000077500000000000000000000000001364015200300251615ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/chromecast/__init__.py000066400000000000000000000034271364015200300273000ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import threading import pychromecast import pulseaudio_dlna.plugins from pulseaudio_dlna.plugins.chromecast.renderer import ChromecastRendererFactory logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast') class ChromecastPlugin(pulseaudio_dlna.plugins.BasePlugin): def __init__(self, *args): pulseaudio_dlna.plugins.BasePlugin.__init__(self, *args) def lookup(self, url, xml): return ChromecastRendererFactory.from_xml(url, xml) def discover(self, holder, ttl=None, host=None): self.holder = holder stop_discovery = pychromecast.get_chromecasts( blocking=False, callback=self._on_device_added) if ttl: t = threading.Timer(ttl, stop_discovery) t.start() logger.info('ChromecastPlugin.discover()') @pulseaudio_dlna.plugins.BasePlugin.add_device_after def _on_device_added(self, device): return ChromecastRendererFactory.from_pychromecast(device) @pulseaudio_dlna.plugins.BasePlugin.remove_device_after def _on_device_removed(self, device): return None pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/chromecast/renderer.py000066400000000000000000000151321364015200300273430ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import requests import logging import urllib.parse import traceback import lxml import pychromecast import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.rules import pulseaudio_dlna.codecs logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.renderer') class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): def __init__( self, name, ip, port, udn, model_name, model_number, model_description, manufacturer): pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( self, udn=udn, flavour='Chromecast', name=name, ip=ip, port=port or 8009, model_name=model_name, model_number=model_number, model_description=model_description, manufacturer=manufacturer ) def activate(self, config): if config: self.set_rules_from_config(config) else: self.codecs = [ pulseaudio_dlna.codecs.Mp3Codec(), pulseaudio_dlna.codecs.FlacCodec(), pulseaudio_dlna.codecs.WavCodec(), pulseaudio_dlna.codecs.OggCodec(), pulseaudio_dlna.codecs.AacCodec(), ] self.apply_device_rules() self.prioritize_codecs() def _create_pychromecast(self): chromecast = pychromecast._get_chromecast_from_host( (self.ip, self.port, self.udn, self.model_name, self.name)) return chromecast def play(self, url=None, codec=None, artist=None, title=None, thumb=None): self._before_play() url = url or self.get_stream_url() try: chromecast = self._create_pychromecast() chromecast.media_controller.play_media( url, content_type=self.codec.mime_type, title=title, thumb=thumb, stream_type=pychromecast.controllers.media.STREAM_TYPE_LIVE, autoplay=True, ) self.state = self.STATE_PLAYING return 200, None except pychromecast.error.PyChromecastError as e: return 500, str(e) except (pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: return 500, e except Exception: traceback.print_exc() return 500, 'Unknown exception.' finally: self._after_play() def stop(self): self._before_stop() try: self.state = self.STATE_STOPPED chromecast = self._create_pychromecast() chromecast.quit_app() return 200, None except pychromecast.error.PyChromecastError as e: return 500, e except Exception: traceback.print_exc() return 500, 'Unknown exception.' finally: self._after_stop() def pause(self): raise NotImplementedError() class ChromecastRendererFactory(object): NOTIFICATION_TYPES = [ 'urn:dial-multiscreen-org:device:dial:', ] @classmethod def from_url(cls, url): try: response = requests.get(url, timeout=5) logger.debug('Response from chromecast device ({url})\n' '{response}'.format(url=url, response=response.text)) except requests.exceptions.Timeout: logger.warning( 'Could no connect to {url}. ' 'Connection timeout.'.format(url=url)) return None except requests.exceptions.ConnectionError: logger.warning( 'Could no connect to {url}. ' 'Connection refused.'.format(url=url)) return None return cls.from_xml(url, response.content) @classmethod def from_xml(cls, url, xml): url_object = urllib.parse.urlparse(url) ip, port = url_object.netloc.split(':') try: xml_root = lxml.etree.fromstring(xml) for device in xml_root.findall('.//{*}device'): device_type = device.find('{*}deviceType') device_friendlyname = device.find('{*}friendlyName') device_udn = device.find('{*}UDN') device_modelname = device.find('{*}modelName') device_manufacturer = device.find('{*}manufacturer') valid_notification_type = False for notification_type in cls.NOTIFICATION_TYPES: if device_type.text.startswith(notification_type): valid_notification_type = True break if valid_notification_type: return ChromecastRenderer( name=str(device_friendlyname.text), ip=str(ip), port=None, udn=str(device_udn.text), model_name=str(device_modelname.text), model_number=None, model_description=None, manufacturer=str(device_manufacturer.text), ) except Exception as e: traceback.print_exc() logger.error('No valid XML returned from {url}.'.format(url=url)) return None @classmethod def from_header(cls, header): if header.get('location', None): return cls.from_url(header['location']) @classmethod def from_pychromecast(self, pychromecast): return ChromecastRenderer( name=pychromecast.name, ip=pychromecast.host, port=pychromecast.port, udn='uuid:{}'.format(pychromecast.uuid), model_name=pychromecast.model_name, model_number=None, model_description=None, manufacturer=pychromecast.device.manufacturer, ) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/000077500000000000000000000000001364015200300237475ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/__init__.py000066400000000000000000000066441364015200300260720ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import threading import traceback import pulseaudio_dlna.plugins import pulseaudio_dlna.plugins.dlna.ssdp import pulseaudio_dlna.plugins.dlna.ssdp.listener import pulseaudio_dlna.plugins.dlna.ssdp.discover from pulseaudio_dlna.plugins.dlna.renderer import DLNAMediaRendererFactory logger = logging.getLogger('pulseaudio_dlna.plugins.dlna') class DLNAPlugin(pulseaudio_dlna.plugins.BasePlugin): NOTIFICATION_TYPES = [ 'urn:schemas-upnp-org:device:MediaRenderer:1', 'urn:schemas-upnp-org:device:MediaRenderer:2', ] def __init__(self, *args): pulseaudio_dlna.plugins.BasePlugin.__init__(self, *args) def lookup(self, url, xml): return DLNAMediaRendererFactory.from_xml(url, xml) def discover(self, holder, ttl=None, host=None): self.holder = holder def launch_discover(): discover = pulseaudio_dlna.plugins.dlna.ssdp.discover\ .SSDPDiscover( cb_on_device_response=self._on_device_response, host=host, ) discover.search(ssdp_ttl=ttl) def launch_listener(): ssdp = pulseaudio_dlna.plugins.dlna.ssdp.listener\ .ThreadedSSDPListener( cb_on_device_alive=self._on_device_added, cb_on_device_byebye=self._on_device_removed, host=host, ) ssdp.run(ttl=ttl) threads = [] for func in [launch_discover, launch_listener]: thread = threading.Thread(target=func) thread.daemon = True threads.append(thread) try: for thread in threads: thread.start() for thread in threads: thread.join() except Exception: traceback.print_exc() logger.info('DLNAPlugin.discover()') @pulseaudio_dlna.plugins.BasePlugin.add_device_after def _on_device_response(self, header, address): st_header = header.get('st', None) if st_header and st_header in self.NOTIFICATION_TYPES: return DLNAMediaRendererFactory.from_header(header) @pulseaudio_dlna.plugins.BasePlugin.add_device_after def _on_device_added(self, header): nt_header = header.get('nt', None) if nt_header and nt_header in self.NOTIFICATION_TYPES: return DLNAMediaRendererFactory.from_header(header) @pulseaudio_dlna.plugins.BasePlugin.remove_device_after def _on_device_removed(self, header): nt_header = header.get('nt', None) if nt_header and nt_header in self.NOTIFICATION_TYPES: device_id = pulseaudio_dlna.plugins.dlna.ssdp._get_device_id( header) return device_id pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/pyupnpv2/000077500000000000000000000000001364015200300255525ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/pyupnpv2/__init__.py000066400000000000000000000635201364015200300276710ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import requests import urllib.parse import logging import collections import urllib.parse import os import lxml import lxml.builder from . import byto logger = logging.getLogger('pyupnpv2') class ConnectionTimeoutException(Exception): def __init__(self, command): Exception.__init__( self, 'The command "{}" timed out!'.format(command) ) self.command = command class ConnectionErrorException(Exception): def __init__(self, command): Exception.__init__( self, 'The command "{}" could not connect to the host!'.format(command) ) self.command = command class XmlParsingException(Exception): def __init__(self, xml): Exception.__init__( self, 'The following XML could not get parsed!\n"{}"'.format(xml) ) self.xml = xml class CommandFailedException(Exception): def __init__(self, command, status_code): Exception.__init__( self, 'The command "{}" failed with status code {}!'.format( command, status_code) ) self.command = command self.status_code = status_code class UnsupportedActionException(Exception): def __init__(self, action_name): Exception.__init__( self, 'The action "{}" is not supported!'.format(action_name) ) self.action_name = action_name class UnsupportedServiceTypeException(Exception): def __init__(self, service_type): Exception.__init__( self, 'Service type "{}" is not supported!'.format(service_type) ) self.service_type = service_type class MissingServiceException(Exception): def __init__(self, service_type): Exception.__init__( self, 'The service type "{}" is missing!'.format(service_type) ) self.service_type = service_type UPNP_STATE_PLAYING = 'PLAYING' UPNP_STATE_STOPPED = 'STOPPED' UPNP_STATE_PAUSED_PLAYBACK = 'PAUSED_PLAYBACK' UPNP_STATE_PAUSED_RECORDING = 'PAUSED_RECORDING' UPNP_STATE_RECORDING = 'RECORDING' UPNP_STATE_TRANSITIONING = 'TRANSITIONING' UPNP_STATE_NO_MEDIA_PRESENT = 'NO_MEDIA_PRESENT' SOAP_ENV_NS = 'http://schemas.xmlsoap.org/soap/envelope/' SOAP_ENC_NS = 'http://schemas.xmlsoap.org/soap/encoding/' DC_NS = 'http://purl.org/dc/elements/1.1/' SEC_NS = 'http://www.sec.co.kr/' DLNA_NS = 'urn:schemas-dlna-org:metadata-1-0/' UPNP_NS = 'urn:schemas-upnp-org:metadata-1-0/upnp/' DIDL_NS = 'urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/' SERVICE_TYPE_AVTRANSPORT = \ 'urn:schemas-upnp-org:service:AVTransport' SERVICE_TYPE_CONNECTION_MANAGER = \ 'urn:schemas-upnp-org:service:ConnectionManager' SERVICE_TYPE_RENDERING_CONTROL = \ 'urn:schemas-upnp-org:service:RenderingControl' def _convert_xml_to_dict(xml, strip_namespaces=True): from collections import defaultdict if strip_namespaces: def _tag_name(element): return lxml.etree.QName(element).localname else: def _tag_name(element): return element.tag # taken from http://stackoverflow.com/questions/2148119 and adjusted def etree_to_dict(t): d = { _tag_name(t): {} if t.attrib else None } children = list(t) if children: dd = defaultdict(list) for dc in map(etree_to_dict, children): for k, v in list(dc.items()): dd[k].append(v) d = { _tag_name(t): { k: v[0] if len(v) == 1 else v for k, v in list(dd.items()) } } if t.attrib: d[_tag_name(t)].update( ('@' + k, v) for k, v in list(t.attrib.items()) ) if t.text: text = t.text.strip() if children or t.attrib: if text: d[_tag_name(t)]['#text'] = text else: d[_tag_name(t)] = text return d try: xml = lxml.etree.fromstring(xml) return etree_to_dict(xml) except Exception: raise XmlParsingException(xml) class UpnpContentFlags(object): SENDER_PACED = 80000000 LSOP_TIME_BASED_SEEK_SUPPORTED = 40000000 LSOP_BYTE_BASED_SEEK_SUPPORTED = 20000000 PLAY_CONTAINER_SUPPORTED = 10000000 S0_INCREASING_SUPPORTED = 8000000 SN_INCREASING_SUPPORTED = 4000000 RTSP_PAUSE_SUPPORTED = 2000000 STREAMING_TRANSFER_MODE_SUPPORTED = 1000000 INTERACTIVE_TRANSFER_MODE_SUPPORTED = 800000 BACKGROUND_TRANSFER_MODE_SUPPORTED = 400000 CONNECTION_STALLING_SUPPORTED = 200000 DLNA_VERSION_15_SUPPORTED = 100000 def __init__(self, flags=None): self.flags = flags or [] def __str__(self): return str(sum(self.flags)).zfill(8) class UpnpContentFeatures(object): def __init__(self, support_time_seek=False, support_range=False, transcoded=False, flags=None): self.support_time_seek = False self.support_range = False self.is_transcoded = False self.flags = UpnpContentFlags(flags or []) def __str__(self): return 'DLNA.ORG_OP={}{};DLNA.ORG_CI={};DLNA.ORG_FLAGS={}'.format( ('1' if self.support_time_seek else '0'), ('1' if self.support_range else '0'), ('1' if self.is_transcoded else '0'), (str(self.flags) + ('0' * 24))) class UpnpServiceFactory(object): @classmethod def from_dict(cls, ip, port, service, access_url, request): if service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_AVTRANSPORT)): return UpnpAVTransportService( ip, port, service, access_url, request) elif service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_CONNECTION_MANAGER)): return UpnpConnectionManagerService( ip, port, service, access_url, request) elif service['service_type'].startswith( '{}:'.format(SERVICE_TYPE_RENDERING_CONTROL)): return UpnpRenderingControlService( ip, port, service, access_url, request) else: raise UnsupportedServiceTypeException(service['service_type']) class UpnpService(object): ENCODING = 'utf-8' TIMEOUT = 10 def __init__(self, ip, port, service, access_url, request=None): self.ip = ip self.port = port self.supported_actions = [] self.access_url = access_url self._request = request or requests self._service_type = service['service_type'] self._control_url = self._ensure_absolute_url(service['control_url']) self._event_url = self._ensure_absolute_url(service['eventsub_url']) self._scpd_url = self._ensure_absolute_url(service['scpd_url']) self._update_supported_actions() def _ensure_absolute_url(self, url): if not url.startswith('/'): url_object = urllib.parse.urlparse(self.access_url) access_url_path = os.path.dirname(url_object.path) return os.path.join('/', access_url_path, url) else: return url def _update_supported_actions(self): self.supported_actions = [] response = self._request.get(self.scpd_url) if response.status_code == 200: try: xml = response.content d = _convert_xml_to_dict(xml) actions = d['scpd']['actionList']['action'] if type(actions) == list: for action in d['scpd']['actionList']['action']: self.supported_actions.append(action['name']) else: self.supported_actions.append(actions['name']) except (ValueError, KeyError): raise XmlParsingException(xml) def _generate_soap_xml( self, command, service_type, dict_, xml_declaration=True, pretty_print=False, encoding='utf-8'): def _add_dict(root, dict_): for tag, value in list(dict_.items()): if isinstance(value, dict): element = lxml.etree.Element(tag) _add_dict(element, value) root.append(element) else: element = lxml.etree.Element(tag) element.text = value root.append(element) command_maker = lxml.builder.ElementMaker( namespace=service_type, nsmap={'u': service_type}) cmd_xml = command_maker(command) _add_dict(cmd_xml, dict_) soap_maker = lxml.builder.ElementMaker( namespace=SOAP_ENV_NS, nsmap={'s': SOAP_ENV_NS}) envelope_xml = soap_maker.Envelope( soap_maker.Body(cmd_xml) ) tag_name = '{{{prefix}}}encodingStyle'.format(prefix=SOAP_ENV_NS) envelope_xml.attrib[tag_name] = SOAP_ENC_NS return lxml.etree.tostring( envelope_xml, xml_declaration=xml_declaration, encoding=encoding, pretty_print=pretty_print) def _generate_didl_xml( self, title, creator, artist, album_art, album, protocol_info, stream_url, pretty_print=False, xml_declaration=True, encoding='utf-8'): didl_maker = lxml.builder.ElementMaker(namespace=DIDL_NS, nsmap={ None: DIDL_NS, 'dc': DC_NS, 'dlna': DLNA_NS, 'sec': SEC_NS, 'upnp': UPNP_NS, }) upnp_maker = lxml.builder.ElementMaker(namespace=UPNP_NS) dc_maker = lxml.builder.ElementMaker(namespace=DC_NS) didl_xml = didl_maker( 'DIDL-Lite', didl_maker.item( {'id': '0', 'parentID': '0', 'restricted': '1'}, upnp_maker('class', 'object.item.audioItem.musicTrack'), dc_maker('title', title), dc_maker('creator', creator), upnp_maker('artist', artist), upnp_maker('albumArtURI', album_art), upnp_maker('album', album), didl_maker('res', {'protocolInfo': protocol_info}, stream_url), ) ) return lxml.etree.tostring( didl_xml, xml_declaration=xml_declaration, encoding=encoding, pretty_print=pretty_print) def _get_headers(self, action_name): return { 'Content-Type': 'text/xml; charset="{encoding}"'.format( encoding=self.ENCODING), 'SOAPAction': '"{service_type}#{action_name}"'.format( service_type=self.service_type, action_name=action_name), } def _do_post_request(self, url, headers, data): try: response = None response = self._request.post( url, data=data, headers=headers, timeout=self.TIMEOUT) return response finally: self._debug_sent(url, headers, data) if response is not None: self._debug_received( response.status_code, response.headers, response.content) def _execute_action(self, action_name, dict_): if action_name not in self.supported_actions: raise UnsupportedActionException(action_name) headers = self._get_headers(action_name) data = self._generate_soap_xml( action_name, self.service_type, dict_, encoding=self.ENCODING) try: response = self._do_post_request(self.control_url, headers, data) except requests.exceptions.ConnectionError: raise ConnectionErrorException(action_name) except requests.exceptions.Timeout: raise ConnectionTimeoutException(action_name) if response.status_code == 200: return response else: raise CommandFailedException(action_name, response.status_code) def _debug_sent(self, url, headers, data): logger.debug('SENT {headers}:\nURL: {url}\n{data}'.format( headers=headers, data=data, url=url, )) def _debug_received(self, status_code, headers, data): logger.debug('RECEIVED [{code}] - {headers}:\n{data}'.format( code=status_code, headers=headers, data=data, )) @property def service_type(self): return self._service_type @property def control_url(self): host = 'http://{ip}:{port}'.format( ip=self.ip, port=self.port, ) return urllib.parse.urljoin(host, self._control_url) @property def event_url(self): host = 'http://{ip}:{port}'.format( ip=self.ip, port=self.port, ) return urllib.parse.urljoin(host, self._event_url) @property def scpd_url(self): host = 'http://{ip}:{port}'.format( ip=self.ip, port=self.port, ) return urllib.parse.urljoin(host, self._scpd_url) class UpnpAVTransportService(UpnpService): def __init__(self, *args, **kwargs): UpnpService.__init__(self, *args, **kwargs) self.content_features = UpnpContentFeatures( flags=[ UpnpContentFlags.STREAMING_TRANSFER_MODE_SUPPORTED, UpnpContentFlags.BACKGROUND_TRANSFER_MODE_SUPPORTED, UpnpContentFlags.CONNECTION_STALLING_SUPPORTED, UpnpContentFlags.DLNA_VERSION_15_SUPPORTED ]) def set_av_transport_uri( self, stream_url, mime_type=None, artist=None, title=None, thumb=None, content_features=None, instance_id='0'): metadata = self._generate_didl_xml( title=title or '', creator='', artist=artist or '', album_art=thumb or '', album='', protocol_info='http-get:*:{}:{}'.format( mime_type, str(content_features or self.content_features)), stream_url=stream_url, ) return self._execute_action( 'SetAVTransportURI', collections.OrderedDict([ ('InstanceID', instance_id), ('CurrentURI', stream_url), ('CurrentURIMetaData', metadata), ])) def get_transport_info(self, instance_id='0'): return self._execute_action( 'GetTransportInfo', collections.OrderedDict([ ('InstanceID', instance_id), ])) def play(self, speed='1', instance_id='0'): return self._execute_action( 'Play', collections.OrderedDict([ ('InstanceID', instance_id), ('Speed', speed), ])) def stop(self, instance_id='0'): return self._execute_action( 'Stop', collections.OrderedDict([ ('InstanceID', instance_id), ])) def pause(self, instance_id='0'): return self._execute_action( 'Pause', collections.OrderedDict([ ('InstanceID', instance_id), ])) class UpnpConnectionManagerService(UpnpService): def get_position_info(self, instance_id='0'): return self._execute_action( 'GetPositionInfo', collections.OrderedDict([ ('InstanceID', instance_id), ])) def get_protocol_info(self): return self._execute_action('GetProtocolInfo', {}) class UpnpRenderingControlService(UpnpService): def get_volume(self, channel='Master', instance_id='0'): return self._execute_action( 'GetVolume', collections.OrderedDict([ ('Channel', channel), ('InstanceID', instance_id), ])) def set_volume(self, volume, channel='Master', instance_id='0'): return self._execute_action( 'SetVolume', collections.OrderedDict([ ('DesiredVolume', volume), ('Channel', channel), ('InstanceID', instance_id), ])) def get_mute(self, channel='Master', instance_id='0'): return self._execute_action( 'GetMute', collections.OrderedDict([ ('Channel', channel), ('InstanceID', instance_id), ])) def set_mute(self, muted, channel='Master', instance_id='0'): return self._execute_action( 'SetMute', collections.OrderedDict([ ('DesiredMute', '1' if muted else '0'), ('Channel', channel), ('InstanceID', instance_id), ])) class UpnpMediaRenderer(object): def __init__(self, description_xml, access_url, ip, port, name, udn, model_name, model_number, model_description, manufacturer, services, timeout=10): self.state = None self.description_xml = description_xml self.access_url = access_url self.ip = ip self.port = port self.name = name self.udn = udn self.model_name = model_name self.model_number = model_number self.model_description = model_description self.manufacturer = manufacturer self.timeout = timeout self._request = requests.Session() self.av_transport = None self.connection_manager = None self.rendering_control = None for service in services: try: service = UpnpServiceFactory.from_dict( ip, port, service, access_url, self._request) if isinstance(service, UpnpAVTransportService): self.av_transport = service if isinstance(service, UpnpConnectionManagerService): self.connection_manager = service if isinstance(service, UpnpRenderingControlService): self.rendering_control = service except UnsupportedServiceTypeException: pass if not self.av_transport: raise MissingServiceException(SERVICE_TYPE_AVTRANSPORT) if not self.connection_manager: raise MissingServiceException(SERVICE_TYPE_CONNECTION_MANAGER) if not self.rendering_control: raise MissingServiceException(SERVICE_TYPE_RENDERING_CONTROL) def _convert_response_to_dict(self, response): try: xml = response.content d = _convert_xml_to_dict(xml) return d['Envelope']['Body'] except ValueError: raise XmlParsingException(xml) def set_av_transport_uri(self, *args, **kwargs): response = self.av_transport.set_av_transport_uri(*args, **kwargs) return self._convert_response_to_dict(response) def play(self, *args, **kwargs): response = self.av_transport.play(*args, **kwargs) if response.status_code == 200: self.state = UPNP_STATE_PLAYING return self._convert_response_to_dict(response) def stop(self, *args, **kwargs): response = self.av_transport.stop(*args, **kwargs) if response.status_code == 200: self.state = UPNP_STATE_STOPPED return self._convert_response_to_dict(response) def pause(self, *args, **kwargs): response = self.av_transport.pause(*args, **kwargs) if response.status_code == 200: self.state = UPNP_STATE_PAUSED_PLAYBACK return self._convert_response_to_dict(response) def get_transport_info(self, *args, **kwargs): response = self.av_transport.get_transport_info(*args, **kwargs) return self._convert_response_to_dict(response) def get_position_info(self, *args, **kwargs): response = self.connection_manager.get_position_info(*args, **kwargs) return self._convert_response_to_dict(response) def get_protocol_info(self, *args, **kwargs): response = self.connection_manager.get_protocol_info(*args, **kwargs) return self._convert_response_to_dict(response) def get_volume(self, *args, **kwargs): response = self.rendering_control.get_volume(*args, **kwargs) return self._convert_response_to_dict(response) def set_volume(self, *args, **kwargs): response = self.rendering_control.set_volume(*args, **kwargs) return self._convert_response_to_dict(response) def get_mute(self, *args, **kwargs): response = self.rendering_control.get_mute(*args, **kwargs) return self._convert_response_to_dict(response) def set_mute(self, *args, **kwargs): response = self.rendering_control.set_mute(*args, **kwargs) return self._convert_response_to_dict(response) class UpnpMediaRendererFactory(object): NOTIFICATION_TYPES = [ 'urn:schemas-upnp-org:device:MediaRenderer:1', 'urn:schemas-upnp-org:device:MediaRenderer:2', ] @classmethod def from_url(cls, url): try: response = requests.get(url, timeout=5) logger.debug('Response from UPNP device ({url})\n' '{response}'.format(url=url, response=response.text)) except requests.exceptions.Timeout: logger.warning( 'Could no connect to {url}. ' 'Connection timeout.'.format(url=url)) return None except requests.exceptions.ConnectionError: logger.warning( 'Could no connect to {url}. ' 'Connection refused.'.format(url=url)) return None return cls.from_xml(url, response.content) @classmethod def from_xml(cls, url, xml): def process_xml(url, xml_root, xml): url_object = urllib.parse.urlparse(url) ip, port = url_object.netloc.split(':') services = [] for device in xml_root.findall('.//{*}device'): device_type = device.find('{*}deviceType') device_friendlyname = device.find('{*}friendlyName') device_udn = device.find('{*}UDN') device_modelname = device.find('{*}modelName') device_modelnumber = device.find('{*}modelNumber') device_modeldescription = device.find('{*}modelDescription') device_manufacturer = device.find('{*}manufacturer') if device_type.text not in cls.NOTIFICATION_TYPES: continue for service in device.findall('.//{*}service'): service = { 'service_type': service.find('{*}serviceType').text, 'service_id': service.find('{*}serviceId').text, 'scpd_url': service.find('{*}SCPDURL').text, 'control_url': service.find('{*}controlURL').text, 'eventsub_url': service.find('{*}eventSubURL').text, } services.append(service) try: upnp_device = UpnpMediaRenderer( description_xml=xml, access_url=url, ip=str(ip), port=port, name=str(device_friendlyname.text), udn=str(device_udn.text), model_name=str( device_modelname.text) if ( device_modelname is not None) else None, model_number=str( device_modelnumber.text) if ( device_modelnumber is not None) else None, model_description=str( device_modeldescription.text) if ( device_modeldescription is not None) else None, manufacturer=str( device_manufacturer.text) if ( device_manufacturer is not None) else None, services=services, ) return upnp_device except MissingServiceException as e: logger.warning( 'The device "{}" did not specify a "{}" service. ' 'Device skipped!'.format( device_friendlyname.text, e.service_type)) except XmlParsingException as e: logger.warning( 'The device "{}" did not specify a valid action list. ' 'Device skipped! \n{}'.format( device_friendlyname.text, e.xml)) try: xml = xml.decode("utf-8").replace(" urn:microsoft-com:wmc-1-0", "urn:microsoft-com:wmc-1-0").encode("utf-8") xml_root = lxml.etree.fromstring(xml) return process_xml(url, xml_root, xml) except Exception: logger.debug('Got broken xml, trying to fix it.') xml = byto.repair_xml(xml) try: xml_root = lxml.etree.fromstring(xml) return process_xml(url, xml_root, xml) except Exception: import traceback traceback.print_exc() logger.error('No valid XML returned from {url}.'.format( url=url)) return None @classmethod def from_header(cls, header): if header.get('location', None): return cls.from_url(header['location']) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/pyupnpv2/byto.py000066400000000000000000000025421364015200300271040ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . """A module which runs things without importing unicode_literals Sometimes you want python3s builtin functions just to run on raw bytes. Since the unicode_literals module changes that behavior for many string manipulations this module is a workarounds for not using future.utils.bytes_to_native_str method. """ import re def repair_xml(bytes): def strip_namespaces(match): return 'xmlns{prefix}="{content}"'.format( prefix=match.group(1) if match.group(1) else '', content=match.group(2).strip(), ) bytes = re.sub( r'xmlns(:.*?)?="(.*?)"', strip_namespaces, bytes, flags=re.IGNORECASE) return bytes pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/renderer.py000066400000000000000000000257361364015200300261440ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import time import traceback import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.encoders import pulseaudio_dlna.workarounds import pulseaudio_dlna.codecs import pulseaudio_dlna.rules import pulseaudio_dlna.plugins.renderer from . import pyupnpv2 logger = logging.getLogger('pulseaudio_dlna.plugins.dlna.renderer') class MissingAttributeException(Exception): def __init__(self, command): Exception.__init__( self, 'The command\'s "{}" response did not contain a required ' 'attribute!'.format(command.upper()) ) class DLNAMediaRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): def __init__(self, upnp_device): pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( self, udn=upnp_device.udn, flavour='DLNA', name=upnp_device.name, ip=upnp_device.ip, port=upnp_device.port, model_name=upnp_device.model_name, model_number=upnp_device.model_number, model_description=upnp_device.model_description, manufacturer=upnp_device.manufacturer ) self.upnp_device = upnp_device pulseaudio_dlna.plugins.dlna.pyupnpv2.UpnpService.TIMEOUT = \ self.REQUEST_TIMEOUT @property def content_features(self): return self.upnp_device.av_transport.content_features def activate(self, config): if config: self.set_rules_from_config(config) else: self.codecs = [] mime_types = self.get_mime_types() if mime_types: for mime_type in mime_types: self.add_mime_type(mime_type) self.apply_device_fixes() self.apply_device_rules() self.prioritize_codecs() def _register( self, stream_url, codec=None, artist=None, title=None, thumb=None): self._before_register() try: codec = codec or self.codec self.upnp_device.set_av_transport_uri( stream_url, codec.mime_type, artist, title, thumb) except Exception as e: raise e finally: self._after_register() def play(self, url=None, codec=None, artist=None, title=None, thumb=None): self._before_play() try: stream_url = url or self.get_stream_url() self._register( stream_url, codec, artist=artist, title=title, thumb=thumb) if pulseaudio_dlna.rules.DISABLE_PLAY_COMMAND in self.rules: logger.info( 'Disabled play command. Device should be playing ...') elif self._update_current_state(): if self.state == self.STATE_STOPPED: logger.info( 'Device state is stopped. Sending play command.') self.upnp_device.play() elif self.state == self.STATE_PLAYING: logger.info( 'Device state is playing. No need ' 'to send play command.') else: logger.info('Device state is unknown!') return 500, 'Unknown device state!' else: logger.warning( 'Updating device state unsuccessful! ' 'Sending play command.') self.upnp_device.play() self.state = self.STATE_PLAYING return 200, None except (pyupnpv2.UnsupportedActionException, pyupnpv2.CommandFailedException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException, pulseaudio_dlna.plugins.renderer.NoEncoderFoundException, pulseaudio_dlna.plugins.renderer.NoSuitableHostFoundException) as e: return 500, '"{}" : {}'.format(self.label, str(e)) except Exception: traceback.print_exc() return 500, 'Unknown exception.' finally: self._after_play() def stop(self): self._before_stop() try: self.upnp_device.stop() self.state = self.STATE_STOPPED return 200, None except (pyupnpv2.UnsupportedActionException, pyupnpv2.CommandFailedException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: return 500, '"{}" : {}'.format(self.label, str(e)) except Exception: traceback.print_exc() return 500, 'Unknown exception.' finally: self._after_stop() def get_volume(self): try: d = self.upnp_device.get_volume() return int(d['GetVolumeResponse']['CurrentVolume']) except KeyError: e = MissingAttributeException('get_protocol_info') logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def set_volume(self, volume): try: return self.upnp_device.set_volume(volume) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_mute(self): try: d = self.upnp_device.get_mute() return int(d['GetMuteResponse']['CurrentMute']) != 0 except KeyError: e = MissingAttributeException('get_mute') logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def set_mute(self, mute): try: return self.upnp_device.set_mute(mute) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_mime_types(self): mime_types = [] try: d = self.upnp_device.get_protocol_info() sinks = d['GetProtocolInfoResponse']['Sink'] for sink in sinks.split(','): attributes = sink.strip().split(':') if len(attributes) >= 4: mime_types.append(attributes[2]) return mime_types except KeyError: e = MissingAttributeException('get_protocol_info') logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_transport_state(self): try: d = self.upnp_device.get_transport_info() state = d['GetTransportInfoResponse']['CurrentTransportState'] return state except KeyError: e = MissingAttributeException('get_transport_state') logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def get_position_info(self): try: d = self.upnp_device.get_position_info() state = d['GetPositionInfoResponse'] return state except KeyError: e = MissingAttributeException('get_position_info') logger.error('"{}" : {}'.format(self.label, str(e))) except (pyupnpv2.UnsupportedActionException, pyupnpv2.XmlParsingException, pyupnpv2.ConnectionErrorException, pyupnpv2.ConnectionTimeoutException) as e: logger.error('"{}" : {}'.format(self.label, str(e))) return None def _update_current_state(self): start_time = time.time() while time.time() - start_time <= self.REQUEST_TIMEOUT: state = self.get_transport_state() if state is None: return False if state == pyupnpv2.UPNP_STATE_PLAYING: self.state = self.STATE_PLAYING return True elif state == pyupnpv2.UPNP_STATE_STOPPED: self.state = self.STATE_STOPPED return True time.sleep(1) return False class DLNAMediaRendererFactory(object): @classmethod def _apply_workarounds(cls, device): if device.manufacturer is not None and \ device.manufacturer.lower() == 'yamaha corporation': device.workarounds.append( pulseaudio_dlna.workarounds.YamahaWorkaround( device.upnp_device.description_xml)) return device @classmethod def from_url(cls, url): upnp_device = pyupnpv2.UpnpMediaRendererFactory.from_url(url) if upnp_device: return cls._apply_workarounds(DLNAMediaRenderer(upnp_device)) return None @classmethod def from_xml(cls, url, xml): upnp_device = pyupnpv2.UpnpMediaRendererFactory.from_xml(url, xml) if upnp_device: return cls._apply_workarounds(DLNAMediaRenderer(upnp_device)) return None @classmethod def from_header(cls, header): upnp_device = pyupnpv2.UpnpMediaRendererFactory.from_header(header) if upnp_device: return cls._apply_workarounds(DLNAMediaRenderer(upnp_device)) return None pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/ssdp/000077500000000000000000000000001364015200300247205ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/ssdp/__init__.py000066400000000000000000000021611364015200300270310ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import re def _get_header_map(header): header = re.findall(r"(?P.*?):(?P.*?)\n", header) header = { k.strip().lower(): v.strip() for k, v in list(dict(header).items()) } return header def _get_device_id(header): if 'usn' in header: match = re.search( "(uuid:.*?)::(.*)", header['usn'], re.IGNORECASE) if match: return match.group(1) return None pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/ssdp/discover.py000066400000000000000000000104241364015200300271110ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import socket import logging import chardet import threading import traceback import pulseaudio_dlna.utils.network import pulseaudio_dlna.plugins.dlna.ssdp logger = logging.getLogger('pulseaudio_dlna.discover') class SSDPDiscover(object): SSDP_ADDRESS = '239.255.255.250' SSDP_PORT = 1900 SSDP_MX = 3 SSDP_TTL = 10 SSDP_AMOUNT = 5 MSEARCH_PORT = 0 MSEARCH_MSG = '\r\n'.join([ 'M-SEARCH * HTTP/1.1', 'HOST: {host}:{port}', 'MAN: "ssdp:discover"', 'MX: {mx}', 'ST: ssdp:all', ]) + '\r\n' * 2 BUFFER_SIZE = 1024 USE_SINGLE_SOCKET = True def __init__(self, cb_on_device_response, host=None): self.cb_on_device_response = cb_on_device_response self.host = host self.addresses = [] self.refresh_addresses() def refresh_addresses(self): self.addresses = pulseaudio_dlna.utils.network.ipv4_addresses() def search(self, ssdp_ttl=None, ssdp_mx=None, ssdp_amount=None): ssdp_mx = ssdp_mx or self.SSDP_MX ssdp_ttl = ssdp_ttl or self.SSDP_TTL ssdp_amount = ssdp_amount or self.SSDP_AMOUNT if self.USE_SINGLE_SOCKET: self._search(self.host or '', ssdp_ttl, ssdp_mx, ssdp_amount) else: if self.host: self._search(self.host, ssdp_ttl, ssdp_mx, ssdp_amount) else: threads = [] for addr in self.addresses: thread = threading.Thread( target=self._search, args=[addr, ssdp_ttl, ssdp_mx, ssdp_amount]) threads.append(thread) try: for thread in threads: thread.start() for thread in threads: thread.join() except Exception: traceback.print_exc() logger.info('SSDPDiscover.search()') def _search(self, host, ssdp_ttl, ssdp_mx, ssdp_amount): logger.debug('Binding socket to "{}" ...'.format(host or '')) sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.settimeout(ssdp_mx) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt( socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ssdp_ttl) sock.bind((host, self.MSEARCH_PORT)) for i in range(1, ssdp_amount + 1): t = threading.Timer( float(i) / 2, self._send_discover, args=[sock, ssdp_mx]) t.start() while True: try: header, address = sock.recvfrom(self.BUFFER_SIZE) if self.cb_on_device_response: guess = chardet.detect(header) header = header.decode(guess['encoding']) header = pulseaudio_dlna.plugins.dlna.ssdp._get_header_map( header) self.cb_on_device_response(header, address) except socket.timeout: break sock.close() def _send_discover(self, sock, ssdp_mx): msg = self.MSEARCH_MSG.format( host=self.SSDP_ADDRESS, port=self.SSDP_PORT, mx=ssdp_mx).encode() if self.USE_SINGLE_SOCKET: for addr in self.addresses: sock.setsockopt( socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(addr)) sock.sendto(msg, (self.SSDP_ADDRESS, self.SSDP_PORT)) else: sock.sendto(msg, (self.SSDP_ADDRESS, self.SSDP_PORT)) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/dlna/ssdp/listener.py000066400000000000000000000111061364015200300271160ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . from gi.repository import GObject import socketserver import logging import socket import struct import setproctitle import time import chardet import pulseaudio_dlna.plugins.dlna.ssdp logger = logging.getLogger('pulseaudio_dlna.plugins.dlna.ssdp') class SSDPHandler(socketserver.BaseRequestHandler): SSDP_ALIVE = 'ssdp:alive' SSDP_BYEBYE = 'ssdp:byebye' def handle(self): packet = self._decode(self.request[0]) lines = packet.splitlines() if len(lines) > 0: if self._is_notify_method(lines[0]): header = pulseaudio_dlna.plugins.dlna.ssdp._get_header_map( packet) nts_header = header.get('nts', None) if nts_header and nts_header == self.SSDP_ALIVE: if self.server.cb_on_device_alive: self.server.cb_on_device_alive(header) elif nts_header and nts_header == self.SSDP_BYEBYE: if self.server.cb_on_device_byebye: self.server.cb_on_device_byebye(header) def _decode(self, data): guess = chardet.detect(data) for encoding in [guess['encoding'], 'utf-8', 'ascii']: try: return data.decode(encoding) except Exception: pass logger.error('Could not decode SSDP packet.') return '' def _is_notify_method(self, method_header): method = self._get_method(method_header) return method == 'NOTIFY' def _get_method(self, method_header): return method_header.split(' ')[0] class SSDPListener(socketserver.UDPServer): SSDP_ADDRESS = '239.255.255.250' SSDP_PORT = 1900 SSDP_TTL = 10 DISABLE_SSDP_LISTENER = False def __init__(self, cb_on_device_alive=None, cb_on_device_byebye=None, host=None): self.cb_on_device_alive = cb_on_device_alive self.cb_on_device_byebye = cb_on_device_byebye self.host = host def run(self, ttl=None): if self.DISABLE_SSDP_LISTENER: return self.allow_reuse_address = True socketserver.UDPServer.__init__( self, (self.host or '', self.SSDP_PORT), SSDPHandler) self.socket.setsockopt( socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, self._multicast_struct(self.SSDP_ADDRESS)) self.socket.setsockopt( socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, self.SSDP_TTL) if ttl: GObject.timeout_add(ttl * 1000, self.shutdown) setproctitle.setproctitle('ssdp_listener') self.serve_forever(self) logger.info('SSDPListener.run()') def _multicast_struct(self, address): return struct.pack( '4sl', socket.inet_aton(address), socket.INADDR_ANY) class GobjectMainLoopMixin: def serve_forever(self, poll_interval=0.5): self.__running = False self.__mainloop = GObject.MainLoop() if hasattr(self, 'socket'): GObject.io_add_watch( self, GObject.IO_IN | GObject.IO_PRI, self._on_new_request) context = self.__mainloop.get_context() try: while not self.__running: if context.pending(): context.iteration(True) else: time.sleep(0.01) except KeyboardInterrupt: pass logger.info('SSDPListener.serve_forever()') def _on_new_request(self, sock, *args): self._handle_request_noblock() return True def shutdown(self, *args): logger.info('SSDPListener.shutdown()') try: self.socket.shutdown(socket.SHUT_RDWR) except socket.error: pass self.__running = True self.server_close() class ThreadedSSDPListener( GobjectMainLoopMixin, socketserver.ThreadingMixIn, SSDPListener): pass pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/plugins/renderer.py000066400000000000000000000316601364015200300252170ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import re import random import urllib.parse import functools import logging import base64 import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.rules import pulseaudio_dlna.streamserver import pulseaudio_dlna.utils.network logger = logging.getLogger('pulseaudio_dlna.plugins.renderer') DISABLE_MIMETYPE_CHECK = False class NoEncoderFoundException(Exception): def __init__(self): Exception.__init__( self, 'Could not find a suitable encoder!' ) class NoSuitableHostFoundException(Exception): def __init__(self, address): Exception.__init__( self, 'Could not find a suitable host address for "{}"!'.format(address) ) @functools.total_ordering class BaseRenderer(object): STATE_PLAYING = 'PLAYING' STATE_PAUSED = 'PAUSED' STATE_STOPPED = 'STOPPED' REQUEST_TIMEOUT = 10 def __init__(self, udn, flavour, name=None, ip=None, port=None, model_name=None, model_number=None, model_description=None, manufacturer=None): self._udn = None self._name = None self._ip = None self._port = None self._model_name = None self._model_number = None self._model_description = None self._manufacturer = None self._short_name = None self._label = None self._state = self.STATE_STOPPED self._encoder = None self._flavour = None self._codecs = [] self._rules = pulseaudio_dlna.rules.Rules() self._workarounds = [] self.udn = udn self.flavour = flavour self.name = name self.ip = ip self.port = port self.model_name = model_name self.model_number = model_number self.model_description = model_description self.manufacturer = manufacturer @property def udn(self): return self._udn @udn.setter def udn(self, value): self._udn = value @property def model_name(self): return self._model_name @model_name.setter def model_name(self, value): self._model_name = value @property def model_number(self): return self._model_number @model_number.setter def model_number(self, value): self._model_number = value @property def model_description(self): return self._model_description @model_description.setter def model_description(self, value): self._model_description = value @property def manufacturer(self): return self._manufacturer @manufacturer.setter def manufacturer(self, value): self._manufacturer = value @property def name(self): return self._name @name.setter def name(self, name): if not name: name = '' name = name.strip() if name == '': name = 'Unnamed device #{random_id}'.format( random_id=random.randint(1000, 9999)) self._short_name = '{filtered_name}_{flavour}'.format( filtered_name=re.sub(r'[^a-z0-9]', '', name.lower()), flavour=self.flavour.lower()) self._name = name @property def short_name(self): return self._short_name @property def label(self): return '{name} ({flavour})'.format( name=self.name, flavour=self.flavour) @property def ip(self): return self._ip @ip.setter def ip(self, value): self._ip = value @property def port(self): return self._port @port.setter def port(self, value): self._port = value @property def state(self): return self._state @state.setter def state(self, value): self._state = value @property def codec(self): for codec in self.codecs: if codec.enabled and codec.encoder and codec.encoder.available: return codec missing_encoders = [] for codec in self.codecs: for identifier, encoder_type in list(codec.ENCODERS.items()): encoder = encoder_type() if encoder.binary not in missing_encoders: missing_encoders.append(encoder.binary) logger.info( 'There was no suitable codec found for "{name}". ' 'The device can play "{codecs}". Install one of following ' 'encoders: "{encoders}". '.format( name=self.label, codecs=','.join( [codec.mime_type for codec in self.codecs]), encoders=','.join(missing_encoders), ) + 'You can also try to disable the mime type check with the ' '"--disable-mimetype-check" flag. But be warned: In that way you ' 'can use codecs your device does not support officially and this ' 'could lead to distortions or in rare cases to speaker damage!') raise NoEncoderFoundException() @property def flavour(self): return self._flavour @flavour.setter def flavour(self, value): self._flavour = value @property def codecs(self): return self._codecs @codecs.setter def codecs(self, value): self._codecs = value @property def rules(self): return self._rules @rules.setter def rules(self, value): self._rules = value @property def workarounds(self): return self._workarounds @workarounds.setter def workarounds(self, value): self._workarounds = value def activate(self): pass def validate(self): return True def play(self): raise NotImplementedError() def pause(self): raise NotImplementedError() def stop(self): raise NotImplementedError() def add_mime_type(self, mime_type): for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): if _type.accepts(mime_type): codec = _type(mime_type) if codec not in self.codecs: self.codecs.append(codec) def prioritize_codecs(self): def sorting_algorithm(codec): if isinstance(codec, pulseaudio_dlna.codecs.L16Codec): value = codec.priority * 100000 if codec.sample_rate: value += 200 - abs((44100 - codec.sample_rate) / 1000) if codec.channels: value *= codec.channels return value else: return codec.priority * 100000 self.codecs.sort(key=sorting_algorithm, reverse=True) def apply_device_rules(self): for rule in self.rules: if type(rule) is pulseaudio_dlna.rules.REQUEST_TIMEOUT: self.REQUEST_TIMEOUT = rule.timeout if (pulseaudio_dlna.plugins.renderer.DISABLE_MIMETYPE_CHECK or pulseaudio_dlna.rules.DISABLE_MIMETYPE_CHECK in self.rules): for codec in pulseaudio_dlna.codecs.enabled_codecs(): if codec not in self.codecs: self.codecs.append(codec) def apply_device_fixes(self): if self.manufacturer == 'Sonos, Inc.': for codec in self.codecs: if type(codec) in [ pulseaudio_dlna.codecs.Mp3Codec, pulseaudio_dlna.codecs.OggCodec]: codec.rules.append( pulseaudio_dlna.rules.FAKE_HTTP_CONTENT_LENGTH()) if self.manufacturer == 'Raumfeld GmbH': if (self.model_description == 'Virtual Media Player' and pulseaudio_dlna.rules.DISABLE_MIMETYPE_CHECK not in self.rules): self.rules.append( pulseaudio_dlna.rules.DISABLE_MIMETYPE_CHECK()) def set_rules_from_config(self, config): self.name = config['name'] for rule in config.get('rules', []): self.rules.append(rule) for codec_properties in config.get('codecs', []): codec_type = pulseaudio_dlna.codecs.CODECS[ codec_properties['identifier']] codec = codec_type(codec_properties['mime_type']) for k, v in codec_properties.items(): forbidden_attributes = ['mime_type', 'identifier', 'rules'] if hasattr(codec, k) and k not in forbidden_attributes: setattr(codec, k, v) for rule in codec_properties.get('rules', []): codec.rules.append(rule) self.codecs.append(codec) self.apply_device_rules() logger.debug( 'Loaded the following device configuration:\n{}'.format( self.__str__(True))) return True def _encode_settings(self, settings, suffix=''): if pulseaudio_dlna.streamserver.StreamServer.HOST: server_ip = pulseaudio_dlna.streamserver.StreamServer.HOST else: server_ip = pulseaudio_dlna.utils.network.get_host_by_ip(self.ip) if not server_ip: raise NoSuitableHostFoundException(self.ip) server_port = pulseaudio_dlna.streamserver.StreamServer.PORT base_url = 'http://{ip}:{port}'.format( ip=server_ip, port=server_port, ) data_string = ','.join( ['{}="{}"'.format(k, v) for k, v in settings.items()]) stream_name = '/{base_string}/{suffix}'.format( base_string=urllib.parse.quote(base64.b64encode(data_string.encode())), suffix=suffix, ) return urllib.parse.urljoin(base_url, stream_name) def get_stream_url(self): settings = { 'type': 'bridge', 'udn': self.udn, } return self._encode_settings(settings, 'stream.' + self.codec.suffix) def get_image_url(self, name='default.png'): settings = { 'type': 'image', 'name': name, } return self._encode_settings(settings) def get_sys_icon_url(self, name): settings = { 'type': 'sys-icon', 'name': name, } return self._encode_settings(settings) def _before_register(self): for workaround in self.workarounds: workaround.run('before_register') def _after_register(self): for workaround in self.workarounds: workaround.run('after_register') def _before_play(self): for workaround in self.workarounds: workaround.run('before_play') def _after_play(self): for workaround in self.workarounds: workaround.run('after_play') def _before_stop(self): for workaround in self.workarounds: workaround.run('before_stop') def _after_stop(self): for workaround in self.workarounds: workaround.run('after_stop') def __eq__(self, other): if isinstance(other, BaseRenderer): return self.udn == other.udn if isinstance(other, pulseaudio_dlna.pulseaudio.PulseBridge): return self.udn == other.device.udn def __gt__(self, other): if isinstance(other, BaseRenderer): return self.udn > other.udn if isinstance(other, pulseaudio_dlna.pulseaudio.PulseBridge): return self.udn > other.device.udn def __str__(self, detailed=False): return ( '<{} name="{}" short="{}" state="{}" udn="{}" model_name="{}" ' 'model_number="{}" model_description="{}" manufacturer="{}" ' 'timeout="{}">{}{}').format( self.__class__.__name__, self.name, self.short_name, self.state, self.udn, self.model_name, self.model_number, self.model_description, self.manufacturer, self.REQUEST_TIMEOUT, ('\n' if len(self.rules) > 0 else '') + '\n'.join( [' - ' + str(rule) for rule in self.rules] ) if detailed else '', '\n' + '\n'.join([ ' ' + codec.__str__(detailed) for codec in self.codecs ]) if detailed else '', ) def to_json(self): return { 'name': self.name, 'flavour': self.flavour, 'codecs': self.codecs, 'rules': self.rules, } pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/pulseaudio.py000066400000000000000000000761471364015200300241130ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . from gi.repository import GObject import sys import dbus import dbus.mainloop.glib import os import struct import subprocess import logging import setproctitle import functools import signal import re import traceback import concurrent.futures import collections import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.notification import pulseaudio_dlna.utils.encoding import pulseaudio_dlna.covermodes logger = logging.getLogger('pulseaudio_dlna.pulseaudio') MODULE_DBUS_PROTOCOL = 'module-dbus-protocol' MODULE_NULL_SINK = 'module-null-sink' class PulseAudio(object): def __init__(self): self.streams = [] self.sinks = [] self.fallback_sink = None self.system_sinks = [] def _connect(self, signals): self.bus = self._get_bus() self.core = self.bus.get_object(object_path='/org/pulseaudio/core1') for sig_name, interface, sig_handler in signals: self.bus.add_signal_receiver(sig_handler, sig_name) self.core.ListenForSignal( interface.format(sig_name), dbus.Array(signature='o')) try: fallback_sink_path = self.core.Get( 'org.PulseAudio.Core1', 'FallbackSink', dbus_interface='org.freedesktop.DBus.Properties') self.fallback_sink = PulseSinkFactory.new( self.bus, fallback_sink_path) except Exception: logger.info( 'Could not get default sink. Perhaps there is no one set?') system_sink_paths = self.core.Get( 'org.PulseAudio.Core1', 'Sinks', dbus_interface='org.freedesktop.DBus.Properties') for sink_path in system_sink_paths: sink = PulseSinkFactory.new(self.bus, sink_path) if sink: self.system_sinks.append(sink) def _get_bus_addresses(self): bus_addresses = [] def _probing_successful(name, _object): logger.info( 'Probing for {} successful ({}).'.format(name, _object)) def _probing_failed(name): logger.info( 'Probing for {} unsuccessful.'.format(name)) transports = os.environ.get('PULSE_DBUS_SERVER', None) if transports: _probing_successful('$PULSE_DBUS_SERVER', transports) for transport in transports.split(';'): if transport not in bus_addresses: bus_addresses.append(transport) else: _probing_failed('$PULSE_DBUS_SERVER') possible_locations = [ '/run/pulse/dbus-socket', ] for location in possible_locations: path = location.format(uid=os.getuid()) if os.access(path, os.R_OK | os.W_OK): transport = 'unix:path={}'.format(path) _probing_successful(path, transport) if transport not in bus_addresses: bus_addresses.append(transport) else: _probing_failed(path) path = os.environ.get('XDG_RUNTIME_DIR', None) if path: path = os.path.join(path, 'pulse/dbus-socket') if os.access(path, os.R_OK | os.W_OK): transport = 'unix:path={}'.format(path) _probing_successful('$XDG_RUNTIME_DIR', transport) if transport not in bus_addresses: bus_addresses.append(transport) else: _probing_failed('$XDG_RUNTIME_DIR') else: _probing_failed('$XDG_RUNTIME_DIR') address = self.dbus_server_lookup() if address: _probing_successful('org.PulseAudio.ServerLookup1', address) if address not in bus_addresses: bus_addresses.append(address) else: _probing_failed('org.PulseAudio.ServerLookup1') return bus_addresses def _get_bus(self): modules = self.get_modules() if MODULE_DBUS_PROTOCOL not in modules: module_id = self.load_module(MODULE_DBUS_PROTOCOL) if module_id: logger.info('Module "{}" (id={}) loaded.'.format( MODULE_DBUS_PROTOCOL, module_id)) else: logger.critical( 'Failed to load module "{}"!'.format(MODULE_DBUS_PROTOCOL)) else: logger.info( 'Module "{}" already loaded.'.format(MODULE_DBUS_PROTOCOL)) bus_addresses = self._get_bus_addresses() logger.info( 'Found the following pulseaudio server addresses: {}'.format( ','.join(bus_addresses))) for bus_address in bus_addresses: try: logger.info('Connecting to pulseaudio on "{}" ...'.format( bus_address)) return dbus.connection.Connection(bus_address) except dbus.exceptions.DBusException: logger.info(traceback.format_exc()) logger.critical( 'Could not connect to pulseaudio! Application terminates!') sys.exit(1) def update(self): def retry_on_fail(method, tries=5): count = 1 while not method(): if count > tries: return False count += 1 return True if retry_on_fail(self.update_playback_streams) and \ retry_on_fail(self.update_sinks): for stream in self.streams: for sink in self.sinks: if sink.object_path == stream.device: sink.streams.append(stream) else: logger.error( 'Could not update sinks and streams. This normally indicates ' 'a problem with pulseaudio\'s dbus module. Try restarting ' 'pulseaudio if the problem persists.') def update_playback_streams(self): try: stream_paths = self.core.Get( 'org.PulseAudio.Core1', 'PlaybackStreams', dbus_interface='org.freedesktop.DBus.Properties') self.streams = [] for stream_path in stream_paths: stream = PulseStreamFactory.new(self.bus, stream_path) if stream: self.streams.append(stream) return True except dbus.exceptions.DBusException: return False def update_sinks(self): try: sink_paths = self.core.Get( 'org.PulseAudio.Core1', 'Sinks', dbus_interface='org.freedesktop.DBus.Properties') self.sinks = [] for sink_path in sink_paths: sink = PulseSinkFactory.new(self.bus, sink_path) if sink: sink.fallback_sink = self.fallback_sink self.sinks.append(sink) return True except dbus.exceptions.DBusException: return False def dbus_server_lookup(self): try: lookup_object = dbus.SessionBus().get_object( 'org.PulseAudio1', '/org/pulseaudio/server_lookup1') address = lookup_object.Get( 'org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') return str(address) except dbus.exceptions.DBusException: return None def get_modules(self): process = subprocess.Popen( ['pactl', 'list', 'modules', 'short'], stdout=subprocess.PIPE) stdout, stderr = process.communicate() if process.returncode == 0: matches = re.findall(r'(\d+)\s+([\w-]+)(.*?)\n', stdout.decode()) return [match[1] for match in matches] return None def load_module(self, module_name, options=None): command = ['pactl', 'load-module', module_name] if options: for key, value in list(options.items()): command.append('{}={}'.format(key, value)) process = subprocess.Popen(command, stdout=subprocess.PIPE) stdout, stderr = process.communicate() if process.returncode == 0: return int(stdout.strip()) return None def unload_module(self, module_id): process = subprocess.Popen( ['pactl', 'unload-module', str(module_id)], stdout=subprocess.PIPE) stdout, stderr = process.communicate() if process.returncode != 0: logger.error('Could not remove entity {id}'.format(id=module_id)) def create_null_sink(self, sink_name, sink_description): options = collections.OrderedDict([ ('sink_name', '"{}"'.format(sink_name)), ('sink_properties', 'device.description="{}"'.format( sink_description.replace(' ', '\ ')),) ]) module_id = self.load_module(MODULE_NULL_SINK, options) if module_id > 0: self.update_sinks() for sink in self.sinks: if int(sink.module.index) == module_id: return sink def delete_null_sink(self, module_id): return self.unload_module(module_id) class PulseBaseFactory(object): @classmethod def _convert_bytes_to_unicode(self, byte_array): name = bytes() for i, b in enumerate(byte_array): if not (i == len(byte_array) - 1 and int(b) == 0): name += struct.pack(' other.object_path def __str__(self): return '\n'.format( self.object_path, self.index, self.name, self.icon, self.binary ) class PulseModuleFactory(PulseBaseFactory): @classmethod def new(self, bus, module_path): try: obj = bus.get_object(object_path=module_path) return PulseModule( object_path=str(module_path), index=str(obj.Get('org.PulseAudio.Core1.Module', 'Index')), name=str(obj.Get('org.PulseAudio.Core1.Module', 'Name')), ) except dbus.exceptions.DBusException: logger.error( 'PulseModuleFactory - Could not get "{object_path}" ' 'from dbus.'.format(object_path=module_path)) return None @functools.total_ordering class PulseModule(object): __shared_state = {} def __init__(self, object_path, index, name): if object_path not in self.__shared_state: self.__shared_state[object_path] = {} self.__dict__ = self.__shared_state[object_path] self.object_path = object_path self.index = index self.name = name def __eq__(self, other): return self.object_path == other.object_path def __gt__(self, other): return self.object_path > other.object_path def __str__(self): return '\n'.format( self.object_path, self.name, self.index, ) class PulseSinkFactory(PulseBaseFactory): @classmethod def new(self, bus, object_path): try: obj = bus.get_object(object_path=object_path) properties = obj.Get('org.PulseAudio.Core1.Device', 'PropertyList') description_bytes = properties.get('device.description', []) module_path = str( obj.Get('org.PulseAudio.Core1.Device', 'OwnerModule')) return PulseSink( object_path=str(object_path), index=str(obj.Get('org.PulseAudio.Core1.Device', 'Index')), name=str(obj.Get('org.PulseAudio.Core1.Device', 'Name')), label=self._convert_bytes_to_unicode(description_bytes), module=PulseModuleFactory.new(bus, module_path), ) except dbus.exceptions.DBusException: logger.error( 'PulseSinkFactory - Could not get "{object_path}" ' 'from dbus.'.format(object_path=object_path)) return None @functools.total_ordering class PulseSink(object): __shared_state = {} def __init__(self, object_path, index, name, label, module, fallback_sink=None): if object_path not in self.__shared_state: self.__shared_state[object_path] = {} self.__dict__ = self.__shared_state[object_path] self.object_path = object_path self.index = index self.name = name self.label = label or name self.module = module self.fallback_sink = fallback_sink self.monitor = self.name + '.monitor' self.streams = [] @property def stream_client_names(self): names = [] for stream in self.streams: try: names.append(stream.client.name) except Exception: names.append('?') return names @property def primary_application_name(self): for stream in self.streams: if stream.client.icon != 'unknown' and stream.client.icon != '': return stream.client.icon return None def set_as_default_sink(self): process = subprocess.Popen( ['pactl', 'set-default-sink', str(self.index)], stdout=subprocess.PIPE) process.communicate() if process.returncode == 0: return True return None def switch_streams_to_fallback_source(self): if self.fallback_sink is not None: for stream in self.streams: stream.switch_to_source(self.fallback_sink.index) def __eq__(self, other): return self.object_path == other.object_path def __gt__(self, other): return self.object_path > other.object_path def __str__(self): string = ('\n').format( self.object_path, self.label, self.name, self.index, self.module.index if self.module else None, ) if len(self.streams) == 0: string = string + ' -- no streams --' else: for stream in self.streams: string = string + ' {}\n'.format(stream) return string class PulseStreamFactory(object): @classmethod def new(self, bus, stream_path): try: obj = bus.get_object(object_path=stream_path) client_path = str( obj.Get('org.PulseAudio.Core1.Stream', 'Client')) return PulseStream( object_path=str(stream_path), index=str(obj.Get( 'org.PulseAudio.Core1.Stream', 'Index')), device=str(obj.Get( 'org.PulseAudio.Core1.Stream', 'Device')), client=PulseClientFactory.new(bus, client_path), ) except dbus.exceptions.DBusException: logger.debug( 'PulseStreamFactory - Could not get "{object_path}" ' 'from dbus.'.format(object_path=stream_path)) return None @functools.total_ordering class PulseStream(object): __shared_state = {} def __init__(self, object_path, index, device, client): if object_path not in self.__shared_state: self.__shared_state[object_path] = {} self.__dict__ = self.__shared_state[object_path] self.object_path = object_path self.index = index self.device = device self.client = client def switch_to_source(self, index): process = subprocess.Popen( ['pactl', 'move-sink-input', str(self.index), str(index)], stdout=subprocess.PIPE) process.communicate() if process.returncode == 0: return True return None def __eq__(self, other): return self.object_path == other.object_path def __gt__(self, other): return self.object_path > other.object_path def __str__(self): return ''.format( self.object_path, self.device, self.index, self.client.index if self.client else None, ) class PulseBridge(object): def __init__(self, sink, device): self.sink = sink self.device = device def __cmp__(self, other): if isinstance(other, PulseBridge): return (self.device == other.device and self.sink == other.sink) if isinstance(other, pulseaudio_dlna.plugins.renderer.BaseRenderer): return self.device == other def __str__(self): return '\n {}\n {}\n'.format(self.sink, self.device) class PulseWatcher(PulseAudio): ASYNC_EXECUTION = True def __init__(self, pulse_queue, stream_queue, disable_switchback=False, disable_device_stop=False, disable_auto_reconnect=True, cover_mode='application', proc_title=None): PulseAudio.__init__(self) self.bridges = [] self.pulse_queue = pulse_queue self.stream_queue = stream_queue self.blocked_devices = [] self.signal_timers = {} self.is_terminating = False self.cover_mode = pulseaudio_dlna.covermodes.MODES[cover_mode]() self.proc_title = proc_title self.disable_switchback = disable_switchback self.disable_device_stop = disable_device_stop self.disable_auto_reconnect = disable_auto_reconnect def shutdown(self, signal_number=None, frame=None): if not self.is_terminating: logger.info('PulseWatcher.shutdown()') self.is_terminating = True self.cleanup() sys.exit(0) def run(self): signal.signal(signal.SIGTERM, self.shutdown) if self.proc_title: setproctitle.setproctitle(self.proc_title) dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) signals = ( ('NewPlaybackStream', 'org.PulseAudio.Core1.{}', self.on_new_playback_stream), ('PlaybackStreamRemoved', 'org.PulseAudio.Core1.{}', self.on_playback_stream_removed), ('FallbackSinkUpdated', 'org.PulseAudio.Core1.{}', self.on_fallback_sink_updated), ('DeviceUpdated', 'org.PulseAudio.Core1.Stream.{}', self.on_device_updated), ) self._connect(signals) self.update() self.default_sink = self.fallback_sink self.thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) mainloop = GObject.MainLoop() GObject.io_add_watch( self.pulse_queue._reader, GObject.IO_IN | GObject.IO_PRI, self._on_new_message) try: mainloop.run() except KeyboardInterrupt: self.shutdown() def _on_new_message(self, fd, condition): try: message = self.pulse_queue.get_nowait() except Exception: return True message_type = message.get('type', None) if message_type and hasattr(self, message_type): del message['type'] getattr(self, message_type)(**message) return True def _block_device_handling(self, object_path): self.blocked_devices.append(object_path) GObject.timeout_add(1000, self._unblock_device_handling, object_path) def _unblock_device_handling(self, object_path): self.blocked_devices.remove(object_path) def share_bridges(self): self.stream_queue.put({ 'type': 'update_bridges', 'bridges': self.bridges, }) def cleanup(self): for bridge in self.bridges: logger.info('Remove "{}" sink ...'.format(bridge.sink.name)) self.delete_null_sink(bridge.sink.module.index) self.bridges = [] sys.exit(0) def _was_stream_moved(self, moved_stream, ignore_sink): for sink in self.system_sinks: if sink == ignore_sink: continue for stream in sink.streams: if stream == moved_stream: return True for bridge in self.bridges: if bridge.sink == ignore_sink: continue for stream in bridge.sink.streams: if stream == moved_stream: return True return False def switch_back(self, bridge, reason): title = 'Device "{label}"'.format(label=bridge.device.label) if self.fallback_sink: message = ('{reason} Your streams were switched ' 'back to {name}'.format( reason=reason, name=self.fallback_sink.label)) pulseaudio_dlna.notification.show(title, message) self._block_device_handling(bridge.sink.object_path) if bridge.sink == self.default_sink: self.fallback_sink.set_as_default_sink() bridge.sink.switch_streams_to_fallback_source() else: message = ('Your streams could not get switched back because you ' 'did not set a default sink in pulseaudio.') pulseaudio_dlna.notification.show(title, message) def on_bridge_disconnected(self, stopped_bridge): for sink in self.sinks: if sink == stopped_bridge.sink: stopped_bridge.sink = sink break for bridge in self.bridges: if bridge.device == stopped_bridge.device: stopped_bridge.device = bridge.device break stopped_bridge.device.state = \ pulseaudio_dlna.plugins.renderer.BaseRenderer.STATE_STOPPED reason = 'The device disconnected' if len(stopped_bridge.sink.streams) > 1: if not self.disable_auto_reconnect: self._handle_sink_update(stopped_bridge.sink.object_path) elif not self.disable_switchback: self.switch_back(stopped_bridge, reason) elif len(stopped_bridge.sink.streams) == 1: stream = stopped_bridge.sink.streams[0] if not self._was_stream_moved(stream, stopped_bridge.sink): if not self.disable_auto_reconnect: self._handle_sink_update(stopped_bridge.sink.object_path) elif not self.disable_switchback: self.switch_back(stopped_bridge, reason) elif len(stopped_bridge.sink.streams) == 0: pass def on_device_updated(self, sink_path): logger.info('on_device_updated "{path}"'.format( path=sink_path)) self.update() self._delayed_handle_sink_update(sink_path) def on_fallback_sink_updated(self, sink_path): self.default_sink = PulseSinkFactory.new(self.bus, sink_path) self.update() def on_new_playback_stream(self, stream_path): logger.info('on_new_playback_stream "{path}"'.format( path=stream_path)) self.update() for sink in self.sinks: for stream in sink.streams: if stream.object_path == stream_path: self._delayed_handle_sink_update(sink.object_path) return def on_playback_stream_removed(self, stream_path): logger.info('on_playback_stream_removed "{path}"'.format( path=stream_path)) for sink in self.sinks: for stream in sink.streams: if stream.object_path == stream_path: self.update() self._delayed_handle_sink_update(sink.object_path) return def _delayed_handle_sink_update(self, sink_path): if self.signal_timers.get(sink_path, None): GObject.source_remove(self.signal_timers[sink_path]) self.signal_timers[sink_path] = GObject.timeout_add( 1000, self._handle_sink_update, sink_path) def _handle_sink_update(self, sink_path): if not self.ASYNC_EXECUTION: logger.info('_sync_handle_sink_update {}'.format(sink_path)) result = self.__handle_sink_update(sink_path) logger.info( '_sync_handle_sink_update {} finished!'.format(sink_path)) else: logger.info('_async_handle_sink_update {}'.format(sink_path)) future = self.thread_pool.submit( self.__handle_sink_update, sink_path) result = future.result() logger.info( '_async_handle_sink_update {} finished!'.format(sink_path)) return result def __handle_sink_update(self, sink_path): if sink_path in self.signal_timers: del self.signal_timers[sink_path] if sink_path in self.blocked_devices: logger.info('{sink_path} was blocked!'.format(sink_path=sink_path)) return for bridge in self.bridges: logger.debug('\n{}'.format(bridge)) if bridge.device.state == bridge.device.STATE_PLAYING: if len(bridge.sink.streams) == 0 and ( not self.disable_device_stop and 'DISABLE_DEVICE_STOP' not in bridge.device.rules): logger.info( 'Instructing the device "{}" to stop ...'.format( bridge.device.label)) return_code, message = bridge.device.stop() if return_code == 200: logger.info( 'The device "{}" was stopped.'.format( bridge.device.label)) else: if not message: message = 'Unknown reason.' logger.error( 'The device "{}" failed to stop! ({}) - {}'.format( bridge.device.label, return_code, message)) self.switch_back(bridge, message) continue if bridge.sink.object_path == sink_path: if bridge.device.state == bridge.device.STATE_STOPPED or \ bridge.device.state == bridge.device.STATE_PAUSED: logger.info( 'Instructing the device "{}" to play ...'.format( bridge.device.label)) artist, title, thumb = self.cover_mode.get(bridge) return_code, message = bridge.device.play( artist=artist, title=title, thumb=thumb) if return_code == 200: logger.info( 'The device "{}" is playing.'.format( bridge.device.label)) else: if not message: message = 'Unknown reason.' logger.error( 'The device "{}" failed to play! ({}) - {}'.format( bridge.device.label, return_code, message)) self.switch_back(bridge, message) return False def add_device(self, device): sink = self.create_null_sink( device.short_name, device.label) self.bridges.append(PulseBridge(sink, device)) self.update() self.share_bridges() logger.info('Added the device "{name} ({flavour})".'.format( name=device.name, flavour=device.flavour)) def remove_device(self, device): bridge_index_to_remove = None for index, bridge in enumerate(self.bridges): if bridge.device == device: logger.info('Remove "{}" sink ...'.format(bridge.sink.name)) bridge_index_to_remove = index self.delete_null_sink(bridge.sink.module.index) break if bridge_index_to_remove is not None: self.bridges.pop(bridge_index_to_remove) self.update() self.share_bridges() logger.info('Removed the device "{name}".'.format( name=device.name)) def update_device(self, device): for bridge in self.bridges: if bridge.device == device: if bridge.device.ip != device.ip or \ bridge.device.port != device.port: bridge.device.ip = device.ip bridge.device.port = device.port logger.info( 'Updated device "{}" - New settings: {}:{}'.format( device.label, device.ip, device.port)) self.update() self.share_bridges() break pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/recorders.py000066400000000000000000000036101364015200300237120ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import pulseaudio_dlna.codecs class BaseRecorder(object): def __init__(self): self._command = [] @property def command(self): return self._command class PulseaudioRecorder(BaseRecorder): def __init__(self, monitor, codec=None): BaseRecorder.__init__(self) self._monitor = monitor self._codec = codec self._command = ['parec', '--format=s16le'] @property def monitor(self): return self._monitor @property def codec(self): return self._codec @property def file_format(self): if isinstance(self.codec, pulseaudio_dlna.codecs.WavCodec): return 'wav' elif isinstance(self.codec, pulseaudio_dlna.codecs.OggCodec): return 'oga' elif isinstance(self.codec, pulseaudio_dlna.codecs.FlacCodec): return 'flac' return None @property def command(self): if not self.codec: return super(PulseaudioRecorder, self).command + ['-d', self.monitor] else: return super(PulseaudioRecorder, self).command + [ '-d', self.monitor, '--file-format={}'.format(self.file_format), ] pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/rules.py000066400000000000000000000105221364015200300230540ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import functools import logging import inspect import sys logger = logging.getLogger('pulseaudio_dlna.rules') RULES = {} class RuleNotFoundException(Exception): def __init__(self, identifier): Exception.__init__( self, 'You specified an invalid rule identifier "{}"!'.format(identifier) ) @functools.total_ordering class BaseRule(object): def __str__(self): return self.__class__.__name__ def __eq__(self, other): if type(other) is type: return type(self) is other try: if isinstance(other, str): return type(self) is RULES[other] except Exception: raise RuleNotFoundException(other) return type(self) is type(other) def __gt__(self, other): if type(other) is type: return type(self) > other try: if isinstance(other, str): return type(self) > RULES[other] except Exception: raise RuleNotFoundException() return type(self) > type(other) def to_json(self): attributes = [] d = { k: v for k, v in iter(self.__dict__.items()) if k not in attributes } d['name'] = str(self) return d class FAKE_HTTP_CONTENT_LENGTH(BaseRule): pass class DISABLE_DEVICE_STOP(BaseRule): pass class DISABLE_MIMETYPE_CHECK(BaseRule): pass class DISABLE_PLAY_COMMAND(BaseRule): pass class REQUEST_TIMEOUT(BaseRule): def __init__(self, timeout=None): self.timeout = float(timeout or 10) def __str__(self): return '{} (timeout="{}")'.format( self.__class__.__name__, self.timeout) # class EXAMPLE_PROPERTIES_RULE(BaseRule): # def __init__(self, prop1=None, prop2=None): # self.prop1 = prop1 or 'abc' # self.prop2 = prop2 or 'def' # def __str__(self): # return '{} (prop1="{}",prop2="{}")'.format( # self.__class__.__name__, self.prop1, self.prop2) class Rules(list): def __init__(self, *args, **kwargs): list.__init__(self, ()) self.append(*args) def append(self, *args): for arg in args: if type(arg) is list: for value in arg: self.append(value) elif type(arg) is dict: try: name = arg.get('name', 'missing') rule = RULES[name]() except KeyError: raise RuleNotFoundException(name) attributes = ['name'] for k, v in arg.items(): if hasattr(rule, k) and k not in attributes: setattr(rule, k, v) self._add_rule(rule) elif isinstance(arg, str): try: rule = RULES[arg]() self._add_rule(rule) except KeyError: raise RuleNotFoundException(arg) elif isinstance(arg, BaseRule): self._add_rule(arg) else: raise RuleNotFoundException('?') def _add_rule(self, rule): if rule not in self: list.append(self, rule) def to_json(self): return [rule.to_json() for rule in self] def load_rules(): if len(RULES) == 0: logger.debug('Loaded rules:') for name, _type in inspect.getmembers(sys.modules[__name__]): if inspect.isclass(_type) and issubclass(_type, BaseRule): if _type is not BaseRule: logger.debug(' {} = {}'.format(name, _type)) RULES[name] = _type return None load_rules() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/streamserver.py000066400000000000000000000404411364015200300244470ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . from gi.repository import GObject import re import subprocess import setproctitle import logging import socket import select import sys import base64 import urllib.parse import json import os import signal import pkg_resources import http.server import socketserver import queue import threading import pulseaudio_dlna.encoders import pulseaudio_dlna.codecs import pulseaudio_dlna.recorders import pulseaudio_dlna.rules import pulseaudio_dlna.images logger = logging.getLogger('pulseaudio_dlna.streamserver') PROTOCOL_VERSION_V10 = 'HTTP/1.0' PROTOCOL_VERSION_V11 = 'HTTP/1.1' class ProcessQueue(queue.Queue): def data(self): data = self.get() if not self.empty(): data = [data] while not self.empty(): data.append(self.get()) data = b''.join(data) return data class ProcessThread(threading.Thread): CHUNK_SIZE = 1024 * 32 def __init__(self, path, encoder, recorder, queue, *args, **kwargs): threading.Thread.__init__(self, *args, **kwargs) self.path = path self.encoder = encoder self.recorder = recorder self.recorder_process = None self.encoder_process = None self.queue = queue self.reinitialize_count = 0 self.stop_event = threading.Event() GObject.timeout_add( 10000, self._on_regenerate_reinitialize_count) def _on_regenerate_reinitialize_count(self): if self.reinitialize_count > 0: self.reinitialize_count -= 1 return True def stop(self): self.stop_event.set() @property def is_stopped(self): return self.stop_event.isSet() def run(self): def create_processes(): logger.info('Starting processes "{recorder} | {encoder}"'.format( recorder=' '.join(self.recorder.command), encoder=' '.join(self.encoder.command))) rec_process = subprocess.Popen( self.recorder.command, stdout=subprocess.PIPE) enc_process = subprocess.Popen( self.encoder.command, stdin=rec_process.stdout, stdout=subprocess.PIPE, bufsize=-1) rec_process.stdout.close() return rec_process, enc_process, enc_process.stdout.read def do_processes_respond(rec_process, enc_process): return (rec_process.poll() is None and enc_process.poll() is None) def terminate_processes(processes): for process in processes: pid = process.pid logger.debug('Terminating process {} ...'.format(pid)) try: os.kill(pid, signal.SIGTERM) _pid, return_code = os.waitpid(pid, 0) except Exception: try: os.kill(pid, signal.SIGKILL) except Exception: pass chunk_size = self.CHUNK_SIZE queue = self.queue rec_process, enc_process, enc_read = create_processes() logger.info( 'Processes of {path} initialized ...'.format( path=self.path)) while not self.is_stopped: if not do_processes_respond(rec_process, enc_process): if self.reinitialize_count < 3: self.reinitialize_count += 1 terminate_processes([rec_process, enc_process]) rec_process, enc_process, enc_read = create_processes() logger.info( 'Processes of {path} reinitialized ...'.format( path=self.path)) else: logger.error( 'There were more than {} attempts to reinitialize ' 'the record process. Aborting.'.format( self.reinitialize_count)) break data = enc_read(chunk_size) if len(data) > 0: queue.put(data) terminate_processes([rec_process, enc_process]) queue.put(b'') class ProcessStream(object): RUNNING = True def __init__(self, path, sock, recorder, encoder, bridge): self.path = path self.sock = sock self.recorder = recorder self.encoder = encoder self.bridge = bridge self.id = hex(id(self)) def run(self): queue = ProcessQueue() process_thread = ProcessThread( self.path, self.encoder, self.recorder, queue) process_thread.daemon = True process_thread.start() empty_list = [] select_select = select.select sock = self.sock sock_list = [self.sock] sock_sendall = self.sock.sendall sock_recv = self.sock.recv queue_data = queue.data while self.RUNNING: r, w, e = select_select(sock_list, sock_list, empty_list, 0) if sock in w: data = queue_data() if len(data) == 0: break try: sock_sendall(data) except socket.error: break if sock in r: try: data = sock_recv(1024) if len(data) == 0: break except socket.error: break process_thread.stop() def __str__(self): return '<{} id="{}">\n'.format( self.__class__.__name__, self.id, ) class StreamManager(object): def __init__(self, server): self.streams = {} self.timeouts = {} self.server = server def create_stream(self, path, request, bridge): stream = ProcessStream( path=path, sock=request, recorder=bridge.device.codec.get_recorder(bridge.sink.monitor), encoder=bridge.device.codec.encoder, bridge=bridge, ) self.register(stream) stream.run() self.unregister(stream) def register(self, stream): logger.info('Registered stream "{}" ({}) ...'.format( stream.path, stream.id)) if not self.streams.get(stream.path, None): self.streams[stream.path] = {} self.streams[stream.path][stream.id] = stream def unregister(self, stream): logger.info('Unregistered stream "{}" ({}) ...'.format( stream.path, stream.id)) del self.streams[stream.path][stream.id] if stream.path in self.timeouts: GObject.source_remove(self.timeouts[stream.path]) self.timeouts[stream.path] = GObject.timeout_add( 2000, self._on_disconnect, stream) def _on_disconnect(self, stream): self.timeouts.pop(stream.path) if len(self.streams[stream.path]) == 0: logger.info('No more stream from device "{}".'.format( stream.bridge.device.name)) self.server.pulse_queue.put({ 'type': 'on_bridge_disconnected', 'stopped_bridge': stream.bridge, }) def __str__(self): return '<{}>\n{}\n'.format( self.__class__.__name__, '\n'.join( [' {}\n {}'.format( path, ' '.join( [str(s) for id, s in list(streams.items())])) for path, streams in list(self.streams.items())], ), ) class StreamRequestHandler(http.server.BaseHTTPRequestHandler): def __init__(self, *args): try: http.server.BaseHTTPRequestHandler.__init__(self, *args) except IOError: pass def do_HEAD(self): logger.debug('Got the following HEAD request:\n{header}'.format( header=json.dumps(list(self.headers.items()), indent=2))) item = self.get_requested_item() self.handle_headers(item) def do_GET(self): logger.debug('Got the following GET request:\n{header}'.format( header=json.dumps(list(self.headers.items()), indent=2))) item = self.get_requested_item() self.handle_headers(item) if isinstance(item, pulseaudio_dlna.images.BaseImage): self.wfile.write(item.data) elif isinstance(item, pulseaudio_dlna.pulseaudio.PulseBridge): self.server.stream_manager.create_stream( self.path, self.request, item) def handle_headers(self, item): response_code = 200 headers = {} if not item: logger.info('Requested file not found "{}"'.format(self.path)) self.send_error(404, 'File not found: %s' % self.path) return elif isinstance(item, pulseaudio_dlna.images.BaseImage): image = item headers['Content-Type'] = image.content_type elif isinstance(item, pulseaudio_dlna.pulseaudio.PulseBridge): bridge = item headers['Content-Type'] = bridge.device.codec.specific_mime_type if self.server.fake_http_content_length or \ pulseaudio_dlna.rules.FAKE_HTTP_CONTENT_LENGTH in \ bridge.device.codec.rules: gb_in_bytes = pow(1024, 3) headers['Content-Length'] = gb_in_bytes * 100 else: if self.request_version == PROTOCOL_VERSION_V10: pass elif self.request_version == PROTOCOL_VERSION_V11: headers['Connection'] = 'close' if self.headers.get('range'): match = re.search( 'bytes=(\d+)-(\d+)?', self.headers['range'], re.IGNORECASE) if match: start_range = int(match.group(1)) if start_range != 0: response_code = 206 if isinstance( bridge.device, pulseaudio_dlna.plugins.dlna.renderer.DLNAMediaRenderer): headers['contentFeatures.dlna.org'] = str( bridge.device.content_features) headers['Ext'] = '' headers['transferMode.dlna.org'] = 'Streaming' headers['Content-Disposition'] = 'inline;' logger.debug('Sending header ({response_code}):\n{header}'.format( response_code=response_code, header=json.dumps(headers, indent=2), )) self.send_response(response_code) for name, value in list(headers.items()): self.send_header(name, value) self.end_headers() def get_requested_item(self): settings = self._decode_settings(self.path) if settings.get('type', None) == 'bridge': for bridge in self.server.bridges: if settings.get('udn') == bridge.device.udn: return bridge elif settings.get('type', None) == 'image': image_name = settings.get('name', None) if image_name: image_path = pkg_resources.resource_filename( 'pulseaudio_dlna.streamserver', os.path.join( 'images', os.path.basename(image_name))) try: _type = pulseaudio_dlna.images.get_type_by_filepath( image_path) return _type(path=image_path, cached=True) except (pulseaudio_dlna.images.UnknownImageExtension, pulseaudio_dlna.images.ImageNotAccessible, pulseaudio_dlna.images.MissingDependencies, pulseaudio_dlna.images.IconNotFound) as e: logger.error(e) elif settings.get('type', None) == 'sys-icon': icon_name = settings.get('name', None) if icon_name: try: return pulseaudio_dlna.images.get_icon_by_name( os.path.basename(icon_name), size=512) except (pulseaudio_dlna.images.UnknownImageExtension, pulseaudio_dlna.images.ImageNotAccessible, pulseaudio_dlna.images.MissingDependencies, pulseaudio_dlna.images.IconNotFound) as e: logger.error(e) return None def _decode_settings(self, path): try: data_quoted = re.findall(r'/(.*?)/', path)[0] data_bytes = base64.b64decode(urllib.parse.unquote(data_quoted)) data_string = data_bytes.decode() settings = { k: v for k, v in re.findall('(.*?)="(.*?)",?', data_string) } logger.info( 'URL settings: {path} ({data_string})'.format( path=path, data_string=data_string)) return settings except (TypeError, ValueError, IndexError): pass return {} def log_message(self, format, *args): pass class StreamServer(socketserver.TCPServer): HOST = None PORT = None def __init__( self, ip, port, pulse_queue, stream_queue, fake_http_content_length=False, proc_title=None, *args): self.ip = ip or self.HOST self.port = port or self.PORT self.pulse_queue = pulse_queue self.stream_queue = stream_queue self.stream_manager = StreamManager(self) self.fake_http_content_length = fake_http_content_length self.proc_title = proc_title self.bridges = [] def run(self): self.allow_reuse_address = True self.daemon_threads = True try: socketserver.TCPServer.__init__( self, (self.ip or '', self.port), StreamRequestHandler) except socket.error: logger.critical( 'The streaming server could not bind to your specified port ' '({port}). Perhaps this is already in use? The application ' 'cannot work properly!'.format(port=self.port)) sys.exit(1) signal.signal(signal.SIGTERM, self.shutdown) if self.proc_title: setproctitle.setproctitle(self.proc_title) self.serve_forever() def update_bridges(self, bridges): self.bridges = bridges class GobjectMainLoopMixin: def serve_forever(self, poll_interval=0.5): mainloop = GObject.MainLoop() if hasattr(self, 'socket'): GObject.io_add_watch( self, GObject.IO_IN | GObject.IO_PRI, self._on_new_request) if hasattr(self, 'stream_queue'): GObject.io_add_watch( self.stream_queue._reader, GObject.IO_IN | GObject.IO_PRI, self._on_new_message) try: mainloop.run() except KeyboardInterrupt: self.shutdown() def _on_new_message(self, fd, condition): try: message = self.stream_queue.get_nowait() except Exception: return True message_type = message.get('type', None) if message_type and hasattr(self, message_type): del message['type'] getattr(self, message_type)(**message) return True def _on_new_request(self, sock, *args): self._handle_request_noblock() return True def shutdown(self, *args): logger.info( 'StreamServer GobjectMainLoopMixin.shutdown()') ProcessStream.RUNNING = False try: self.socket.shutdown(socket.SHUT_RDWR) except socket.error: pass self.socket.close() sys.exit(0) class ThreadedStreamServer( GobjectMainLoopMixin, socketserver.ThreadingMixIn, StreamServer): pass pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/000077500000000000000000000000001364015200300225105ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/__init__.py000066400000000000000000000000001364015200300246070ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/encoding.py000066400000000000000000000043701364015200300246540ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import sys import locale import chardet logger = logging.getLogger('pulseaudio_dlna.plugins.utils.encoding') class NotBytesException(Exception): def __init__(self, var): Exception.__init__( self, 'The specified variable is {}". ' 'Must be bytes.'.format(type(var)) ) def decode_default(in_bytes): if type(in_bytes) is not bytes: raise NotBytesException(in_bytes) guess = chardet.detect(in_bytes) encodings = { 'sys.stdout.encoding': sys.stdout.encoding, 'locale.getpreferredencoding': locale.getpreferredencoding(), 'chardet.detect': guess['encoding'], 'utf-8': 'utf-8', 'latin1': 'latin1', } for encoding in list(encodings.values()): if encoding and encoding != 'ascii': try: return in_bytes.decode(encoding) except UnicodeDecodeError: continue try: return in_bytes.decode('ascii', errors='replace') except UnicodeDecodeError: logger.error( 'Decoding failed using the following encodings: "{}"'.format( ','.join( ['{}:{}'.format(f, e) for f, e in list(encodings.items())] ))) return 'Unknown' def _bytes2hex(in_bytes, seperator=':'): if type(in_bytes) is not bytes: raise NotBytesException(in_bytes) return seperator.join('{:02x}'.format(ord(b)) for b in in_bytes) def _hex2bytes(hex, seperator=':'): return b''.join(chr(int(h, 16)) for h in hex.split(seperator)) pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/git.py000066400000000000000000000027751364015200300236600ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import os GIT_DIRECTORY = '../../.git/' def get_head_version(): def _get_first_line(path): try: with open(path) as f: content = f.readlines() return content[0] except EnvironmentError: return None module_path = os.path.dirname(os.path.abspath(__file__)) head_path = os.path.join(module_path, GIT_DIRECTORY, 'HEAD') line = _get_first_line(head_path) if not line: return None, None elif line.startswith('ref: '): prefix, ref_path = [s.strip() for s in line.split('ref: ')] branch = os.path.basename(ref_path) ref_path = os.path.join(module_path, GIT_DIRECTORY, ref_path) return branch, (_get_first_line(ref_path) or 'unknown').strip() else: return 'detached-head', line.strip() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/network.py000066400000000000000000000063331364015200300245600ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import netifaces import traceback import socket import logging logger = logging.getLogger('pulseaudio_dlna.utils.network') LOOPBACK_IP = '127.0.0.1' def default_ipv4(): try: default_if = netifaces.gateways()['default'][netifaces.AF_INET][1] return netifaces.ifaddresses(default_if)[netifaces.AF_INET][0]['addr'] except Exception: traceback.print_exc() return None def ipv4_addresses(include_loopback=False): ips = [] for iface in netifaces.interfaces(): for link in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []): ip = link.get('addr', None) if ip: if ip != LOOPBACK_IP or include_loopback is True: ips.append(ip) return ips def get_host_by_ip(ip): try: return __pyroute2_get_host_by_ip(ip) except ImportError: logger.warning( 'Could not import module "pyroute2". ' 'Falling back to module "netaddr"!') try: return __netaddr_get_host_by_ip(ip) except ImportError: logger.critical( 'Could not import module "netaddr". ' 'Either "pyroute2" or "netaddr" must be available for automatic ' 'interface detection! You can manually select the appropriate ' 'host yourself via the --host option.') return None def __pyroute2_get_host_by_ip(ip): import pyroute2 ipr = pyroute2.IPRoute() routes = ipr.get_routes(family=socket.AF_INET, dst=ip) ipr.close() for route in routes: for attr in route.get('attrs', []): if type(attr) is list: if attr[0] == 'RTA_PREFSRC': return attr[1] else: if attr.cell[0] == 'RTA_PREFSRC': return attr.get_value() logger.critical( '__pyroute2_get_host_by_ip() - No host found for IP {}!'.format(ip)) return None def __netaddr_get_host_by_ip(ip): import netaddr host = netaddr.IPAddress(ip) for iface in netifaces.interfaces(): for link in netifaces.ifaddresses(iface).get(netifaces.AF_INET, []): addr = link.get('addr', None) netmask = link.get('netmask', None) if addr and netmask: if host in netaddr.IPNetwork('{}/{}'.format(addr, netmask)): logger.debug( 'Selecting host "{}" for IP "{}"'.format(addr, ip)) return addr logger.critical( '__netaddr_get_host_by_ip - No host found for IP {}!'.format(ip)) return None pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/psutil.py000066400000000000000000000030631364015200300244040ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging import psutil logger = logging.getLogger('pulseaudio_dlna.utils.psutil') __series__ = int(psutil.__version__[:1]) NoSuchProcess = psutil.NoSuchProcess TimeoutExpired = psutil.TimeoutExpired def wait_procs(*args, **kwargs): return psutil.wait_procs(*args, **kwargs) def process_iter(*args, **kwargs): for p in psutil.process_iter(*args, **kwargs): p.__class__ = Process yield p if __series__ >= 2: class Process(psutil.Process): pass else: class Process(psutil.Process): def children(self, *args, **kwargs): return self.get_children(*args, **kwargs) def name(self): return self._platform_impl.get_process_name() def uids(self): return self._platform_impl.get_process_uids() def gids(self): return self._platform_impl.get_process_gids() pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/utils/subprocess.py000066400000000000000000000054641364015200300252630ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . from gi.repository import GObject import subprocess import threading import os import sys import logging logger = logging.getLogger('pulseaudio_dlna.utils.subprocess') class Subprocess(subprocess.Popen): def __init__(self, cmd, uid=None, gid=None, cwd=None, env=None, *args, **kwargs): self.uid = uid self.gid = gid self.cwd = cwd self.env = env super(Subprocess, self).__init__( cmd, preexec_fn=self.demote(uid, gid), cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) def demote(self, uid, gid): def fn_uid_gid(): os.setgid(gid) os.setuid(uid) def fn_uid(): os.setuid(uid) def fn_gid(): os.setgid(gid) def fn_nop(): pass if uid and gid: return fn_uid_gid elif uid: return fn_uid elif gid: return fn_gid return fn_nop class GobjectMainLoopMixin(object): def __init__(self, *args, **kwargs): super(GobjectMainLoopMixin, self).__init__(*args, **kwargs) for pipe in [self.stdout, self.stderr]: GObject.io_add_watch( pipe, GObject.IO_IN | GObject.IO_PRI, self._on_new_data) def _on_new_data(self, fd, condition): line = fd.readline().decode('utf-8') sys.stdout.write(line) sys.stdout.flush() return True class ThreadedMixIn(object): def __init__(self, *args, **kwargs): super(ThreadedMixIn, self).__init__(*args, **kwargs) self.init_thread(self.stdout) self.init_thread(self.stderr) def init_thread(self, pipe): def read_all(pipe): with pipe: for line in iter(pipe.readline, ''): sys.stdout.write(line) sys.stdout.flush() t = threading.Thread(target=read_all, args=(pipe, )) t.daemon = True t.start() class ThreadedSubprocess(ThreadedMixIn, Subprocess): pass class GobjectSubprocess(GobjectMainLoopMixin, Subprocess): pass pulseaudio-dlna-0.5.3+git20200329/pulseaudio_dlna/workarounds.py000066400000000000000000000305561364015200300243110ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import logging from lxml import etree import requests import urllib.parse import traceback logger = logging.getLogger('pulseaudio_dlna.workarounds') class BaseWorkaround(object): """ Define functions which are called at specific situations during the application. Those may be: - before_register - after_register - before_play - after_play - before_stop - after_stop This may be extended in the future. """ ENABLED = True def __init__(self): pass def run(self, method_name, *args, **kwargs): method = getattr(self, method_name, None) if self.ENABLED and method and callable(method): logger.info('Running workaround "{}".'.format(method_name)) method(*args, **kwargs) class YamahaWorkaround(BaseWorkaround): # Misc constants REQUEST_TIMEOUT = 5 ENCODING = 'utf-8' URL_FORMAT = 'http://{ip}:{port}{url}' # MediaRenderer constants MR_YAMAHA_PREFIX = 'yamaha' MR_YAMAHA_DEVICE = MR_YAMAHA_PREFIX + ':' + 'X_device' MR_YAMAHA_URLBASE = MR_YAMAHA_PREFIX + ':' + 'X_URLBase' MR_YAMAHA_SERVICELIST = MR_YAMAHA_PREFIX + ':' + 'X_serviceList' MR_YAMAHA_SERVICE = MR_YAMAHA_PREFIX + ':' + 'X_service' MR_YAMAHA_CONTROLURL = MR_YAMAHA_PREFIX + ':' + 'X_controlURL' MR_YAMAHA_URLBASE_PATH = '/'.join([MR_YAMAHA_DEVICE, MR_YAMAHA_URLBASE]) MR_YAMAHA_CONTROLURL_PATH = '/'.join( [MR_YAMAHA_DEVICE, MR_YAMAHA_SERVICELIST, MR_YAMAHA_SERVICE, MR_YAMAHA_CONTROLURL]) # YamahaRemoteControl constants YRC_TAG_ROOT = 'YAMAHA_AV' YRC_KEY_RC = 'RC' YRC_CMD_GETPARAM = 'GetParam' YRC_BASEPATH_CONFIG = 'Config' YRC_BASEPATH_BASICSTATUS = 'Basic_Status' YRC_BASEPATH_FEATURES = 'Feature_Existence' YRC_BASEPATH_INPUTNAMES = 'Name/Input' YRC_BASEPATH_POWER = 'Power_Control/Power' YRC_BASEPATH_SOURCE = 'Input/Input_Sel' YRC_VALUE_POWER_ON = 'On' YRC_VALUE_POWER_OFF = 'Standby' YRC_REQUEST_CONTENTTYPE = 'text/xml; charset="{encoding}"'.format( encoding=ENCODING) YRC_REQUEST_TEMPLATE = \ '' \ '{request}' # Known server modes YRC_SERVER_MODES = ['SERVER', 'PC'] def __init__(self, xml): BaseWorkaround.__init__(self) self.enabled = False self.control_url = None self.ip = None self.port = None self.zones = None self.sources = None self.server_mode_zone = None self.server_mode_source = None try: # Initialize YamahaRemoteControl interface if (not self._detect_remotecontrolinterface(xml)): raise Exception() self.enabled = True except Exception: logger.warning( 'The YamahaWorkaround initialization failed. ' 'Automatic source switching will not be enabled' ' - Please switch to server mode manually to enable UPnP' ' streaming') logger.debug(traceback.format_exc()) def _detect_remotecontrolinterface(self, xml): # Check for YamahaRemoteControl support if (not self._parse_xml(xml)): logger.info('No Yamaha RemoteControl interface detected') return False logger.info('Yamaha RemoteControl found: ' + self.URL_FORMAT.format( ip=self.ip, port=self.port, url=self.control_url)) # Get supported features self.zones, self.sources = self._query_supported_features() if ((self.zones is None) or (self.sources is None)): logger.error('Failed to query features') return False # Determine main zone logger.info('Supported zones: ' + ', '.join(self.zones)) self.server_mode_zone = self.zones[0] logger.info('Using \'{zone}\' as main zone'.format( zone=self.server_mode_zone )) # Determine UPnP server source if (self.sources): logger.info('Supported sources: ' + ', '.join(self.sources)) for source in self.YRC_SERVER_MODES: if (source not in self.sources): continue self.server_mode_source = source break else: logger.warning('Querying supported features failed') if (not self.server_mode_source): logger.warning('Unable to determine UPnP server mode source') return False logger.info('Using \'{source}\' as UPnP server mode source'.format( source=self.server_mode_source )) return True def _parse_xml(self, xml): # Parse MediaRenderer description XML xml_root = etree.fromstring(xml) namespaces = xml_root.nsmap namespaces.pop(None, None) # Determine AVRC URL url_base = xml_root.find(self.MR_YAMAHA_URLBASE_PATH, namespaces) control_url = xml_root.find(self.MR_YAMAHA_CONTROLURL_PATH, namespaces) if ((url_base is None) or (control_url is None)): return False ip, port = urllib.parse.urlparse(url_base.text).netloc.split(':') if ((not ip) or (not port)): return False self.ip = ip self.port = port self.control_url = control_url.text return True def _generate_request(self, cmd, root, path, value): # Generate headers headers = { 'Content-Type': self.YRC_REQUEST_CONTENTTYPE, } # Generate XML request tags = path.split('/') if (root): tags = [root] + tags request = '' for tag in tags: request += '<{tag}>'.format(tag=tag) request += value for tag in reversed(tags): request += ''.format(tag=tag) body = self.YRC_REQUEST_TEMPLATE.format( encoding=self.ENCODING, cmd=cmd, request=request, ) # Construct URL url = self.URL_FORMAT.format( ip=self.ip, port=self.port, url=self.control_url, ) return headers, body, url def _get(self, root, path, value, filter_path=None): # Generate request headers, data, url = self._generate_request('GET', root, path, value) # POST request try: logger.debug('Yamaha RC request: '+data) response = requests.post( url, data.encode(self.ENCODING), headers=headers, timeout=self.REQUEST_TIMEOUT) logger.debug('Yamaha RC response: ' + response.text) if response.status_code != 200: logger.error( 'Yamaha RC request failed - Status code: {code}'.format( code=response.status_code)) return None except requests.exceptions.Timeout: logger.error('Yamaha RC request failed - Connection timeout') return None # Parse response xml_root = etree.fromstring(response.content) if (xml_root.tag != self.YRC_TAG_ROOT): logger.error("Malformed response: Root tag missing") return None # Parse response code rc = xml_root.get(self.YRC_KEY_RC) if (not rc): logger.error("Malformed response: RC attribute missing") return None rc = int(rc) if (rc > 0): logger.error( 'Yamaha RC request failed - Response code: {code}'.format( code=rc)) return rc # Only return subtree result_path = [] if (root): result_path.append(root) result_path.append(path) if (filter_path): result_path.append(filter_path) result_path = '/'.join(result_path) return xml_root.find(result_path) def _put(self, root, path, value): # Generate request headers, data, url = self._generate_request('PUT', root, path, value) # POST request try: logger.debug('Yamaha RC request: '+data) response = requests.post( url, data.encode(self.ENCODING), headers=headers, timeout=self.REQUEST_TIMEOUT) logger.debug('Yamaha RC response: ' + response.text) if response.status_code != 200: logger.error( 'Yamaha RC request failed - Status code: {code}'.format( code=response.status_code)) return False except requests.exceptions.Timeout: logger.error('Yamaha RC request failed - Connection timeout') return None # Parse response xml_root = etree.fromstring(response.content) if (xml_root.tag != self.YRC_TAG_ROOT): logger.error("Malformed response: Root tag missing") return None # Parse response code rc = xml_root.get(self.YRC_KEY_RC) if (not rc): logger.error("Malformed response: RC attribute missing") return None rc = int(rc) if (rc > 0): logger.error( 'Yamaha RC request failed - Response code: {code}'.format( code=rc)) return rc return 0 def _query_supported_features(self): xml_response = self._get('System', 'Config', self.YRC_CMD_GETPARAM) if (xml_response is None): return None, None xml_features = xml_response.find(self.YRC_BASEPATH_FEATURES) if (xml_features is None): logger.debug('Failed to find feature description') return None, None # Features can be retrieved in different ways, most probably # dependending on the recever's firmware / protocol version # Here are the different responses known up to now: # # 1. Comma-separated list of all features in one single tag, containing # all input sources # 2. Each feature is enclosed by a tag along with context information # depending on the XML path: # - YRC_BASEPATH_FEATURES: availability and/or support # (0 == not supported, 1 == supported) # - YRC_BASEPATH_INPUTNAMES: input/source name # Every feature is a input source, if it does not contain the # substring 'Zone'. Otherwise, it is a zone supported by the # receiver. zones = [] sources = [] if (xml_features.text): # Format 1: sources = xml_features.text.split(',') else: # Format 2: for child in xml_features.getchildren(): if ((not child.text) or (int(child.text) == 0)): continue if ('Zone' in child.tag): zones.append(child.tag) else: sources.append(child.tag) xml_names = xml_response.find(self.YRC_BASEPATH_INPUTNAMES) if (xml_names is not None): for child in xml_names.getchildren(): sources.append(child.tag) # If we got no zones up to now, we have to assume, that the receiver # has no multi zone support. Thus there can be only one! # Let's call it "System" and pray for the best! if (len(zones) == 0): zones.append('System') return zones, sources def _set_source(self, value, zone=None): if (not zone): zone = self.server_mode_zone self._put(zone, self.YRC_BASEPATH_SOURCE, value) def before_register(self): if (not self.enabled): return logger.info('Switching to UPnP server mode') self._set_source(self.server_mode_source) pulseaudio-dlna-0.5.3+git20200329/samples/000077500000000000000000000000001364015200300176445ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/samples/images/000077500000000000000000000000001364015200300211115ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/samples/images/application.png000066400000000000000000000262501364015200300241270ustar00rootroot00000000000000PNG  IHDR``w8sBIT|d pHYs6GtEXtSoftwarewww.inkscape.org< IDATxY%Y^_wbˈȪ}zj4, @H h$hAZM=LOwWugYGwk׶sfwsDdCv\9nBq"V\Nȹs._JN3X @$@✳BS >@<=XmIc}t[BݫH_U bX&3ax4F@˳BιltJRLo__3h|:yxi>u^Tqu'4S!'>IB \|!FBhB᜛p0yYO#AuM|bs}OO6'-> FjС G7~Cx> _J\QD!|[??6ip 9g'u`U)K?|Y#'f PH06+;Hv?g5ި΋Ъ4$ëp|t8xg?K<R5??g97B gt#08WhBa/rl)xwr%!B+z'UFRHՋQ7:ڥPPޟ8b }!Bn1D`s`dNJV^&Ġ%8EɍE{0]-;#rhe 4)@–~Φ߾v$Wl/C`WʽZy<{z'cjJ܀ѹJ*\%Zj<ؙqzQutnW6oN_}{ax;Afwkpwq:@)߯/̀pñܤUma̻=pUnNOnn8U"V̦o385+6IFFgRE !΅9fpSB4:t rGf,B[&vp(748Wpt6i  2~jEozJ 4B(d H) OcbpN &ǁzBFxue%Z ck:ֹ6~dg60!O='_'pB[n)BHIPiTVx5@Ъb *PB!Ut$RA8+hu^r2>]ڗ s5V T(#1a:4i:QDtڼ4M8AhߞnD|B[%$%5^|JjHBBJ,9^JYR !+V>^E%f.="V_yXb[H4cV droYz_7']؛ĵ6`9.عPRjpߠUE "B(%AH,%BHnrvkz/Oe8s,8X*? 4;|~{sxu2QR#FKrg>!pdxF)=g7hO^oqs<=vE$}||<<<gyZDK$}6fy;k&^rNWK[\)6G<8SGiR RF9%4iiTo_J@)JQKdL-Ok*gQHcJbDJ!F@9|.d$ën_@5^З a<K-.BhfNdC1J}Ze !@6' &J+^be=N uAya_̔8KQN5jn=%\ sooCMBJ)B*|Lǿ˱' Z*G)YK[BAG voq\`֚×iݦ"|0F"D^tι 0D)uۛ-]u|>`L_--!hPjo=Nx yghQZ KHrל;c[Mmܦ.Bk)*d,C3<0'hq.C;snjYULXj~,p &/uRx:|NS!-ZUj'ec6e+oڥIvcv$ hgd9i9N}.Ҹ.CYGLM`)컏j]#FSWJB§<ўGG>=uސ)F/hF\b-yF@PRЄUŠߝ' wx4j7):WͣW.07ET Iқ"_*UBLdJy}z)B'Zyx#CkM:3(\zu%<F'dz{ެ/79w*HRI(% G DnjV>{x;^32I!.(cL !K׺B&RBa|5䞆)_)u)H]OlPSUtӜVVz' |ў惼fIu T%qT.Oi3Fl?%,򤼔ޥ O:{2%%R]O 1*2ixS"Lᣕ%AH[_Ӛ3r#RRll4k촛d#P|!`@J ̢˳)vp%.|OՈß$/ĒHʗ`mF**B᜙R쀜;V_b^ZØ*Jog'V;4tRhT|zIWrw4 ;XfP9 ˉJ,U a<} yǼ|:y@O!¯rv PR9)ủ c;*CE:qs/Gc|n8VxVҬjt3T0L\ e5:*ڢJaA)an<|m#Ǽs9[@ ܢT,hʙ4HjΎ !4J'꫄ ,T@O*p(zyVFlU1V%IQ(i"B$X$_0+u]=`㛠ʉRVWj=Fb͒%fae9' 4JbTV$JH1`Kk RxB[uRR<-VthfW19@{48b8tSEIic%Š";tș%<%Zn~p#̐/2@ۘ, q E-tT2_j C73RLu Goyu!; z/}ּ> ^[iPՊ͊VX?8\^hA\,[),\NcK)$F a3_7<ᤸ㔨k\ @@<Ž{Dd{q5V{8z>Bo-dWuriQ"N4!D oDcmxRDKMNv>?Y Y7w6:TCNrVu*8펱M@JJ*7SiVWH1G޺z/-K"E?ya 7\ʍ Ip!."ho :f YO[2vTdr6f[Co2/p!m]5HM~qp@7JՍUj#8uqqxjbsebE6I5J!IBl>B47&Z#uE\[7i6Xpc|?lxmPᴏᯑHCJ)YFܲQYEetEܤqRk!R 6_j+0\"B ;rA8fF^ پ_9k֡;۷(ǸnY4kG膇_U?#=A28|ZJ+`x?]];sH IP^!z9cAȫǿ$S[cU>8I~Plk\($;d)/pQ".qt8UiWyJ 289< ȕ4ޠXnmɤ/sq3)ԫ}e&C99w!6[) HN++ ?uQio{ jm pDpƒ"DK b/X``GcHc $YNo7&MsD`G֫$F$)$uDΤ.HǛ_R(ftwpS*~*EH- Z[:TjcҘDvsP+mܿ匞5d!^{ vxMcV7n78|vV)1u YNeiƊtlݭa+)0&'a( y.>$ *ϓA &M(BHc䞏 9,Z(:d%@h{?}%ݣr>-2+[dzcqc6 *YNkgxZ{8,r61$ɒ`F, :lV 6)YaB2P\l(M,#P&'/]MkT2j8[$:AʢN!@|vԊʝ~zHnΫuL"66=#LB^ (ɈՆFI8-d ̈0INa)TQ9,H)y9A1#Lv@&KRč_TGN L'c1`K$$.Y:ƭg7D6tw.~E7Oqܒf4A)ekrlGQTpJZ-\n>9hL61z\ȳFdYrz{VDf臝$?r yN܅G/:!u'=DgHITN0?ȂmudQ!y^l"WW! yEq+KQ"ϊٮMyK/A{_73rSLl%m`y8"" |_XB y. 줅98y*H)'OȢbs'~6r[8_<},/#YV<3 ٮ)08s5&/3KKU#/K}zYꆊWĥo<7+R}O.~ILϨ=xh410@T}jwH}7ƫ2ifn1acbnxU1y'6`94 aIq#l7ٓ7F>@O IDAT tpn`=!72c0Y~b$H#q[fmXZ_P>xf>CjNܫj'_[:/;3X EdO[Tk>ã=z{d4!N,nU.IqГ|A#ZpϟbMQk/JFlMs$(.zxhHKTMy8'xO.D &jKSg{'u\X{QdL5Ct%qXǝ`琯aqΡM8aÏѾGm?ehrr ANZ@xȓ24aai J6Xnrjo^Ψn SвT;59nQ_ B7 pC'X`Bu8jJ4Nu#Fݐ~x/Ca/~q6KUzoNtjqsbC;ܡv2yc-iTI)`|%ϲa?[$Imatavin5 {#nHx6$3L yph^Ʋm\5c2#F?Ws1!h y 5ͩK9 egx>iźeQ+F']Zwb }o<ؓN΍1x5VEmx8f "gC*Xؤj [RX@p֐:/B8J T=J+VT LnȲ!LQX>?2F>JM#Ҵ>MM7L ׾$h0aQ*!GMv \psWdo )ɞY ;w_|l+jȒJQHG1>YM+Oeǧ OmѲ!oI>?&7s{Iͱzsp{<]ꛛdxtfɐiΪU8&vɲz~ja*M9?_jSiTf 9O<&Zg_b i?Rtی P a-Z1rUe&Y01Y6E>, S뇼yYEyr\k1Y8W2+UY(‘9nQCVGQ8o) me 9zz.q .-pdF)ƀ8ȒtT_ \(=tS5CkMڛmSk8ҡDx^!H&ڏpі^ BͶd&hoGՓك;;/! IS~\W ,Y%I|=΃!>3]m6n" < /8/9^dbX bln#mIiτ` 07 VwqL>c%',gՂ9yJ}FmF'9~G$J_lkyOuDGlZZ޹ &^qISbWQt4wMMWSb,|T<5=yiaxnkiLQe*^ufS\rt!*VJ)2k?[Yن ,ȢG>Y'۟ BAtчx;gd8|NF%jiڝ#v~e2uAo|ckCϟ c,"M7F> C[rdcvҕEދOHf?Ow9ab˲%2.~ y˷@ݳ Bz)0KjwLf8~qxp$L]f}wFgL Q]|9sӧɡuy6z͐]A'Y>ElsI{v`ʚ9a̒4Qmç˷4~[0k -\kotrdvocp%yYppćäk]:ټ Ri;p5^6q""lN/F'_<>#  p)KH Ϭc-`%s oǞh~i~Ko~K`;FU.nYTeo_|Bks9C]Xg4ǜׯ{E 0 rU>n>,$Ah }ߣٌR8|$I8%WL05&ǎi-,ח^MɄP%UUqxxDLJQ`mjS%hGƼض9`6Ɍ5}4 7p]hV߾\׸.m2F;E}`1_~c2Pu]s|9e4~G]UIu,K6wDz,Eu\qmD]ضMeϿu]E4M&)Wl:}+>v4Kx4F4t]Ƕm=Y1͉tFe(B躎4M3H;^^_0 ,˰m_r8u8iǯzŷooiۖ{,,K"g<2L~v]b,K+a4 ՊY,_iǶlڶl?>WE˒ 4Iit:;?iPUE۷o/Fш8yNAp`ZOƣg/x||t:[7AEv;톇5n& 1O[%MӶ-łhu(O7TŠ5t. hwmǼX{Gڢvf:rJcEFbۛEh!XӉ/_eZC-*{@(ץqڬ`>s/!-z%`6}>߮r$MS,$cʲ۷o_Y;ʲ<#EeY9u"8:꺦nnnq$ȒMh|pizoEQ(uN_^, M}VɄBEdYFr&|rI$m\W~f!G}/{=-BGj.mY!s8x!qZ^4CmIUPUu+t:a=( ɄJ稪y4 MӠX,8g B.cRq錢"w0u<3Ḯ\0detXjɷodY O߿ue0wض-˕^^^ <<`Þ,4Isxx|Jgx q5#IkNTuM3ɲow||la*>I]ט&l6[ZE"ɸx9SUQ|gB즪1Lx۾u8jۖłaTUkL&c,#MSzfш~G8(P9Qѷ-I>u]3=Ϣ,um[|>g=my~~AQ ϲitGg;i(0qlzM躆EQ|rIEe},d_Llޱm )ʂ;~ ?Q=1y'"mW%s/AK>qд-aYfyB"Mi[N.Y!TېGUTLÁ5 iu)?ѳaTu (誁2z!m|䰗%kS]V9IuU QtIZ??qB&PXDY!ӉQDW 6ۭ\cH˂,q-hvC&jʮfdڸARfTMMb4p<]Ǵ8LSΗ3i:.]".!:A+GN4*ؚNYܭnHQ iGxhA%R1Ǜ;^  ;&iȴWY]&"N /𩚚tJ; m߱͹Nk(mjdsv,ʂrlI pt1Rz: 61PMg__ ZH/[_^TYDc+^/{*G*LT~|z5 C,G,pS5^neb M ]g$+'|ϣ y]>IcY_|!IʁiO&c~E&B6:i@K(MS4?NppOgTU×/\Wʢׯf9ook먊l6c6p^f_Isiꚯ_?,0,b uSӣh*@gt6e1|0DxGUWܬVO'd:ݎ~9\㘾vI۶LfSʲ7zJPV%?x{GtQ9ق"/Bp<Mp](X.0L( IҔۛ^k\O i\.YIa277+?(r*& ۏZѢ dEQ]˗/_񈪪L?g`ZyqssCH 9m`ٶ%oou777v;rJRl?>ׯ\x],|F$lsAQid2f:DըztAw!bf#uRT%cg<6xʖiY|_PI4"I&9*{E۠:6ɄبTu0`ݒ~c2 #h+$tf)ݎSR3/ijl>$qsbhJ^iJmFPURaUTPO&$IBuxuXr;&F>>0 MU黎xL|YD3O`w4ut6( V!ke `*@SI0 CΧo HUyN,KAeF# v;}tlT|a>^k 75m"ږY8& CvJQ $zj_nXt4kgEQ=B,sKbjEQ$4( Yo7要Z %\\M` 6^#>m yӴ-(6 Uנk:a 1u4@U"ϣ:D4$S5"I*r(l0 $1 ;yK<8Vyb (h}OqɄ}^]'<"(\5 cg^i*m.q`& \? >z] Z<1tӎa aHv #sEuʺu%zTUA9zAPPW<ד2 )) E.gTEA7D{Xp9_PuNt躆u뚚{otd~|:QRY%//|$2fa@T$u\9<\+Q75A>m߿Ͽ±:xzzB5Pgݎ8Lo US1Mk|aZ_c1dy`^Ct麔u]Dz-6 I5boN4(}H|1zrwwiPMd; ]יL'zu €bcYR5N\4*.($<~{?4 ijV7+^ׯuC4DKKܐ)Ig9=w"ˤ2zw{3MSj*әՊ5<>~e*?(x߼Z.x_<,\ۻ[v#Ub|p<0"okOnuyxx`45u]6Ŝ|g!ghX,0yx?](%ɲS)OOO ]kZ NYH]t:KjiI{@6dYFvkQs3Lhhm۠[==@* |%ojhFmiЃLgS땦F-$koӊ0 l7C}Ih[myzF۠w 4*noo/ضnunno}|Piq<X,փ.mX6]Ni*ASr^r.ѤB%LJמ;XJ|Jryx:ps+툚*\lJv5NEt<:Y;ٌȩ2|?BzzLMg49i!" 8{>0 Cu1i)yqqlp^&H.e* * iW%ӸփTMMTh9US6u]t˵ﺜzU }DAӋNnٌkm^w) Q7XIDa%EK_4BЊ۞瓈*1PҌqzrb<ߣ{:y>eUuUy>m Hc4E4T]|eҶ `&MUC8 ՞Q9gˆʼndTUQQP(MVtTw. 4"SE{bMn\ӔYN\gJ q[rvv }+mr9Gg :1-Q)@+8X+gDaDgԢAmcq8Dt4eFwidu:e蜯!{>OO8u]3Y_PgQ@lXZŜ(4LB09N<|y]$hՒ,B~t&M3 @Q`<:4]*˅lx:Ѷ _~ay L?C'a{J[nhrz}k ӎxS%h \Χ+X,^\7Ug;\.4 ײ #MQ /q\0q- frt,%U0h+J~&GQ|@d 75 qRuI!*E5eQ HRb_@_ uNzwz:8j*Y&*˖fzdc׋KWa *8x~zC;(@'I~prXy3Iiؖt˗f2mKQC6*1 t堈ZfMHWMQ4˶)X@bY6Li\fJV_40z:KTjTe|d,U}. ;XEYx\ʲ)K /r|N(b`{.uUaҗ^*"TCHb$h*m+,ϰto՞dB\.,zyyya<sTr;eU#};4, | ; `$ 9wT]#/r9HuM^R%&1דa;6)F#D'%ze=d,zZ傗o߿ndBS?Cޯ'c"Ӣz)_.N|—%ό2,[gsISiix]`k yMb)C}c6}0Yy(ʂd"֦a:ugЩ,K?߾ʷ!aZ{{Ҧs7%'s,`2v4 0 Meu:€*~h4_gf q"Cnٔ?ijF1}1E_:\G. \ bJfAUeﺎdJYdYF]Gnon8ONG&1qZD4QDUU'#8-ѵܬnx %@9tg톯_%?c\ @:F]W<aTͦ$yzaE2i;\ǥeuX~_"D?CJٔ-UYҵPQHYUFf^>\}>@5VHr<~lMjUQdY:(ɘ+cY&W4k,<.3γr*YCץ:TEIy4V4u-U0ȸ˴8O89g3ڠ,s8S*Q*m0տ(rf6ъ1Qlm8vc &HYTeE &1UQ"-LY;/K' $yE!˅}_^T01ز\AQ)u])\K`*^~EQ@Suor$˩TUXMUV4 5u(s=hLQ[AfhN7MMIZKnC(;lˢKmYY;"2 4u(+TMñ,4Bz"iKڣ5n>糤MۢIrBe*XX#fi ,-M#Vg}G? 2'Сi *SF}iJʐH* ԾGSi6 MUhۚhD_7Mg(S5g1qb!()ږr9d"bN=x\Eב& _)ۑ{N#AzvoV+~=b>)EUpp/-Smr`pfN8R:lp$(,S˶>(r$1hDQ|nsd]t:A$ٷ7T ^^2qo0*\R5 ۲rTTBnVơɋiL&o6th\XpxӶQו prwK]hx2ׯҗ:$YJE>dLEd~[jڶ 0 5777K`i(}ۏ-?0 ߝei ̲ɲșͦ۶,I2FǸh2c]'kx,}۳ٔ*+?c]Y1 "-"m3 0GG ZUn^q3N9ت!*Hia mشM{E!P'1+u]K*LہE5ƔEEțqF{-ӦD/$Ie8qOmY&"MQPH2v\mIn4mް4'?y$gY_KϾ͒Lf$ **^q^ԫr/DD DT%@"$ Y6If9s>ګsd&dL|I2S]oVW.OuDym[a$+U<%3,eL)Èt8JуӨs㜜1vX16 Iar(b8kZ#C<# b$P* ,Rt]d۸%}{i*A`cZz,K334׋rЊWIXa`P,JY_^LI5V.$<< o9I\Z)4klnL}㈭۶o>( N4MZ+gaѱU\.v)W˴[m,"Ibmmd ZG541(qWd.ܲezUU6NNMMaZI >@4M:Vn pbz^Sѓb CSccb|5(B33E#\8iotr@,קn32R)9T" y0˘/F+fiTe., ^)V E (22 G<f-߿HQ,Nd}^LPQ.p,JpXi4FâWPy.y SSp1'WLUlYX8X͈c4vb&N0d}8C~(HnS\]z (6Wy}|8b@R)F,s#Om'p6fVVԙRh4|&Yk6w69%$Kxc"j*7l8_9gzzn|ߧZ-AqIXKƢbE\fg}}rv{NX<99A4V u({UuM>}87۲6%N4MKӤi/>YVTE4쌊hctZgŽ'm4"MC!ql׊BLDΡR.bt=*RiEOz'4dӄ$FWբ:N)MZ驩EiZ?i6W1Mx FGGvzv\y֭hes[`0N`;ŰsT*eO\޼JE)9%Bz aS "q],ul1^!^)`PmAq124D3Rmjܕ-`0Oc]ئAS" BEK`/DC/Ө0Urhڅ'`X]*[e+ӄleDa$qt50]6\M]+r\bB2'x%/_5AC1{( TjEOkXjj[̷b0b1BzN)njF8(pJ.u( RlLUc-(@IDTG4W)*uȡ?dT ~C7G:aZ-~KkAP< <4ưMKxk֘?)YZTფi>cQG+E钘'4#7t3h4v‵nQQzaV1Zq`m!5MbGGIVZc39gCt8lt}(b4g.][oʸ]dt=4.>;涰p^“LM5!h cFJ~!Sspq!a:~/ <,JjSSz4u cyeyP1>1jsX edyNc80t(DQ&ryVdq]QZ6Fzbᔢ*{hxivAX Rre}_|ժmU;<:(nC^+F6Edmm oP4*ma^+*,,,¨.b_L3\)H^mTkU.zTJ{=L`rrVkNqܵTehTm?n;KTՍ4vCӋZ(˫rC5XZ^$MU,qPR:.kE0 auu H1 0355A8)wEGGj1br$^JkXDAHe,Uℊ%~cp*Vݦ5=8tm>ib(*NH)F:n1~'Qja14i[LOO<#igZ8u4#x^0 |X`Y~$)FE0(k!d)rVk=#u<J\plNMÌ`!a"MS%Au}R2AIᘬPy]õYm Gk]ףfk #2ut<猍Sj0ph $-MJ (FGGGYXX() a21:κC9*l4!-B!6EVjJT+*kkYb&NjJhYшB!b0M( ^۶)K} @/6yQ^Dz~h"h!B鬶y8n ۲}$N?.:^d넻 IDAT%{C˿g>vLӤVjM(\,Ӥz&v;NCkhiH !i((\o]s~s-o! 4y]nWyn:δp%W[my'r!Տ>w?i1K%ON=;⦅?+P5?bڷtm4q˨*2V]Q C' g An躁1,TB n7&Op[ݲ0U 2є#|^)EQr(eYF>'?5)yݍ~ήqO^=O|O硐>_]vveQY1ͼaǟ};O{ ~v7iFV>WK9;ң'mQ=p-Z곴\gxFð׾Wz}9~5~7~K&Sxw7N^ow+/o%2Lɾ`½t-7?U\PW 1Q< aht3YePN'0oZ_` g3ȿ[Shy T0Gtmge>I %($ɇe* 8%#p !Sdi( [晛7]an:v٢Qk9JnKR!#:D !N\7^Wt/G'{W(W'8ZB)y_V>i4f'1FA?ūs٫_'7r'&۟c\6'Ǽ)e9?w7͟;Z-ً_8ȥ:vm ͟k'}oˌ1T?g]oySyBG.;qw ,MSwH'NZ1ZaXTU2ڼY;DQHrPU(o/pc(Tq S4lAWc~j٘YF cLP0qؖJNE~Ǐpb:>avG*O!eyL>qAD1 (4M4C4a{A yOyJWi0p}iEG]2HGI?aˌ|{N{IeGv[HAdq osfq}}m4eNB<,9 bY6(I3RnV6%B!bөT*EFA؎K>T*g6BBJ>'Z!Bl.mJd:)j$I4u:AkccA!Bm4]#''cfffH(Wet[]F]\B4ݖDP!Bl:NDdYhA#"4'rT2T8O}NB!t|GQj#ϋjnaS3t:b? !BMGu4U.:i eZ >i3154C#JDP!BlF4m;ATU(f ]U/2=AT>Q;!B!f, ](W*u-뺎iXZ"Mh U-z,-/IB!Ħ':RV]V5Qtʆ$$ĩ$B!bq(u~Gtm(²-v(a8Xzk^DP!Bl:aRMus8GڢP.ZV(%Ǒ !BM4MGfhNg8k6qZK!!B!ؔ0$"4Mc?@Uܒje clЍ|t$d>A!Bqm^0Q8˥˘I躊iZ,//c*ty!B0??ԟI8Vta;Upz+&i^_G '4M-!Bq̲my}$AU#Lq(D7M AKq]8PrMB!bQbZs8CJ%6gi IN4F 扖FB!8eiv 0,vna( #>~$M1MuY[[ !BM'Nb( iV N$GCZx$ !BM'b,Qnt8zi*a>QU%B!bu4 ?쑶,N`踶C-& 퐩 =ߗ !BMql MS,#"!2ybYڎsZVTN ( TV\sرcQM6׾92g︒ksy3h@޿/'%ON}!L菻c狹xO8 Y;>u%~'1窏 W_c7ܱl(Gs.~}z֧O渊9O(uus+hq?Us׹i06VBWzgGqN,}+Gu <̗PI_n䠽m/;Yz\-q#V-iG={$ie؎C(* htZm$\djz~Ev(5q.?8m 0vƴӻ߹%py֋O1לē&@{Z싦<՛yț`VЫɉӎJiva``s+'z`[9w9s$e]]k*c'^«~wdfL?B sm2u'^yCW?/;~]g1.f{Z8gf[N݆hU\qUS_\0лSW󣗑_qnX\R =5_ǽMT?g?t&.)>kUq;cu\* zH0u|mvVƹ]̩# =7s)'O܋cgM{z? _n?O ʓ'Äк&}˾FQ%owxck>CA@掳gs Lj˺|V=/sgR0魶 gNr=@n+2|c+x+{7IlEPT &_|u[Gc{=W~#[=ī_WKzܥAJ[n},tC'u:a1;\'o/w q# *<\}ב_W_i]G<8oJ~tuIg9Ir C2LcFYX8@VbS_V?RxWocc\s g򗌳|Ǹ~Kwx aCG~կnПwN_-7 ~.{?ۯ;~n.Y?nө=x%o|0^ke;Q =fUoFK ?(Y:;_T=?{Pו>կ"q;SNam'>||e^r+gX[s|^tQ k?oA,K95I;rշy7|CW/a5|iS?|=_k ,^xqsBϨȥq <.4囮fw4ccwQWVF~N<wz]nzCߧ|gUgpۤfjau2[J T{e}JxpF5V cG_+Ŀ҈c;%4M4<Ѓn+K)aVڝc4x.'7|ӟಋ`pYu?>?ᅲrԣyw:Tc=BUn6ɷ-/#F>=z/na@?fO.߹Dop)䇉㾝o{5}sz$_GMVsaOk3e7w;X[GQ c~31>7},0Sl2-qՒ}5q~er5WsYu=?GYW9VJ026AA7 ,ƶm۶vI]өV4F div}_u w\W߁8KYOԽ<1=eA>dg|G1}\wWsۃr{ ~пsۃr,́^oyz A?InOkl;:{kj\̋r^}‹Oǽw.#"kK[ ρi.W} Thr]*̀*'-xA'0CnNm6ʬrlxE eF wB1F[1ҨeT {o_vl3^^[B=F(Vg1ӻ^}F㨱_wHw4пk (㏿V<mۨJZ!},%^$I޷˶ CQ qekg?8 _̶aN1fL^zVCW_I=%> 3Ky_xGM׆Rx}}7]R)^k3wkymx#7*&+| zK~MG.+fyY;rh4# :H IDATNl}2yæް` *sƁ/s+uDcl^tI_?DsL>[֟.}g yC?]rݭNI%yuѻ|g/iC9rl#cԉzTg0sf g.}y`}St7,=ϿxQR~2_O~&>}}W\|/4ݷO)NB5e/\PW)Gq?zΙJǾwOl^eab:n`࡜3cm[ıG\D74$Tv}_~QyDh̫.4% i#:p 2;_Wߍ25C5Ǘ>|h^vsTk:~=#ڱ]RWb鬷h4{Le1ydYazfIe؎FZgkue^H:qML̝x978e+xM}d.}3^,KH +%8Buץiy*+{c>Iqטaգ^W#2C!B '0Wނiٔ ^Eh4FJj(JL0qZhJרB!|NE>3>9E%7A:8 !BMu]>A0`||(Q55BFG(:>ak.B!t%0 |@V 8ZrKL(¶mfgx%B!b!*2Q0T*:ippEP\-9\\\.KB!Ħc6a6kkJ,R-QHRe۶m$w'B!t~ޠO^guuY"(B!6qTaQg93t{}tMSE7tl;o%'MbPB!t* 9I`ss~@䠚޽{LEJI05=Ͷm$B!bz4MRaiifarI'޽B\&'0  !BMGQJ%199 Ev-[<902==KKKA!Bqjj5>iiRy~2nTb˖--L$sB!t äR)Z_JJD"Q8!Kwuϔ^n/wR)*U,iR[xDTDAAADAҔD?@!$\/{ٙB .;3{o~?!BL:L&i4&Lh$J6mI~T*ʥlD{DP!BL:tnzMLetdP0j8a:m9fS"(B!&׋]n"|F#͠nǒ)~?4>t&M: !BIjZݭvхBZƶmMR&EVE4DP!BL:)Jhꀮi,z$Auq:;YT]EtDP!BL:hq4Mxq uAQGmQHӔ+,e$B!bCUiTUEA-l6:H UՈH2A!B1<^:JXU!9B@aB.w >66v'l%3"l>> z$B!x֮[c&YF(8ttQeQUH(6 :  &n#/ի$NB &s6n]-cBN+m#tʑ/B6 qUQ,62fP%:(x}~LBUUp(CC+en7P>τ,ԪT ic6 oчyȍsaNa&LB!x\v_ll6-t]=N2Bu jB'ё`xt.V|q&戅Mj@oS7z9JMqx9|{'W "뼸>'~ru<2n(~|(ޑmDMA9琓Hq ?m3}1|, _B!x#ꚊÃ=m:RU]vލwFWV  L&F8& 3>D,aTUesR3 Y(v0-8d OPq4d]%}4~oϏO奔=q5YH2z}^SNoGWo=_l컒#O׮;ɟn >|!⭹wfjn7?n4Ucv˲0-4Uj<e6 K^(\"`̙,_fIOOi2m Y(i% dhLA}gNܱf&FXiӲq7N*ڛpn TwY@v637_? R:hKK$-T !-zvJS)l8O>jm__"]{JtNZ(\V B> Z聠`(Ⱥ!<>$ v Y(ہeaxhm ,~Ƭc]]cB mZ˾Wׇd|_rG6 Z V\|'$Olw8Kˣ`s4睱O%}&Gϰ?oo1z%ѝ9#k8gM3s kL='\p6/)/>~l-W (.ɏw Ʈ蓞dF8Kצ5:_pGѰ-~/4U/r{=|!ͧ'ŏ2sLR$p˚r\sǡ^ole4bD8! Q-WmSQ].uL$N yp]3LIfax<*T:!ŋxb6bxךVM5W~{ l5'|=f~F/8@D?pfs/_U(v92e=κU|{kͱE njI~G):`U=kY?qO؃EW|uߙ]~?~t򨹼JnV<|ל`v֥pwڹ3N/?A;\vtڭ7B!tu,[gV{a;F},sRE àl6xnmdjF웡q`/_~͏s Oz.Ğ>pt B|2xP䠃OlMju%q28ǕHSv@uGuaf͉Si3fna/;'g>%^`Dq(>uVJ W25l@uǘGB37'VЄӷT~BmmwP!qNF ¤) F\Bv?k8\%mkce{Ӯn]u,1U^Zkc7_{[xuK|钟p&z0f!eDzz-BM[o ZB ?pT*M|M*o?ŢfD{$PP(Jb1"evDΥRa5J*Bͷ51f&AӲXFO8 [:|雬Xl{vkbnoܷlB9oʇa=F!4z?:5ˣt|u D a/?_KH&o-Wi.gr9*B#Oac;`֒ nN%7mA!?-cN6v !bcsG'PU)WJT*5,I\##' s衇rSTFYZX,Ro4XVjF<Gwx b6:`_ŪU+$OBh0kP/Ye;Z}/>V (SG&6},*.'M/S GP[sLe .5 TV+& þSuPBlԞ,W7?sGOOcg~OwzPlcfqi;9?~~/w±l FܵO?DiVY]OO{>E!ŢQ~s9k_eSOSg}h6_,kƁіWul"`&jMf(_gN@?U' 'A:\-Sb…o-{Z|ؾ4Ef%Kn`/eVw;܂&C/.?p6KCq盷ˮ>B!z9W,y L-;ƱLjpLJL&zE<ӦMRPV '| +=@%5L%zԫU,݇W/N1@gfW;߀:n)GjLm50 vԖgG0B2gl.y|x62> T+E! ( >|>Ogg'hQP[ɩLjBJQu8$7,yp( [g̙ځn$*oo'M0T:M{{;p|!O($HB!ĤlZ|>JP(BRaʔ)46z4v/}^i$B!b҉Db 4&pt6بS,3ff*[%B!bi4xtlb*~׋Ԓ%tuuf.7>E[[LZ"(B!&Z@Z#mQ7)dJ mǪU+6u*a` RLV"(B!&׋Ljݥݍ^Tho`RȖ`4 !BI^07F:9dPTx,Ƭ3IS~܆B!t¡0Z Mq+A2,. \dY2 c===A!(2Yn5CXu蝅KwIŦxf0xu,phǸM7~xGunA\D"Q "0\=HtdɓQTpu"vFǤ&Z!+i6-vi xSuV~VdI{b_U6[{'ǮK7c/NQlV70OJG w^|yxqWr`;!X<}Ϩ/]Žgሣ==S9lT E6=h4 Ba:8Q֭[Kgg'^ӧKTV9"vjfM'a0{f]Z(#-洋~Ğ'vn~|J<nra8!rΆY/˰1G1ot%7s+O<^@ؽӿ!,(\;߮78ïnμ=7on{q{. 4MFӬ)auO?m(B `ժUx^B,U>N,twU? v܉_I[Nq!U*e<4.`0sϱEnhU34n9;n)4N%d{ . O}N=o*vynЮ=4Fs#ÍFH$ԪU cll =F-Ns%O0D fn`9Q9bbdY!ZN;$WN@Fl5xA}v/{ġ\Ю :ms-N^1"XFF<eߏ:|`0S) >sLޖ⟣4<}^u'3~˜u*T!{G|@vy'q ]89{ !x?hYVGßQKWO3lumhryOwe緗5W$ERq.v؂_΢3\qX^oSt-6i|lvj:zEQV$:x^rLgg',_NT"i:H}ќ1/=Bifi %Ixi9\<͚U9>f}>r9;Gs rPםP|R.;s0[W5sȮW?1YOe8nfu뫯.] v &_ߔ9 m7*Sɧz)qJ,ûqorc&mmmr9|>?Çm;|>l.G&%Cw9s˟]N-D3O5sM{A|Y$wzB|E[U`Ӵr}!ο1EAFM/DzU4MuDcv]Y__{& oi^p|_xzkwL^?}zYEhߐvgOrz(n׾H=;qo?х\DFQTH$(Kr##̙3kz\eoc|e#ҽΌO~W+{7<$8|ٸ_a=_`+:`o|wl0w, h#>>p/޳wq_ώ߿ZO|#N8m(*jJ3Fl4 Q,p\6^ooK\|ř-_bߔm9ǧrⷷTH&]Q/; I0-ס\)]4o6Z4ʕ0wfD@6c@Q%.]&iL,/-\6GE%HL&#U3sL ##<^dz+se…oiXLTz`u CɢCM@j}g1Vӎ QUM W`Y$iHt$$L&yK;x <>JӨ5fҥٳg'20؏㕨 imn  7]QMWQt],"s !ޖe:nd:UVo QS]SiCCCq$`Ӓ5B|3,_4ϭYq|躋=P?sNfllc& 6P5 Koo/RZeYABC|>zOrϿþB7+Ѩ70 qcx<iYLI.i|>\.7qi,bho`߽/n({RUj_nt$PD&xGƓI܆fI$348H(vNX"NNqptbQnB1qlfڌEwMEQv3zgKdsY"XqT]C4:oB EUU?s<؃ '8*Ex,iDQ D'6np$D*9F\Bs%B ' qAGqrrI`x:::P#nLԩSIRNPҿsA9߅m 0:6T5* 8J=NZ˒hogm:ՄPX[B!i:DBQh~hT4j5q,vmG@?hRD^;B!Ĥdh6\|0(r9jY{H':Uk(ahO"(B!&FݡTʣiC.T*8^> Çgxt*B!o6~Jvqlۍ::cysh/*B!NC ZiR.1ML& =k6L>ǡT*R-B!&h,F.CQ^( ^fCV KEfdt| /HiR6$B!b)k.Ph4x*I[zL:dLP(D !BI0 4M1s].t]hBGAWWP>DO!BL:Niooiۘ0(tfY2308ZN6 !BIX,M*B4*RDG˗jAzy45A?p$b.ABw!xm2ͦC*Du:::q8ma֮[CQ#Q[lfA!B1JE&twP,H$SiTUQ "xRtC A lK !BIl(>*"6.N<'.>tQTf̚5\6K: !BIhP,TLˢ\.c6. 5)c6 niT*eJ:`L"(b#b2xX4h˟kzZ*!bL^7qƔTN2D- (B8&HHu9'ZL (we/SdY0/1bI'*='7:;Ns(-3>^FɁ" yߝC\tm s؉5|f}8e߯'l/s녋n^b2d*vlZk`:pB`7~HńQ]z< ~w p{2 Cy1%FB|4"[i=c9+k9s9܇9g1,NżѕdC|έL?8 (Xؽӿ!,(\߆sW\MOgƌ}/!&j(HV'd1 E)f\6hh#~ġzާ?! 1B!hsк!us&'O}\}*pqĕ?de?䨋stz fk|nC PtfZ%!m}+,r0sϱ_Ɗ}yU6<[\磓g=׃>s'#ɽ XfY9fW1֎ZƇvuCc|qFl!6ib qp D5ӱQ݆A:"I9iն8\ӏ3wc9Sg+|򤯰K9pq's]:¾}7`~MvM-_'AЩ(Z|㔓|=|sfQ0[t`}‘dώ)q}v10~fkvnWiU^i#(u١Bl* ~(`eQ(@- HP.ktDPL syWq󅇢]-N3>AAEAc>\ob퟿DR_giv {uY.?)>~⮬vERq"  zKYG/gqq8w,>kVt-d(2!6}h(uEb@$Yv-i B1qa֎pE3wAn*6],^;Mlo'Ąz?f=qTYy˕7y6<’?\Oa?~v0x^k+pӉ 22:Vx/6,mH>OZx^R4~B$JBOO1,bA1A8Wďp3V<}~f~D~Wy+':9جaQj7=g}˖Y*>ìG0|h4;@<}[ _csf|~Hᬧ273n'q_ aU^l]_y9'|oUe*:4Nx !6r` D2MdcqP#?|ql FP8LVacwa…oi$-B!u|?hzb;o_u4$BlL2[zw}ZI 9?e5z(J*D(CÌST!LbB޳};'7{??%bRPD"nr,lZeYL6}ph]װ*p uB!F_/.]֘YĩلbTZrUj5/}}i>>KIgYM ibx< M322at]X*v5dxS!bR*A2 <(RyomO643l=:/p .F#dY ϏcC{{c躆ɤhF\vSװ*vB18d2JA:26׹\ySB?,JzͶ.4 \,³³ϮO i)`!222B"щ0&B0MXe*X47d"n7<\I$`ʔVbvҺ{FJe}MHK: ccz5k'n7̘ 3g.Zg&jP,1|sY`V 1qí lz=6V^J_Ķ[׬oo] l{x ,"SVQ4ۋ>44NX$16>ʴi=d2ib$B!ki4`ɒVjk8+J-m}ٍqb#H@" Ax֯ /Vn%7s FLx 7. H(@]9tx(̚51Jj]6B! <(5ċs7}kc`Yç\!ȣĖ<;ܪ~LLh͋4 %T  S(hkgoP(DP!Q*#&/|cNA8Eiu >*Vދ|S hT*tuuШ74O6$z(eP&]SXj%=ݤ)DP!Ħ4FWd7}z+yzki*>V/Kּfu|PD$gl,M  `IwW7VaѳZ:LJBA!%KP7H^>}n((C=w^5 ϢE0:ښW#8tM|^o2-#tuuh4 ocY >i:`3BMO.r|˧wu?l&{DQ`[qGkKd-9Pk>l1>6T,Hϛ7A:m T.IBlZzZR__k yW2B 03Ϡ}ve9&?F.bH,X*aYE(P\”GE_z1ܲÄBlByo~y"яАLo%}.Unq>{Wb6\P0@Zd2BX<+fT*- ۅBl",Aڗ>y$tuI|9@ЇPgjM_eժֱ:}D(k\.Ca<uZo;ȪU0MzL#.Bqs+X@*|c8'( x[G?G:K>Ky0.h4JQi۔JE fӢX(DP!˶Q C1G"8'кyPL IDATۍs8'[7ڶ=h'Np\|~3f300D*&r2e b4%B!6NrQ_0{6'̙q5C\yt:IiFH&(bt0@]\.D10֭Z)B1V7aK8/C,O(;d pQU }~tӦ[K 9tamG"(bbۭ%?; ^{vT룳D{Np<%;hM[ qV;wtoR}Q0wz/z/gf{v@B HSQ (*``EU*] "f}g~l w};93{=\͆JAUU2zBT*jVCaaM5f3J@WB(P@ 'o" WfUR+4j3\nYGd_(FaѢ*"ȣGW E_n9 @ vt:=2C(**l6cPӸq# ոYQal7I턇!bwرz1 QQQWjIfs^$Y:GeUh (_AI ~Уǿ^h{ Յc{Bb)c <=zJeeOټi#;wnqӥK:vH9VǮݻ@ChH(=[k̷![F:rk9"T*H 6 @l\4 +eeeȲ |Vj<^*Dxkz_2}Ʒlw# F%nʢ@!SysW# G7g.݃,I'7UjY袅4m|u h%*:͒Jj5Z^O^m.v% Hlڄmoh^DUXV "+7ۃAoGpZLKe@§4.nĚiAeP#D<`"뛀.P: zo7,ޜBKGRyѾΕӖovckEm5Kx=0eu1k_齃8/r:^~{ O}bиٻ-py(\E H|3yy g|N_6a 2\w9=:=1D?Q@7#50YZtaɎt*$.ބ{7Lt/Ql#ge3wl0)X̹xf4$.݄^Q*.o=R4n2cZ+IXK/"KY)r傺jl2uHlJJml9.?cUzGF6֞. DNNj+{(4k҂{vӼy3Z= zqK2l# tv;[nSN(hHDl޲?Au$%JJ-x^BBBPv"g[gd"#+Hj͒eVU4 p# " UXJݔ͆=I ;3vg~^W\+#$|[#iXkm`&?ցX HŬ.w9HDpdSrǠ]8m?OWq it+/A:q|Ǭ[) V@AMdeUi8TI YVn'ކ$xsЇ7p9= .#W4Y>m%;̅P癩݆-_Up|BP`|WlDU=~{!HEdl_j_z#p,aSM*YJrw֦p]|s gpq||6mAVVf#h~'?;kCKt`PǎӺUjn6Y%:}V""hn$ _e:? (nHfV&K9`pdd@:P9.z WU/Dl4 Yw"}W1E )@i۶ #MВw?#Z`IxEl̰bjϐ{b@?`P gunҎc*ϯIP:Db0/B7#H k5M+wI}l ^\9=67B1uGV[am5|ƴ9+Hu$Fgy*5=y73xv$ |#&D?ks|Rk 0t6)1mb:O~4N;`|,,vs`\ÄQ F{}C@+5ǎe]Ѩ:@?X,UCCL+Ub7e{ jJb3Y>kvlSYrSv~\|HbmmeٙɪycanmȎAP|JDHiTx4vnl'm).8\5|I |$e00,Z؆ݝFI1qMcKgKپ|,^ ؂k-?9Bin$5-GvQч$q}G*+anՕ[8jC4kM3"~\qdž%b%$$I[ iwCSS=|5GB‰US$t"ٔѧՃz"! !$$4CMYY)&c`~wm\=D{..'~^Dž0=n^$!2j K:_h3ZQ)**d2p:)HW$8$B`M0i,HrrD#xn;Z҃dM'AL'9b#{qx zFHffT' BFͻWh9Tyx.p>>=k̙@֏yu#1_Ќ(x?1Of "BgƜ3П!'WMOMе?,`-xbX ,tMcƻkZhu͹D)>4c0{׻ c&ח4uz; i7sf3bz~7?~ǭy̹XIO'_ad<=;d|͸{~`nR-%rǓH<2S:|k,x>܉FkZ\#۹睔 &DqVo/Cm>n Mvjȱ}6\1# wfkKd&xKi_7ѣE"O 9l滇1(N BԢn=]K Q<:y A9s{c}In&n7*.7FCAA怺#s4pKi@Aq!a!h4Jhܨ1QQD 2Zkb_G]7qhn}]6b9\JqW#m7@o_3.\/ s%Q#"=.%b?WJxQlߝQ#\&" &6>@'F"q'#"z-,~ c]m`#ة+COݕK hͰGfyzW JQ#ZbcqkRXv.eo͌jcBDr6>Ʋm%xV3zC 0?8܆la'߲/xvDE)۷wb;"4^ݲ]c8%Lau:M AmRW$ms#(d4a*04Sp6[3ۓ_DLHS܋*<.7Bd7F5Ŋ$2r .$jM"Mh,.rRw_h-Cb6rHNVmBAvGsP3{vb9lC0ݞU_/vI[QqrZӂzi~m ~l|-K6RCݼ%!*}\{ٚGbҸ(wx|yfJ3)+-ftpxdwOxݟj-rp:p8vN. Ǎc)$lPO]Xhq{h\.eeeĒDzFJ 6Khj>XmZFd<.7ݘA&5&3c{i|_<"U/٧q=TUϪJ[0ζ|7,'L{'g,IS[q- #( pN-gL})3pֱ3Ƨagǖ|"ٛl-$O#J߭@O &l60%. *M\ٹDdd4iof=Uz׹JHesa>UI}TA:+hA mNFBfOʪ{AQ3$ u5`gً#@FN-3jpW8ʲrA~@K ȞҜt$`tlzRYwɖ#NV_ZKlog:{j:Z##u{nF,>qTYwƾ682g4!NOM9|0m۷CeTa<. ^ϱc P2l6cp`$3+˭f3ZR^^ZahIBVEAWqot U?I[ƒwNYִm ~^4VFږ-58{:IW7K{l#}]Stb8Zğq峊ȫ&K)Ha#xM]95Rv~5vXK^hȲTBuW{fS=I-σ,d~Z3uBܔW a?>U^Eܸkqh PÒ廹ED/rZ].; =ѕQ;W+m&ڷ0+ ( D6}'PGЩQ(#ơhHfm1:` ֨3qwz+pemς(Nh?}5,#KVv/^LV[+5<>2,dQF~˨_:0ķ#l np hdF*]++x(9',+͘[׫z,@ًM.c7d8fHfK#*7$J6ϴ;Ⱦ6FZP n}>=̞A8vN^~'v௿(%.ӐЄ|RSRCT0ZT" GVpg0`4(((@բO ++ ˊHiXvGDTDlxvY-׏mBXe B0a$S"T/GΝ"%1x?nô> EudO1y7g:Tȝ7DZ nN˓ͭΣ|Sؽ ̙N@x zRL4 !6/Ngx0y>xaW՞/]ghl˘nnY;WMt }#"Dp3.m%ь"[VY%vXbQU-bb"tdeAb? B=6ݱ2몦ig^_ ;0vL1.^&7B*VZq2|r8ei+iWb؏{>}pcTk~Rf'ǹh531~t)-,YR \,ZPF(Zm>kC>3J@{ENSDpzkă^-q몥lZ{]v5魵ӧ9Ocf {]N' iSYmRkX/$swY/olL ?~ԓV+>\8n+n?=zfۻ(z\Z}LõDDvI.D犊 <jbC?ǍZB"#dA`DaṯT\^/IGb9Z& ą6u*>Dd6LE("i*6_4;]\5K$OJG@-h~={Pܦ F2GM%4\{_.(]=r aݺ*}| 03}=bc㨰U *6;!!)# "yyyl6$Wr+OKݻ+BP@Khq1jii_2k'Réc}"%!ŠUQjBB}C}Xtp\ FdgeNBTt j 1+;Z,{Eh4bMx (PCB? E. uȗlYU! ߋ/}*:^d6AA7p܈f4v;ѤSXXHjj*F?(P@N r0a|p+;d 1˅Wpjߤ tpNY&nwMk%J0uzT JTQa 8(r݆pOƍ!,<? C@ .U9>\(Ppxڬ]$ !?wl4"z+kDlL,. }bOz.bcb=,--ŠpF~~>yy&z^G~~>vDž C$(P@@wom.zBêE%K`߾AxEGE# y1FX=t]MTj.m#+y 4!< (PA>܊U^nPdscNG.WB43(f3]$ˀ[a<Iſq*m]/?|+kϫqݝL[SkuH''Ww̙p(VOx*R LYk~!80WYUTzW1剏L$|d<<^ŻKԗf´>^ϳr}bV$w2]_)<1=ZߕW*v|Hq:T*$IB $##???D@f;ݦh5}t M^~[,[;kW-79.d<*mˎ|cm-eRv7`X4wgO1⺗vR_YWx|:!5Ե%gؑɱkrf׿r~/nYĻiq0"`|Yc=zz\8&|T[`S1ska3p,Ϥ;ufk.-ER>X̚59o<+ ;H[%e6BC,1z,{g6O fo*B U'[ P66ѽW~Iu*%w{.Ew("_wQ缡xs{Y,QTTZBߟDS@4x=. -FÇ^HP A>ywӃ/nmD}^?k0EkM_y) ϏC+w4K籩Yѭ|y:^>2~Ժ̅t~Vw\ <>w}aI\)[6M&kBGJtH <KR-rlE+3!0p rYeO: $uEkccG[-'}?7AGxa?̖VY&?WJ_ϙ-cSGѫ?MFDjjUɄ|}pՊ|.:!Zq:2 z= jӉE椦Qv{RpsUuauGz9tMTw#M_s}ɘxd(<^X8l?ȧOk:L^ř Zjb\Gý5sד"%K~"ߦ+wO0J|vQ g3yO?9SfƾRώd1}$wϘ6g.(,BHw2Tp,̀Gik&/.;sGY)w51)?]{ϬyU-~Wխ;O4;ʻΚ, Vs7r-["?$?=NEᆡ`pa Mx zN)޵Eoōwkg#\ȱl7-IZvҽłz@BxXnrҳ)Svp+QW`8c*B;^AĢ ^K_#Cɽm]ly3=BR֑WpRjMS\pTT ,[շ o yh0]B l @d2"Hnn.:mC֔)**uHPe OIe(5c=/J$/6[~!wF3 }*ϳ\6<Lָ@G-9/TgYy󽹌".c_"?i6773a{ }u=:=Ar^Wεq_qe/Y,6GxK&5-`mw8$?$(]71=&wBa\︕?9W+i5+\r}a5f}vF?cCCӏUGȎ%4@¯iAŌeu_XE33~Susl\-X `>(\{:9z*MwqjK̿Nh/MtzrEWuxFCw[pϬyLԊJwھ|м  ulقG9@7n};5Wt;>Ézi;G pQtSB!g߼8k#ܸ՝0 `V]<oE;uraPHdT4)!.\.Oի^ͿI8ONV"prc3R1A IjVdbJ` l"E%T0t2u`y+G?=2#b.^ Kds$Vcgzyx>J{9\! !(ذt<4-⽏W \@9X{H#˾ !bNS\E[*(r>U {qz=,?ēڣ,&x]z% |2^hqp;;[S9G> C1 ;gl|j8s[բ’PXF"psmdu&=ѽ,'oũ5L'݇dΤ[DtG)ݷܠ64 FFǡ9G6&`PvQH,j;"î]˗CIۛ!_=;Moj4jX% ?B1ᲽMJE@PEQZZV#66ۍj%/?@JJKQ+3:S}ya^q._Pg-[Tw(tA>x+;e烗~++g[ i|^NJv'& ޯ1:إ*ܩZO}3?@5 QS&ō7اd{T+-<}zϛ7?ke1۸! yvˀX8 ܊㥴^OXX-z?addъ8lJA3DM|w:u$`k2luS?at{΂WaD\u ߙɲ#:ͤKxh"iZ]L&/LI?LDt$^pz/SBy|| cX3>6#αgc}v$3w<6l׽!/v88vx\t6^{/% IAhum"4v9sS6޴=_pŠJ/.F\SZOc:?LV;/eWzLz=Q}&po+">ޡS]-cZ,g[ ÙSgdY@D;bH; 0,c9jDLH>SSR|M&]~+CdÑiV~q/$|gFStc,/6P dӛht2 ?#ɴ7 HExC C%}(Hi=j̜HE6zq2t`Ƈ#R9ss7p_= GDFIPPCPP0^0dd2B׍ IbgUhppExfkS|ȍᕣa)Cw%m } Xx$ɶm*ܻ.N֨o%lܸݻ+d}۷2y S@ła8OrsP}>иE YL y{5 Jo>+x$EHJJCwv:n6U+Vc&N qqqX-Vt:= Vk=4JTX+ mFAQX ^7?r:*A7Ѧm`&?ցs$aBHjA*cO(luM\(:}A`6#x# ;v&T:q{<-7l}{HL}]Ƀr_DԿObR~7%Su14d֧h6ż(Yuy?Tj5Pm@&>>/Sy .`aIZ@Q?&j&D՞:uNtۼ3O%Ľ7o[58 O?Ȏ05B)%'\rtZ21xQx<@d\n%DDSP@s\ZEMH%%nh^v#^yo33ާ48OeoOJɹ<2i31͵dΦ\M[{:(8I?Lyg!UD/N<\3ѻ@gNgUcv̕7;[0iGiJ~DɬBֿ,^z^r ኛE/,4_]}/UFY/ŸBdLw!!יڌ ³R/Z'H%(ǩc.~Aؽrs9e.YYA7> ܪV"0[Vxb8f ("3E=OTu뜇$Tk2AJ|΅zƦ&&xMwobL@%,G%]QM(on*.?\z*O슣83W1y8{gw`$Y2z7qxWDtXAWͬs=[α4ri9nQ\=HЉ7cRwh4JIi)r|.GII)h`oFNuD" Pw -t;xv{^^3~ɋ #2,yuك4a*F|:xu)? FuQVL|a a'9bgcIk =ʊ)Wb1Y8jvt@fقD"ah.H$HYIw/f`6Y۩qg,ԪGrʀns]7Lߏ'GP66Q.t:y+Q,\ԥ*D |s>+ucwX{ôCxx9_Osc/!ۅ|_>7P/_ákQP?p8`1unݠBfۄ{_fcSRm ~Re`"H$6,7v;..*E=^£537A3u>MV9M3Ηs$o{Yj8G8ϻN٧lzCmyvށ&Ŋv`(bl&[)L&t RS] ٗwOqC]6S /U ]IOp]?3 /LbgܘXmو#jڿxrGtGt +S]8ho.~^X]|[;췍}spNd ՋW "?t/f~Kezu)郱n}+j0~x~,ojN]wU_Os%Ǡa&X-x<^0\BŊf˗$KSQQU+ܥ3h;) :q2A o;&ߔc)8 .41ύ̹D1w ޙ][Ma^=e $)}{:={} /Z!v4GaĎc  `|~l&SHؖ-CyB"ֹ3`TWCm-ujuMM_|XBvW^]H74^{w0m~}}vȽg򈛸u'?- & UY3)--E9D"A$l1S]SESs#`'ZRdh!DK|mat-ZamBmTW*~M'Yj8Q!|8e4ZsoLQt^ӽOITlw?e_WIӈ':hJ(ڪ5-tTK6oP/% TÚUx^BK)/r}-˗߅SW( WQkA[p6 MMJs?'[E)\;FPWW)TtXX,xPUC,#ɠY-V,j?_}eE 4eeA!}P ] V*<7(t9PM Pd*!B!b(.ab*@a2 n94BWGY?ގ'uPBvi1B….\r8EA{{;aXV7UAUA6gN$I%䍜DP!јL%%>7>Å7W݅Ǻ.)wQx W\.9./bbۉc6Nr4FIq1@+UUԯ\ŪaX2VBSq~Pn&L׭bCIykP.nlۇ}?n"wYիi,|[*b05"D<؈bAx܄BAjkXÁf&PR*B]]1cӾFخvJJJb***F(B&A )+-czb8t2ڃA!Bx^24fOCCT|>(5ZQɄlN'%bo9; zM3~J\ڲ>w*w! ![-`(.1 ۅbuɒ%躁'P]UMsK3N0,x fIaWQAWY Yqx'?z>Kn GW_pW>+z?PnUv,bG( 6X,N,X,FII paS]QECCE^/mVJJ00p8Aqma澼3ʺ4q ۿ-\3jp5VTk9}GiB!v(v>H8BE -hmK.44I`h&lTNsԴ6BrF_}W_;n_"u p20၏h}C?5S.CyZfssų_ent=#|M̼FisSé~~΋!}upL'W]^v.pݳWߖ퓩LE$,8,IꓝޗGx,Vd@W+9WsXضl2[xgMX֌/ix3m ͛MޡdCEYY___j8T<_t':FcUW~`Bn訪D"lp81J+W&di44]+ N8v˜ٌ!g{>}87̄>yCr,ι:rIMx{47̍߸7ݹsg7d|_ª"ٝ01[}ny;x58n^꿾͞w>{:I~(g_ jԖMŠuw9,*dVdn[ߖos͵1ldAc_[w)P#KS"FCCS0 m}/Gs}fTS9;8 y~vY|}tsAF м1BpB! O$X6&24%%Q^Vɚ5bl>s9̻äFSޢ۴M'^O^ccWrQyC}f]'3YgbYV7m`z:AFHM5O3:/d}[lN6+|*( 0nl-xA'yb^0)ridzO`"?z٪ !ؙJ<j*nH$B(B3Y4v*++XӸŤ 0[LtD{xxFN?<ۮmowțxp7޶Uk6 =_7DLn'^5b|9 |GNb??ߦ{?.7ONkX+n8nj5=*7k:&42S9/f@+nѧ2pM1qc3rQN40aۏs}7)ޏC' +BU7/o1BSeX6f VB&"`P Q/B6!JUu%VsߎbԨQ۴b*s` Mkfܙsx!ش`0MϚ5Ol6fÀ|fL&Q BMm ˗/GץObǡB7 =̼D`n!e9 EE>2 Pd*~VZ$B$n磻y$Y=\: !ɤzhmmrQTTD<`-͒d鲳jJҙ z^ !v aMaDB!įO ІX62 %%DQEAra P]4ʪJB!pZ[ZlhD""HX,hVM=H,EQ\.vB!p 24e\n24fsa/Ղziim!:t:illtJB!DrXf ( aj6tSQǤ܂IQɤA!B 8].8\P(D ІᠹMQrkVӥKgDa\.D !BG4(/+G cD' , l&ѭ[7۱X,f\nn["(B!:t:M!OΓJld9L& =Ԏ"JHҤRI+MA!Bᨊ rV ͠+A6<^++EB̚DP!Bt8HX,ᢩ F:!P0m xl6n'vIRA!BhFii) .D b_=[έO'f@y&ƌ Oա5i[m׏/VMpisQstLvSx"@AlpX-TנR)B!V+%%%$ f3%Zf}1ud07F"޵8ț<)aH|=3}o3@\`?2⴫xf}$eδ)׾7B/%>N'zM3~J_ޟi͠Qh!DQ\yQWPŌ}/L˵Lk9Z|)nZL0n{dzK n~w:+O?vm> Ʈo9a pٝvN.' dYE^4MrORXm6r,d22L!\ooxe+$gL4كP?L|"?l6QL|62xT5f}Ix w6;>MtokySF\_y?z>w7MY'ݲ9\6->z]k4-⹛.!ftUCSo {9445lBP/kg>b$.;Mל\w̸+ƹo0my_NZNϳbKٖz>|}}|DmBj<//>;O}NBN*]]wm"Ds{ ,5ϗpF\un6Z (We %e.i<o֚^WwcFu2Lp(QJhAtOC1P|XA8fjf3vd2bL&5C^ybXX̧#H?q#0hC=7Q7x = fqṓN;aCoc\B:`Y5f8snՕ>a SHgͯȰ8 4G#bD9`8{ &>(n^ ,C  l;~9QgOf.2wCS?M=H.#d7Hk8;Y@O>$9kf}NFl>8 ?E'ϑc/wɬxs;d}\ʕRxӹV@;9cI{Is4?#n`^G8CGʨoᑇ&pƉ3rω[di}|ǎѿsqq;?IlpM]ks}4C]l>kR-Z;?D΋1|HCq~Kez!N!V}t z>^[sr!Kx(˦)βԏNFTNŢ*[/Ѯm WٻTWNɏ2`b |6Q43s쬭iJ IDAT5Ӊin7pՊn'L},1N0jֶ~*G/Y^ [Br4Gt^_${}50}E0}/'#@׃'#`÷%q_g{Omim/]̡'eO~ysI/zxo:':Ź6y,Ww7 M>} 뀺32y7 n/笑=)_o*f-k㳷ۙn:QW˘AbXT1|*׸oxg|r_xvڝmK&ҌxuB7>/=QdcC //_^_> ~dOSr/c^ +Icn~<yr~|tOO⽠g2O<]>;>!j@.i OܸijO-arWScDs[.õ_'t25>/\2އNWY~1E,K%!}83WgHũ,s46F/%t'vYU?+C-nK{9m'oh(.1 xS;.wfZX,fB0xL6F[d ɦ#nrhkA?nGn3q/yq!|$Z%oN={pݟ2\Ũ=_.ب?= ID3']c@;>KZc5VQVL|.|)y6v)7kggoʰh֦sut.Wн$C,lq PLMNT PvW'űcƲk"6=8ro0ܓSPm]5Qx-eAW_u"7qΞGsZǙY;'楮ڃKoJY۟Hө‰ tIqp" ͹>Ą'{A> [J5V/or9"^c}c7@ѹކ|XknL[z2HpQҶj#ܗ8'8|r(^oΠ΢`s$#0cqMMNmIZ9B6޾*eKB|}[O7Xu,ػīRMf w7mU Ox~OsZ̀(e]^zɃ[:ʟ._|j/$ ZW~Y(9!r蟮 0b|q=Ugh$b2a1[ESSդJ&H$4ǻ~.z$ f>;E|caO)UASP '?v+֮Dsm=z@~:L̔Oo]rP;<9u8i3s/ڛsǧ^\q{7غd`xw+:PJ<_}7 40 @Yp,(b)O?8??cViS69JΦs9̻äFSST2Qd#N$:E}۪o,G\93oӋ.>Ӗg*`Is4da7!8~_|ڛ2y6Q-[o5Kxt]֙g]CC&+ǜJg9|~Ooܠrx >e)l`eiANk[M3c2[q؝ˉ(:!lvp`!Qh~2N-8Sc lnVՊשټ/X4{ < NC3f|40u]~>lߟ1wcL<qnƿρߟ^_[9^kf1t _9OpQ#}xB~gų[qX޳ТMbi\~W=_t]`c_Ƒv/D~ s]0|̺fO⳿<׶sgI-DG[nSu s>KG>fr2蚧b~ޝy>q'ӺƸUUy1& Z ՊL&46)T.‘;G!Y(t#=0#d䉃(Z{z78i 8;r㮿K90mV3ib1s_b.>3R9{2֩Lxzäs0i*vU3x6eg1'tlE5I;hrpL{0MUsӄF&p*LN'~_Na[-o9 `ٷrMkviى 7p-D{=$׺QnLR tCYwˬ=<இ7X̄_:q#'J|m}-Ob7;BD/PVRsŝC6D7qNbB.oCX/n׽/rz,`7v!? q~/,__`ރ%L0ܦg͚/#+H&Sd2Y E5-*=؎b󡩪ǚ5mB^thni !BǤD1Uu<^DŌZZZի0T:IUq\vx!B9t=O J>#ͦQͤx0-jGQ KB!DcimmE3 UU(..&A*!hƦFr,ntDB! jǤ$+:&MCl4VT'QUY)1, !!cFxL&l6WL*"Fx<(B>tаF"(B!:x,N&XL&4n\.8NXepV!Bsy2,NP(((jȇFd2TUVH$)-+cMCifB!pl;eeäit]=j%lZ¡aXFA!Bf ǃT+HR:af5"aL&ښB!p<u|ajD<Ŋf6t8(**"&B!jP^^n4իנ|>V\i$qH&*B!N[]_i HX4FC.Q[ZZp8N(S'a`$Blw #4EUY%B!vyn UUiin!lgvZK,qr; FF2U&ƿU_egSWRIPnn8WsɵzTpG8WECL!Įaq=hD`[0TU%JQTRDKK nfLjn)c|7/ɱ}m0~fԖT4+YL@.CdEk W։ʙܹ< [{&CK/ Y{Vq?FɢB캚dY6v5kVc;p:] MuLÚfJhlnQT#H-qv߿QI^^xrl4, py /KD6> 0'#ͯu`)GB]f6+.0 U%aZq8D"NcXh Z-ftZ"(vzѕ489oF?epJg&֙_ξnw'?S>=c\3 zH+.aoauL:3{sKvf=x.1GM$7e7U4h^fPvF|,{ {Y֕q.d.&!0Cf2x= 6h4JYY)j{0L,P,vE>M3YT$;?=Ϝ٭w/g\Ln+8z&.+[c FxѽKnfJ*J] N'Wc758[oMuy) V)Ԉ4e듼)taԪx,g0g#I-V:}B!vaI F2H%W PEQZߵ!F2#F}6*=-rrѝZ.륒5p(: >nuf(\}Ld7֖\X2FRS%зؗ[5p)y2ϊ轇8ki3|C'S)Q:B̚F<jR\\B:k;*kiiiW\U+Ҙ4SapwDP" \?Ogm\lO]'~YY` 8xj93j=9,liuF2dRlSPfVQ\:gA͇?56#kUYU[Cp#2|J!Į+Hc;0kTWאJmL+V`2hjjcX\.BDP:^Om2 ip)ƚuEAA۠J6s;9;gr$-:P|^ ڌZhY WV:v8V(;YdfRpz}p)e7R6hn.3cRוpp3s>*')K8f gCuV )KB]8N&I&TVVL2b6|%ڱXd2)L GQ`"L{?pޅsq~uL=H'YE#xs4L :_ ߎ^<+'dt,\$k).* fβ/ZxsN 5}|*7\Aa\'kbsf=Ϟ9̾{8eJb w=TU}:::&L(DP!B8n;.:X8뢺kuZ("b$ ly8+3POBSdimSV4 zituv7\t]aN,0Ss8a9B5|r 5>LeN.N1sc4~׸=ns=|:s\xrvSiT|^Ъ3~ߟ9G9W{~C6c`3ku5;,"{ݿG38qCOZČ;fex8똛`2"!g+7pƌpNiV;K_ŏَ˧5\{x)Ҏ{y(?}9_6ˇF |cqƍǘQcP7N, YyE4Ccƌanʕ2]:)KNOOˠ_ym7Z+e+Zmc"j>[Oܫ,7;S)@Ͷk]n1㝓*e7mwp[z.}[nƩg>8UÌ[\zSt9vcqy 39eFmrwTe"o?|wOsfBVȒ |5 ^}Uƍ7.XP(DX 0CFDr9$088Hss}A\AxA4xsflb!ks|rs'jcN;p;>Dž+ \r_!-38rf{_/eE )q9KbAm߭OS xcWo#~Hf}s8ќ AǮmy><~2%kv^Bm9ryK?ye<֗˦ SѳL.[u?^{s0>PIN=?5.?L9k+ˡ2Ll̼wխYQW׷c F 1"D2}aE2VUSfPhnjJ:8NKk3r QTmsuM3Xoo~'1k7͑־*4ΛnƓ%qIscs09hr5 0Wr1ʦ<B IDATٿen1}o?é Λt׹3/Nepފ_fiq8fc|0W,ͿE[y5w?\_k)7o\o< ܚ4ɦ>R_-AZ-Wr!!}e;wLlNLѺ׆T.SItU~+a5H0 \J.'b^k`aTU:ry: Jf0D,tCw\~2Jյ#k7Ai$M9/gov3ry2K#O}H MYPT =خ66M (={)zekh*v 5ܼiwV3MGDQA搂޴Gy.>@`0ZFU<5B#|ا߯e)1Zck6KmO3]x3sV+]EK箧19Bh=P_[ZU&BϿ8GV$Z4*"<'de#QLDϕ ttS(u(E\oJ+ZmO4luXl{9/s֓ԧ8+7p%w1rU g1f_4w&>ݻK*jU.ƆKJ55")FI}5E;09->P:]_߸{ksg~sY~+9y ~bbdmvGN?+4VTZF*!R*P5 ]רm'pPRa-,YxbGr\N ks!}&#}?𗥴mǙ<39-8{0~yb=5Fty4oƦ! <5d/LKBr֜S P)k0kҵy3.f>?"v9"ƿǚ0~U:*dJS/3/crd(A7u;[09 t㘐$Z\!IHRA>#bj&W^^kDQJ"m%$A0|7fm>֝,S!["{\}aZ'lgwLa*{uWٹ)G7W3Hn˾i~N&Ǐ>xX6N'|g)@gO^> f+dϋ$;.ahΤ7^dj i\Jt6xލ|c>gŅ1+Wrpr!ϐ5lF]cO.Ld?P&ʢB!FRv%FH?$:O$}H$ L755|iUU0L+"R*QUʜ/2}Ti&D_Ԧ3dMx6Is-pǡ`N*[-Ǟ;?1w s9'…Sc*ܼy~pGBϹޟh]Cܹs8.(tttPUh4FbpUUY!\!4lQsڽkuO Z; 9¼;µS$b$ϟl6GKK+RUJ&m˲t0 p:V8L6;{00;5_X{k6ϪP'&O~K ͎f%'r1+YX8byu*RJDNFA (F_pţN$ԇmk8k8(+ÒfMIJ !F^v%!~te\ϧbDbq`f@zww7a K*\.Pz°& v}3's[Hז"Kb6rv|Gomnaٲ44taΡ򐬴OB&6Yiadj,ZT*Ioo('UBN>xZ,SB!| OU6#La‘-M͔el@Y ybZ L%X=l_X$_ˋԵۻҾr"L1<\0 2hT*CoObYaPv^u$eed0;0l_T܄hH%f;nm ]]I$Bɕ EƆet]' G<3"PhhaauU5ƎҥK'=,_ޛFyfi"Z^\7*JE B!yнJF iJ4$O RD"IRAOxj*Zm'2ϯחdk&L$!B~===Ϲh4ʨQttt A. Gp0IRT5|tq(Kz,Z&ibY&(sw1%ϼn*Bl]w OhF K YdYAGG'=== J"Rp8B6 !BGt(Dm]Fph4NR%jMMMcq,]iA!B1⨪FPFAT4 :-ͨT*MqPTJB"ıe!B1T*8b SVB!j*Th4LZ& j +B!#N$V^tvvQ(1L4M(b\>Gx^6#M`/@"Bh 44BN7QTimmP(`먅BΎ6,^8 MM#{1{ps :\[<7W#K͛8Kl@k9_۳[O<(<çdPr\G\;o%oPHm, 䗖BcuZZZ(;E<4 Tu EtUu4 ]?]*<_^vbr?^]3mw͔nYmA|E5Sw_>T2c^ /7ܷο9`8iJ:!1ItsK b˲YX!btSWesTe<#|;vdm}N[q`0O^d/0wXC':Ё8g˙w9YoT-$ﴲޮ-8.~R,z9={=i3f}s8ќ AǮmy><~2%kv^Bm9ryK?ye<֗˦FVSѳL) IoKBzWg2ʢ^v&s9YU_#ˊmS|e œ| ಙSyzq\bmf7pfS'z#^ߎa| !](ZR(>)rNBjEQH&SAef,N{ )mU{Ç7qQj/cOOCC߾C6Q~|- v;@woz_.Xo]K̛}3q!X5䃹bLn,o#o}9*_O,|I4v?\5V Ms}&%hWj8&.{1}*.Ï&'P@Q!ؐZGcF P è*/roM0mDJ6 )]V&3~l*go5WT%m\詖e_^~r|@s3Z`U5(FLx !\^8LZA$AD"dYTU%E^4MF[glU}7Fa@ib-p<M@TQEAQYݟ?/|]ϻmzw}57ҲRuEs>fHʢQZow3˯f?GG{}sw~㮿c Pضnį[CeKϭ6R{N~h*Ҟ)kBr֜S P)k0vjҵy3.f9̝9|ַ3Sߊa7^a>#m^_b}3s؁oOͼ7oƦ! <%x =&&y?pǘC r}|o;*8&$Bri0~'^aXEOOtBvTZF8l? IXB O6Q6g3Կ ޱ{4[6ۃ:Y993/.k^Cw GS#=s8=/FX p=S ;x#[FuڹN~rX^ՈlGb?ZV`{}߁f]U2tvnʑ`)!$`~;?aV8 uPlu4Ayte#LEBHn Fh,Jvt EQh4ttvQ/hkR)baܜӧJ3D^y0uT p-cO~`3]œcNfɷpԘE^1l#?w\ 8 I$3IdZZ,iim\.c6B0H>bB8s ǐ¼;7H-#E^^1MJL*BUUAd2,VH%aZDPE%wɡKh Qov4/`=ٔC!F Mӈ&uMDr(nXev0 IyW4o"axOx6OHb$+4{>eDdeݨHEUinn&8AcYjU"(B!Fx"A.T,R) d21 }``]׈'WrB!B1"9inD1,a`!tUUio`…B&Rh,B8lQ(%bxk5B!pI-+L2A84.F}̘1 xH$9|ߓ !BG54]}jQMgtk+A B!#D @4* mmmry:G'ѻ4 $M,[UQ4UzL!Vxb"n-!3$BunyWXt1ƌ3M]שbq Y--yzN:3tKQFH D"cBEV۠**mK@bTOO{$ёHJ@>'H$󨚆с빌3e*(B:% ber4qG!yI\^'U+bJX,F8<4h4|QG?FXeˀIkBr3*!S2abYa H&7`/^LR"L`5:;rdҶB|hIӹ+(*T|>G$Ŷ Ka,ˢR.Eo( LFzK!V(: !xufDu#hNDIR`bt:E67H,öx^CzJ!#|ߗ@!ħp]W0|x$z]DWU`s@Kkۊ:8B,X@)C<GU4]aYr-!$B\%RSe9nD?zQ&9IO(WDo%8 2)R?O|&K^dv2}VO^߼J%5t#9ʬ>-ԙߘytkqy+8{xfФco,ah,^En˴_dsgξV5)v#vlݵRy.Oƕ7>ʒj+;y _ߡ e<t ҒEOeМ<˫%|q_aihS"+,g߾ }M6%0ax];#ePT $_,5jLMCvx^\A{[JIz[ZAIn~ Gfr~In}.<Ǯe 3ǒ,q[8z}L~hkN>rCx}r*^y>gdMӲr5tޝ_mGNWp9_ie|pn+M!&dtV7n#Ӛy!^nfz^/s \z1\| n>|1Jј s쿱W0c8j( Aϼ*4p۷Ooǥ?q=o54[]NynC\<16c8B8=-gS||/Ci[;7Y}MZ%m!JTUeq{@`^gHeY4( -͔J%DEǎKX4岤RibGs!V˧jwnGmݑwro¢b(2~Aڴ)$o7Pnjccx ʴgϫWǛ~7?yrmg9rwgf{\ 2>MǯvX+ЅA|ct`nJ7jLZXNv_a_l\wH_gĩP`DJ/{Zޞ:Z+][^x_̉ <~/uz.~(f0jH(Gα9 $B?{}L:[eֵj]-,l4Dcq–E_?Db[qhjjBEj/QUEzZj2?xݟ{!|( >ފys+ͨ8^lę7=+AI:޻C~M6%AE>[%gQ} Nxc?hhJeǂ⿕@q g\͡η:=~WNW*"`{VYA@b '^s#y}.\tl!{刺ow~i:|xݳWwk,.O>=m:y,_"O\Wb \?_'^ظ5*}7/ᘯ*]9/?s\~umu|T=w h?we5V "EWDlYGn:Gc4)&AAPY uƧu"KaѮL轋TksqL_U-itLއ:繬+;C5moz[/sh˲T*R ˲T* z[[!+,ZaG*x³4\~-T fF_1%q>'PmYkN0/Ek|'ƇrhǥuC$R]>~aҗ7Wwn>ߞ!;)ͻqƩ}os|wު/{G_1 y/]O4٦-҅( 7:ΑftU1Mt&Cuqx>z 'JBHmHo !{|B!>wvݦhnnmZ[[)˨( vm(FuthB$QVT!ĺ( zX4NV!X,JXl!H`!\.G8C.JB&a`pPzL!V2kbq$B5 :]'a6LJzL!V*]]]tuuI0sP,JgjDt:iܜ!ˡi*Lr(B!#N STQ56ʊ;|UUr4TZA!B18 4F,^EQ(ĢQb8]Xha;qG”KeB!qLDU5* T ȤӨ6Ot&{rرcYx ԉA!B1VHq|T*M:؄L.[J2X*`.B!#NRAQ5UEQ$N2Bⴵ|yJZiD"zHB!Ĉyet qFDQ*C%dXX( B!baJmqp$5 =_(*L$!*2˗DP!B8FN&T>rLxC2}If+B!buRH@Ն} ^ifA4ZƸQV [4M%IB!ĈS@Q+eLӤ^A&u=F,AB!qlFQlFS5\4 lFUUL:IT$jDr_ωB!}R4 "dYǡV)a:iR.0͡B!B< TJ mۘ!VzzzQM2ijn"P0bQ'B!Ft&`uQ5d2jZJSS m*mE%B!bĩTʄa $}}}(B<C 6.\(( fLXJEيB!yN HO4EUUQ)KIdYƌC!B!FT:$I >Z\.K<Ƕ:::XlJp8,B!#NRFtEA7 ("ӃfijjFW,¶cq:IB!ĈTuBEشwP(=LQ(hom%ϡj*AL sJ$B!ĈQT"Q@0 ,ztvvR-q.jFbB&Deu 򴶶Q5x"N. q]USu\eY4 Q.V:cZL&O8 iZ *~  i롪*d4Mx,NEt X,e+x+Whjj^i:zH$n8MP$ CH&Ukש@(d>R0h8 RbHӔKC,X0Hl6e)F&ӄظM H$(+4`Xl6H$!C<= 0 j(hmDQl|4ME7 BBsEc yJEQ1 d9 @UUL$Qp]u<g ;i8C2<zHl.O$Vbx|>aODT}\'dYXEZETZx xTuNbT P0S S5%P%Áh6ξkaoRqRO8NU\Πy/m[i{HU͙&_e}߱ P75y3O :W<E80Lmi:㾞ahLLܮW DS4MmCQ4kZ׍8)VQ ضͲT t&~`Q0Nb6U~ Tu]1Mpe<0x<<؎C4MúmLӄmYQ]}=sð7;e, ~o˲PUa2r.uhr6eZ\e rƉ̲<q<ϧm;,:Lp8|>uÀqWk"m2kXYG]uzmoq_e^1 x<8eL0躁mD +AɶP ,i0"M3n;b6E1eY12EA83Yyr<@3ir~)UYaY6E^0N4Hi\n@44M`ZR8srNu#MSJ\ێ8R۾mqB)ENei/44mbY6bƉup=(x>8ü,lNTȲnAd[w f5|?`Fe<s1-ԋq@ЗCi:.y c֦1,u\v4IFxb>awZ60RӼЪf&q0 lˊF%G;88O'Ya1͋oף{NفXueY1\Ybk[&+8bZmcY6˼nAt0laaYW; pq]JжQ=C?$)q,K | x<aH`x˒$mHiYu?x%ik:۶e5٥ FIt]<A006H+a˺E10&)%Nu˶_%/"wmy;3#Ӿ;Mb<EV5^ ںG C:6o a3#8% ۍIJ.ض ;FVHm2-m߱L5C7HSy{fEq*u4?{g8˴ҜidZ7("Iʲ"?2om$Y؎<$I@w˼ض)iZym4M ?4Y5ĩ7Ǻn;\.o,/|ʲTRw`gy>KGY˒^VYY[,~kuS̺nA$I0 ay |ue1LYu( Y(0M i0 v}n6e!;m0 $ }' a0#i)G@p\C6 q 4neYNh Xȯ44Ԁ1z60$)4')alj<-} yƱvlוij`L I 8UU)eFǣ=˺>33A*@T} ((%eaG1M]?AHtmZZ069\gYJ-u\9023n`:A z@QnɾhN^L8lFxOYVG$4m' a9S ay$IJ6|`}GyZ,mLx>Kٶ ie-t:u=u=&zU6uҜ+a$of#]wa1 ۲cM]iPrׯO@cd͋"b'邪n'M8-+Eqv2-~M/vMò%M3nk;,±eIӜzq\(4L]A6(HI%Yו4eY&9 ˺PAiFUUi9}?0abY6Q(Ci aiӵ ɉSi&a"/daGaLA z\7ifoItM`b&vĩ a2M3iﰫ0L(t]t n4 4 ,˂Q<-퀦,y@e IDAT̽MH˜홷 0w$Imۙ֕uȢiuyZXճ|mcF0aag ²l)$_}|AIQJ+u0l_Rr]1 e߰mqYvRۺe]W6M0L8ter]|g_weŴlYRiYqˆ8IiՐ=Bq0xI8hAHvij@ tMaN7YM]#ss1}?0&YV_=/fMvv?d9L$NYlӜՋtaD~S˲a.@ Ұnc,M̦Ѳ,Nt,0dIdlm}i|$,Ӳlx6m<ö@r>,otO!ۺ)u]:.:mfuU3N2tT@òҖgx c8s>'a(IGqTLm;NWE%qU֍w;)QDӶX-Öi(LIx^H? !d߲;Am@= qu/B 7,Ƕ! AD]%W/pGO'/ ]Bd@˲_Dq臑,hGް,e#2EU̓P;qszXl&2~$ #l&8vMѴ󼼐я;Ȥ)oێ Uͥ:.YQUBk NuYYWux{;ahr;~ y\at|/ 'i.*A׶XžXMf"5 ݯr4MS{He\o7#qz?dbPQ4NOIˀe94g9+ k|0:lRX,SHi @$77%)2 85A("%#++v1`ge\n#54Lj%r @ 4]K<'Oe9}*Β8agHy<ȹwOU͎.hC/f93 I!+H]9Ap.1P8V> a&ґm2-ÑH DYUxOD/AUɉ&Ű|// s"a<'y^Fɳ树 RJ" 0F":*v./l,ai]դ\؁9x>Klq]^AU4GOX9s,{C,SС~Ȳ$BiJUj˛H[^v# av8Nԕ/ɛaX4m+*iۖ}$˛}YRQҸ ܧY"2H֯z4 qLRat_C$H#CH.\ڶ0r B4 mEW my_ Ie|~^eF,E_+D9ˋB;O3I⺂$IR7 nFPq$+ 9;,aZ[t'SYZ|UU5ZH,[KSmY~ qU/D H:ci^( 8kŘ D'LjqBLSeٜ/oF4:m&:˲dkpԳNbI_OB$eEºn;膁/`ms zxHUUrY_x~Oqq]%('<+oJ&8EZ> Ӵy<\.e9Q w(cY .3oQ4tm;ۺQh敿ʲdFN۶'Q6-i"Mә(y{mZu#N"ӂ}_p059TeOߍqa~įOA7P?~J!at> ѺG_#9Q?TyY/*hhdWt`%yAU/[|ຂІa(CNQ8^xV StY)SJ,JTUM4B N}@$w4]m;<EP8#feya2lΧ3u(Cæ˅,c~m:I bMvD.뉜d&@|ybZTr]a`&j&ۺk4s9Yat Sqfv]ghz<%sQ0ƶCuA[ϳL)$fe]. y~{і3hF4e&K/hR$ML'q̳YbOuNVt݀Ix; B)0FZԷ7Švd+˴Dæ3ao+ ?)L4,oWTV׍tj~`eG7 NeY atM)a'8iZYܿ0uB?@Đ)]QeU˾Mә㉩]~(lӴ۶:.M*r9_ 6H5?_Zq((a,y[vMG8̭:-eӈOa=WK3$糔u:%@aP$4mӓ)y^I^KֵH7gY%T/ܗ32~}~2pI[9 dy8|PVxSbݖuq\躎ɺm eY!}MH2J4M^IGő뉑rFNxclmt_'``;bl'QJò.rwMKzi.~Y~ ) ۱1).%R4SJ`wTt]Ou.ew=l0qӰ,i_h/џ1e%Q CQI]DQ>FFˆ~؎BxeSZDej5i! c~LO(4ksOX&k˴^ 0 e{\W>}zbٶ]hܺC$[U,mx>y_fkbLګ~q,F?ݥ- $7jF!Ñ0JX\וg}5m_rfDM(|:+3v27љZbq<<'Ne5 0%ǒ=MZҍz*iR+ reͺ,O]U/' Ha@y, Ƕ_yF۶|Da>Rg% 2ضSԃ㞮JG-8JJy*O;IR0bcW}Z\(J8 eK!LJ7.52t]<+躞k\2U[C:Ł0ŧuC: NAd TjU]\ׅ,=춃U|*Kt|i-fBI^q0|A5vPe/U !|1,DvR7 (i}m3o~2௻$|M$}e p80M% @UW|+u= x<ҩ_1&PG[ ?) 4MvzwtZ7(De헭PG0(˒i|1.7 1?06.Ҋ,F(CD˥i&m!~p\(wa`Sn C5PJcu4.6-Gh$}-8 4q8u*9Ж}_>9h Ӊ4q ,˸^oL,r 5Àm;Ӥ4a!gUtuD 3&>uqˆpe\4{ELdɳo؎+g7 i ô1,(I|<v]Mי7l~8Ai(T@ߵ2[&0;S7$Z4?IZ By1rx<ю^-B׏/t]7XwYڔ|VvL4L+::ڮ;$kXw8O쇁u^9Èzؖ-c%1AӌuK4)=ܮ Aa̫,t;i{>FL]gyνj8M fb$M"_R=vM4¶i!m۳_2ٖ:lxv2 #Ǣl8a֝D(2ںPHAuqɷu4LTm2 Dд۾{J&VCe^uY,e YӉa?Ƒ$ijځC5ۼ{aӾ3.3a)m?=Pd9}SSג1oGE)Xm~3o}+]7HڅX8̋4e~W ;$8׍4N ض0IҌuۨӲq]8HRoo2(.YL#$Ñ|ee07#:6QehZtۦOm U4ĺڲL?yKbSa 'FY eLDSxn4قWφ8Y1Ga*u%DuRGC5 IDAT /u81-8MzVuõl>?CvH⬌Q$IQa>y]S7-д='Ǎ4Oqѧn+m/l,U!h0 =cD² E)QA@eVNĊ*D$?qTQ!^ne3h;oZ3i34]'U@C%Nq0-qX@J sGUּ|;up)?~rl| mv|2~~ZH| 7N*@׏N0|||P7 >'?oJ%2K0y*Nm  $n:oQr: ?UՐeh۲P0P)(EڲN'5*.QdR$t]X2m,Suq(N=2 Q0 :]rX0-\c'stE)J}|c&5 CQKT0dy*lǗt<<hv%y@ IҪ4ɫ8䅘=iܶis>y>JU}I@(DJcY]좨M g*pWu]8Qf ?$˴ȋBŠ)(k,ˢ뗌OTљ~HEC5ȟW 7/ 4]// ,eG׵äx3oZ}_(^'Mn1#a[,jw Sa_A( `aY r:R9q,J1=jg3V;^D]FUVtg*0>Ψ2)sT58X0tNi+g!cl8τaH8iŢadK2hx2 |ÁMVe*mF`r͗kr6`0E8N4h_F؎ܮ7k4\ٰq5u\qL6j > |Гm3͑o7Lƻ"Ǖ%d8a0t|Nu΃,K1 fnڒdm,szz cMߵBiE1 >xqUU 6ôa O2T!iPJV#OJ#踅XN'tKQ̞ H,íqQMc2xrKe (l8t}H@mrO;aӥ_h*JͶd `*GjV3{|Nma x<*9MxR .zqm?>f3Ļ~MQM۱^x0 unT#wD`Jʑ,Kl;yEmY#_y>=ё˅@.rVt6D˙i |<*IENDB`pulseaudio-dlna-0.5.3+git20200329/scripts/000077500000000000000000000000001364015200300176675ustar00rootroot00000000000000pulseaudio-dlna-0.5.3+git20200329/scripts/bootstrap.sh000077500000000000000000000054411364015200300222470ustar00rootroot00000000000000#!/bin/bash function lecho() { echo "" echo "####################################################################" echo "# $1" echo "####################################################################" } function install_tmate() { ensure_private_ssh_key lecho 'Installing tmate ...' hash tmate 2>/dev/null || { sudo add-apt-repository -y ppa:tmate.io/archive sudo apt-get update sudo apt-get -y install tmate tmux } hash tmux 2>/dev/null || { sudo apt-get -y install tmux } [[ -f ~/.tmate.conf ]] || { echo 'set -g terminal-overrides ""' > ~/.tmate.conf echo 'set -g xterm-keys on' >> ~/.tmate.conf echo 'source-file /usr/share/doc/tmux/examples/screen-keys.conf' >> ~/.tmate.conf } echo 'ok!' } function remove_tmate() { lecho 'Removing tmate ...' sudo apt-get remove tmate sudo ppa-purge ppa:tmate.io/archive } function run_tmate() { hash tmate 2>/dev/null && tmate } function ensure_private_ssh_key() { lecho 'Ensuring you have a private ssh key ...' if [ ! -f ~/.ssh/id_rsa ]; then ssh-keygen -f ~/.ssh/id_rsa -t rsa -b 4096 -N "" fi echo 'ok!' } function install_fonts() { lecho 'Installing powerline fonts' if [ ! -d /tmp/fonts ]; then git clone https://github.com/powerline/fonts.git /tmp/fonts bash /tmp/fonts/install.sh hash gsettings 2>/dev/null && { gsettings set org.gnome.desktop.interface monospace-font-name \ "Droid Sans Mono Dotted for Powerline 9"; } fi echo 'ok!' } function install_dev() { sudo apt-get install \ python3 \ python3-pip \ python3-setuptools \ python3-dbus \ python3-docopt \ python3-requests \ python3-setproctitle \ python3-gi \ python3-notify2 \ python3-psutil \ python3-chardet \ python3-netifaces \ python3-netaddr \ python3-pyroute2 \ python3-lxml \ python3-pychromecast \ vorbis-tools \ sox \ lame \ flac \ opus-tools \ pavucontrol \ virtualenv python3-dev git-core [[ -d ~/pulseaudio-dlna ]] || \ git clone https://github.com/masmu/pulseaudio-dlna.git ~/pulseaudio-dlna } while [ "$#" -gt "0" ]; do case $1 in --remote | --tmate) install_tmate run_tmate exit 0 ;; --install-tmate) install_tmate exit 0 ;; --install-fonts) install_fonts exit 0 ;; --remove-tmate) remove_tmate exit 0 ;; --dev) install_dev exit 0 ;; *) echo "Unknown option '$1'!" exit 1 ;; esac done echo "You did not specify any arguments!" exit 1pulseaudio-dlna-0.5.3+git20200329/scripts/capture.sh000077500000000000000000000060571364015200300217010ustar00rootroot00000000000000#!/bin/bash # # web-ui http://fritz.box/html/capture.html # HOST="fritz.box" ARGUMENTS="" IFACE="" function get_session_id() { local SID CHALLENGE=$(wget -O - "http://$HOST/login_sid.lua" 2>/dev/null \ | sed 's/.*\(.*\)<\/Challenge>.*/\1/') CPSTR="$CHALLENGE-$PASSWORD" MD5=$(echo -n $CPSTR | iconv -f ISO8859-1 -t UTF-16LE \ | md5sum -b | awk '{print substr($0,1,32)}') RESPONSE="$CHALLENGE-$MD5" SID=$(wget -O - --post-data="?username=&response=$RESPONSE" \ "http://$HOST/login_sid.lua" 2>/dev/null \ | sed 's/.*\(.*\)<\/SID>.*/\1/') echo "$SID" } echo -n "Enter your router password: "; read PASSWORD; SID=$(get_session_id) if [ "$SID" == "0000000000000000" ]; then echo "Authentication failure!" exit fi echo "" echo "What do you want to capture?" echo "INTERNET:" echo " 1) Internet" echo " 2) Interface 0" echo " 3) Routing Interface" echo "INTERFACES:" echo " 4) tunl0" echo " 5) eth0" echo " 6) eth1" echo " 7) eth2" echo " 8) eth3" echo " 9) lan" echo " 10) hotspot" echo " 11) wifi0" echo " 12) ath0" echo "WIFI:" echo " 13) AP 2,4 GHz ath0, Interface 1" echo " 14) AP 2,4 GHz ath0, Interface 0" echo " 15) HW 2,4 GHz wifi0, Interface 0" echo "" while true; do echo -n "Enter your choice [0-15] ('q' for quit): "; read MODE; if (("$MODE" > "0")) && (("$MODE" < "16")); then if [ "$MODE" == "1" ]; then IFACE="2-1" elif [ "$MODE" == "2" ]; then IFACE="3-17" elif [ "$MODE" == "3" ]; then IFACE="3-0" elif [ "$MODE" == "4" ]; then IFACE="1-tunl0" elif [ "$MODE" == "5" ]; then IFACE="1-eth0" elif [ "$MODE" == "6" ]; then IFACE="1-eth1" elif [ "$MODE" == "7" ]; then IFACE="1-eth2" elif [ "$MODE" == "8" ]; then IFACE="1-eth3" elif [ "$MODE" == "9" ]; then IFACE="1-lan" elif [ "$MODE" == "10" ]; then IFACE="1-hotspot" elif [ "$MODE" == "11" ]; then IFACE="1-wifi0" elif [ "$MODE" == "12" ]; then IFACE="1-ath0" elif [ "$MODE" == "13" ]; then IFACE="4-131" elif [ "$MODE" == "14" ]; then IFACE="4-130" elif [ "$MODE" == "15" ]; then IFACE="4-128" fi break elif [ "$MODE" == "q" ]; then exit fi done echo "" echo "Do you also want to write a pcap file?" echo "" while true; do echo -n "Enter your choice [y-n] ('q' for quit): "; read WRITE_PCAP; if [ "$WRITE_PCAP" == "y" ]; then PCAP_FILE="$(date +%Y-%m-%d_%H:%M:%S).pcap" WIRESHARK_ARGS="-w $PCAP_FILE" break elif [ "$WRITE_PCAP" == "n" ]; then WIRESHARK_ARGS="" break elif [ "$WRITE_PCAP" == "q" ]; then exit fi done echo "" echo "Starting wireshark ..." echo "" wget -O - "http://$HOST/cgi-bin/capture_notimeout?ifaceorminor=$IFACE&snaplen=1600&capture=Start&sid=$SID" \ 2>/dev/null \ | wireshark -k $WIRESHARK_ARGS -i - pulseaudio-dlna-0.5.3+git20200329/scripts/chromecast-beam.py000077500000000000000000000520071364015200300233020ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . ''' Usage: chromecast-beam.py chromecast-beam.py [--host ] [-p ] [--mime-type ] [--debug] [--network-address-translation] [--audio-language=] [--audio-track=] [--audio-track-id=] [--transcode ] [--transcode-video ] [--transcode-audio ] [--start-time=] [--sub-titles] [--debug] Options: -h --host= Bind to stream server socket to that host. -p --port= Set the server port [default: 8080]. -n --network-address-translation Enable network address translation mode. When used the application will fetch your external IP address and bind to all local interfaces. You just have to setup a port forwarding in your router. --audio-language= Select the audio language. --audio-track= Select the audio track. --audio-track-id= Select the audio track ID. -t --transcode= Enable transcoding [default: none]. Available options: audio - Transcode audio data video - Transcode video data both - Transcode both --transcode-video= Select the transcoded video options. Available options: c|codec - Set the codec b|bitrate - Set the bitrate s|scale - Set the scale --transcode-audio= Select the transcoded audio options. Available options: c|codec - Set the codec b|bitrate - Set the bitrate ch|channels - Set the channels s|samplerate - Set the samplerate l|language - Set the language --start-time= Set the start time of the video in seconds. --mime-type= Set the media's mimetype instead of guessing it. --sub-titles Enable sub titles. --debug Enable debug mode. Examples: - chromecast-beam.py 192.168.1.2 ~/test.mkv will stream the file ~/test.mkv unmodified to the chromecast with the IP 192.168.1.2 - chromecast-beam.py --transcode=both 192.168.1.2 ~/test.mkv will transcode audio and video using the default settings and stream that to the Chromecast with the IP 192.168.1.2 - chromecast-beam.py --audio-track 0 192.168.1.2 ~/test.mkv will just select audio line 0 from file ~/test.mkv and stream that to the Chromecast with the IP 192.168.1.2 - chromecast-beam.py --audio-track 1 --transcode=audio 192.168.1.2 ~/test.mkv will just select audio line 1 from file ~/test.mkv, transcode that audio line using the transcode default settings and stream that with the unmodified video data to the Chromecast with the IP 192.168.1.2 - chromecast-beam.py --audio-track 1 --transcode=video 192.168.1.2 ~/test.mkv will just select the unmodified audio line 1 from file ~/test.mkv, transcode the video data using the video transcode default settings and stream that with the unmodified video data to the Chromecast with the IP 192.168.1.2 - chromecast-beam.py --transcode-video=b=2000,c=hevc 192.168.1.2 ~/test.mkv will transcode the video data using the encoder hevc and a bitrate of 2000 from file ~/test.mkv, and stream that to the Chromecast with the IP 192.168.1.2 - chromecast-beam.py --transcode-video=b=2000,c=x264 --transcode-audio=b=256,c=mpga 192.168.1.2 ~/test.mkv will transcode the video data using the encoder x264 and a bitrate of 2000, transcode the audio line using the encoder mpga using a bitrate of 256 from file ~/test.mkv, and streams that to the Chromecast with the IP 192.168.1.2 ''' import docopt import logging import os import threading import mimetypes import sys import subprocess import signal import requests import json import shutil import traceback import re import errno import http.server import socketserver import pychromecast import pulseaudio_dlna.utils.network logger = logging.getLogger('chromecast-beam') RE_IPV4 = r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" PORT_MIN = 1 PORT_MAX = 65535 class StoppableThread(threading.Thread): MODE_IMMEDIATE = 'immediate' def __init__(self, *args, **kwargs): threading.Thread.__init__(self, *args, **kwargs) self.stop_event = threading.Event() self.stop_mode = None def stop(self, immediate=False): if not self.is_stopped: if immediate: self.stop_mode = immediate self.stop_event.set() def wait(self): self.stop_event.wait() @property def is_stopped(self): return self.stop_event.isSet() class ChromecastThread(StoppableThread): PORT = 8009 def __init__(self, chromecast_host, media_url, mime_type=None, *args, **kwargs): StoppableThread.__init__(self, *args, **kwargs) self.chromecast_host = chromecast_host self.media_url = media_url self.mime_type = mime_type or 'video/mp4' self.desired_volume = 1.0 self.timeout = 5 def run(self): chromecast = pychromecast.Chromecast( host=chromecast_host, port=self.PORT, timeout=self.timeout) chromecast.media_controller.play_media( self.media_url, content_type=self.mime_type, stream_type=pychromecast.controllers.media.STREAM_TYPE_LIVE, autoplay=True, ) logger.info( 'Chromecast status: Volume {volume} ({muted})'.format( muted='Muted' if chromecast.media_controller.status.volume_muted else 'Unmuted', volume=chromecast.media_controller.status.volume_level * 100)) self.wait() if self.stop_mode != StoppableThread.MODE_IMMEDIATE: chromecast.quit_app() class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): allow_reuse_address = True daemon_threads = True def __init__(self, file_uri, bind_host, request_host, port, handler=None): self.file_uri = file_uri self.file_path, self.file_name = os.path.split(self.file_uri) self.bind_host = bind_host self.request_host = request_host self.port = port self.handler = handler or DefaultRequestHandler os.chdir(self.file_path) socketserver.TCPServer.__init__( self, (self.bind_host, self.port), self.handler) @property def file_url(self): server_url = 'http://{host}:{port}'.format( host=self.request_host, port=self.port) return os.path.join(server_url, self.file_name) def handle_error(self, *args, **kwargs): self.shutdown() class EncoderSettings(object): ENCODER_BINARY = '/usr/bin/vlc' TRANSCODE_USED = False TRANSCODE_VIDEO_CODEC = None TRANSCODE_VIDEO_BITRATE = None TRANSCODE_VIDEO_SCALE = None TRANSCODE_AUDIO_CODEC = None TRANSCODE_AUDIO_BITRATE = None TRANSCODE_AUDIO_CHANNELS = None TRANSCODE_AUDIO_SAMPLERATE = None TRANSCODE_AUDIO_LANG = None AUDIO_LANGUAGE = None AUDIO_TRACK = None AUDIO_TRACK_ID = None TRANSCODE_VIDEO_DEFAULTS_SET = False TRANSCODE_VIDEO_CODEC_DEF = 'h264' TRANSCODE_VIDEO_BITRATE_DEF = 3000 TRANSCODE_VIDEO_SCALE_DEF = 'Auto' TRANSCODE_VIDEO_MUX_DEF = 'mkv' TRANSCODE_AUDIO_DEFAULTS_SET = False TRANSCODE_AUDIO_CODEC_DEF = 'mp3' TRANSCODE_AUDIO_BITRATE_DEF = 192 TRANSCODE_AUDIO_CHANNELS_DEF = 2 TRANSCODE_AUDIO_SAMPLERATE_DEF = 44100 TRANSCODE_AUDIO_LANG_DEF = None START_TIME = None SUB_TITLES = False @classmethod def _decode_settings(cls, settings): try: data = {} for setting in settings.split(','): k, v = setting.split('=') data[k] = v return data except Exception: return {} @classmethod def _apply_option(cls, attribute, value): if attribute is not None: logger.info('{}={}'.format(attribute, value)) setattr(cls, attribute, value) @classmethod def _apply_options(cls, options, option_map): for option, value in cls._decode_settings(options).items(): attribute = option_map.get(option, None) cls._apply_option(attribute, value) @classmethod def set_video_defaults(cls): if cls.TRANSCODE_VIDEO_DEFAULTS_SET: return cls.TRANSCODE_VIDEO_DEFAULTS_SET = True cls._apply_option( 'TRANSCODE_VIDEO_CODEC', cls.TRANSCODE_VIDEO_CODEC_DEF) cls._apply_option( 'TRANSCODE_VIDEO_BITRATE', cls.TRANSCODE_VIDEO_BITRATE_DEF) cls._apply_option( 'TRANSCODE_VIDEO_SCALE', cls.TRANSCODE_VIDEO_SCALE_DEF) @classmethod def set_audio_defaults(cls): if cls.TRANSCODE_AUDIO_DEFAULTS_SET: return cls.TRANSCODE_AUDIO_DEFAULTS_SET = True cls._apply_option( 'TRANSCODE_AUDIO_CODEC', cls.TRANSCODE_AUDIO_CODEC_DEF) cls._apply_option( 'TRANSCODE_AUDIO_BITRATE', cls.TRANSCODE_AUDIO_BITRATE_DEF) cls._apply_option( 'TRANSCODE_AUDIO_CHANNELS', cls.TRANSCODE_AUDIO_CHANNELS_DEF) cls._apply_option( 'TRANSCODE_AUDIO_SAMPLERATE', cls.TRANSCODE_AUDIO_SAMPLERATE_DEF) @classmethod def set_options(cls, options): used = False if options.get('--start-time', None): used = True cls.TRANSCODE_USED = True cls.set_video_defaults() cls._apply_option('START_TIME', options['--start-time']) if options.get('--sub-titles', None): used = True cls._apply_option('SUB_TITLES', True) if options.get('--audio-track', None): used = True cls.TRANSCODE_USED = True cls.set_audio_defaults() cls._apply_option('AUDIO_TRACK', options['--audio-track']) if options.get('--audio-language', None): used = True cls.TRANSCODE_USED = True cls.set_audio_defaults() cls._apply_option('AUDIO_LANGUAGE', options['--audio-language']) if options.get('--audio-track-id', None): used = True cls.TRANSCODE_USED = True cls.set_audio_defaults() cls._apply_option('AUDIO_TRACK_ID', options['--audio-track-id']) if options.get('--transcode', None): if options['--transcode'] in ['both', 'audio', 'video']: used = True cls.TRANSCODE_USED = True if options['--transcode'] in ['both', 'video']: cls.set_video_defaults() if options['--transcode'] in ['both', 'audio']: cls.set_audio_defaults() if options.get('--transcode-video', None): used = True cls.TRANSCODE_USED = True option_map = { 'c': 'TRANSCODE_VIDEO_CODEC', 'codec': 'TRANSCODE_VIDEO_CODEC', 'b': 'TRANSCODE_VIDEO_BITRATE', 'br': 'TRANSCODE_VIDEO_BITRATE', 'bitrate': 'TRANSCODE_VIDEO_BITRATE', 's': 'TRANSCODE_VIDEO_SCALE', 'scale': 'TRANSCODE_VIDEO_SCALE', } cls.set_video_defaults() cls._apply_options(options['--transcode-video'], option_map) if options.get('--transcode-audio', None): used = True cls.TRANSCODE_USED = True option_map = { 'c': 'TRANSCODE_AUDIO_CODEC', 'codec': 'TRANSCODE_AUDIO_CODEC', 'b': 'TRANSCODE_AUDIO_BITRATE', 'br': 'TRANSCODE_AUDIO_BITRATE', 'bitrate': 'TRANSCODE_AUDIO_BITRATE', 'ch': 'TRANSCODE_AUDIO_CHANNELS', 'channles': 'TRANSCODE_AUDIO_CHANNELS', 's': 'TRANSCODE_AUDIO_SAMPLERATE', 'sr': 'TRANSCODE_AUDIO_SAMPLERATE', 'samplerate': 'TRANSCODE_AUDIO_SAMPLERATE', 'l': 'TRANSCODE_AUDIO_LANG', 'lang': 'TRANSCODE_AUDIO_LANG', 'language': 'TRANSCODE_AUDIO_LANG', } cls.set_audio_defaults() cls._apply_options(options['--transcode-audio'], option_map) return used class VLCEncoderSettings(EncoderSettings): @classmethod def _transcode_cmd_str(cls): options = {} if cls.TRANSCODE_VIDEO_CODEC: options['vcodec'] = cls.TRANSCODE_VIDEO_CODEC if cls.TRANSCODE_VIDEO_BITRATE: options['vb'] = cls.TRANSCODE_VIDEO_BITRATE if cls.TRANSCODE_VIDEO_SCALE: options['scale'] = cls.TRANSCODE_VIDEO_SCALE if cls.TRANSCODE_AUDIO_CODEC: options['acodec'] = cls.TRANSCODE_AUDIO_CODEC if cls.TRANSCODE_AUDIO_BITRATE: options['ab'] = cls.TRANSCODE_AUDIO_BITRATE if cls.TRANSCODE_AUDIO_CHANNELS: options['channels'] = cls.TRANSCODE_AUDIO_CHANNELS if cls.TRANSCODE_AUDIO_SAMPLERATE: options['samplerate'] = cls.TRANSCODE_AUDIO_SAMPLERATE if cls.TRANSCODE_AUDIO_LANG: options['alang'] = cls.TRANSCODE_AUDIO_LANG if cls.SUB_TITLES: options['soverlay'] = None return ','.join([ '{}={}'.format(k, v) if v else k for k, v in options.items() ]) @classmethod def command(cls, file_path): command = [ cls.ENCODER_BINARY, '--intf', 'dummy', file_path, ':play-and-exit', ':no-sout-all', ] if cls.AUDIO_LANGUAGE: command.append(':audio-language=' + cls.AUDIO_LANGUAGE) if cls.AUDIO_TRACK: command.append(':audio-track=' + cls.AUDIO_TRACK) if cls.AUDIO_TRACK_ID: command.append(':audio-track-id=' + cls.AUDIO_TRACK_ID) if cls.START_TIME: command.append(':start-time=' + cls.START_TIME) if cls.TRANSCODE_USED: return command + [ ':sout=#transcode{' + cls._transcode_cmd_str() + '}' ':std{access=file,mux=' + cls.TRANSCODE_VIDEO_MUX_DEF + ',dst=-}', ] else: return command + [ ':sout=#file{access=file,mux=' + cls.TRANSCODE_VIDEO_MUX_DEF + ',dst=-}' ] class DefaultRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self, *args, **kwargs): logger.info('Serving unmodified media file to {} ...'.format( self.client_address[0])) http.server.SimpleHTTPRequestHandler.do_GET(self, *args, **kwargs) def log_request(self, code='-', size='-'): logger.info('{} - {}'.format(self.requestline, code)) class TranscodeRequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): try: client_address = self.client_address[0] logger.info('Serving transcoded media file to {} ...'.format( client_address)) self.send_head() path = self.translate_path(self.path) command = VLCEncoderSettings.command(path) logger.info('Launching {}'.format(command)) with open(os.devnull, 'w') as dev_null: encoder_process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=sys.stdout.fileno()) shutil.copyfileobj(encoder_process.stdout, self.wfile) except IOError as e: if e.errno == errno.EPIPE: logger.info( 'Connection from {} closed.'.format(client_address)) else: traceback.print_exc() except Exception: traceback.print_exc() finally: pid = encoder_process.pid logger.info('Terminating process {}'.format(pid)) try: os.kill(pid, signal.SIGKILL) except Exception: pass def log_request(self, code='-', size='-'): logger.info('{} - {}'.format(self.requestline, code)) def get_external_ip(): response = requests.get('http://ifconfig.lancode.de') if response.status_code == 200: data = json.loads(response.content) return data.get('ip', None) return None # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: # python3 -m scripts.chromecast-beam 192.168.1.10 ~/videos/test.mkv if __name__ == "__main__": options = docopt.docopt(__doc__, version='0.1') level = logging.DEBUG if not options['--debug']: level = logging.INFO logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.basicConfig( level=level, format='%(asctime)s %(name)-46s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M:%S') media_file = options.get('', None) if not os.path.isfile(media_file): logger.critical('{} is not a file!'.format(media_file)) sys.exit(1) mime_type = options.get('--mime-type', None) if mime_type is None: mime_type, encoding = mimetypes.guess_type(media_file) try: port = int(options.get('--port')) if port < PORT_MIN or port > PORT_MAX: raise ValueError() except ValueError: logger.critical('Port {} is not a valid port number!'.format( options.get('--port'))) sys.exit(1) chromecast_host = options.get('') if not re.match(RE_IPV4, chromecast_host): logger.critical('{} is no valid IP address!'.format(chromecast_host)) sys.exit(1) host = options.get('--host', None) if options.get('--network-address-translation', None): bind_host = '' request_host = host or get_external_ip() else: bind_host = host or pulseaudio_dlna.utils.network.get_host_by_ip( chromecast_host) request_host = host or bind_host if request_host is None: logger.critical('Could not determine host address!') sys.exit(1) else: logger.info('Using host {}:{} '.format( '*' if bind_host == '' else bind_host, port)) handler = DefaultRequestHandler if VLCEncoderSettings.set_options(options): handler = TranscodeRequestHandler http_server = ThreadedHTTPServer( media_file, bind_host, request_host, port, handler) chromecast_thread = ChromecastThread( chromecast_host, http_server.file_url, mime_type=mime_type) chromecast_thread.start() try: http_server.serve_forever() except KeyboardInterrupt: chromecast_thread.stop() finally: chromecast_thread.stop(immediate=True) chromecast_thread.join() pulseaudio-dlna-0.5.3+git20200329/scripts/fritzbox-device-sharing.py000077500000000000000000000066511364015200300250110ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import requests import sys import json import pulseaudio_dlna import pulseaudio_dlna.holder import pulseaudio_dlna.plugins.dlna import pulseaudio_dlna.codecs STEPS_PATTERN = """ Step 1: Open your Fritzbox web interface (http://fritz.box/) Step 2: Goto Internet -> Permit Access -> Port-Sharing Step 3: Add a port sharing via the button "New Port Sharing" for each device you want to share. """ SHARE_PATTERN = """############################################################################# To share access to the device "{name}" add the following entry: ------------------------------------------------------- Create New Port Sharing [x] Port sharing enabled for "Other applications"') Name: "DLNA for {name}" (does not matter, just the name of the rule) Protocol: "TCP" From Port "{port}" To Port: "{port}" To Computer: "?" (does not matter, make sure the IP address is correct) To IP-Address: "{ip}" To Port: "{port}" [OK] ------------------------------------------------------- The link to share the device is : {link} """ class IPDetector(): TIMEOUT = 5 def __init__(self): self.public_ip = None def get_public_ip(self): self.public_ip = self._get_public_ip() return self.public_ip is not None def _get_public_ip(self): response = requests.get('http://ifconfig.lancode.de') if response.status_code == 200: data = json.loads(response.content) return data.get('ip', None) return None class DLNADiscover(): PLUGINS = [ pulseaudio_dlna.plugins.dlna.DLNAPlugin(), ] TIMEOUT = 5 def __init__(self, max_workers=10): self.devices = [] def discover_devices(self): self.devices = self._discover_devices() return len(self.devices) > 0 def _discover_devices(self): holder = pulseaudio_dlna.holder.Holder(self.PLUGINS) holder.search(ttl=self.TIMEOUT) return list(holder.devices.values()) ip_detector = IPDetector() print('Getting your external IP address ...') if not ip_detector.get_public_ip(): print('Could not get your external IP! Aborting.') sys.exit(1) print('Discovering devices ...') dlna_discover = DLNADiscover() if not dlna_discover.discover_devices(): print('Could not find any devices! Aborting.') sys.exit(1) print(STEPS_PATTERN) for device in dlna_discover.devices: link = device.upnp_device.access_url.replace( device.ip, ip_detector.public_ip) print((SHARE_PATTERN.format( name=device.name, ip=device.ip, port=device.port, link=link))) pulseaudio-dlna-0.5.3+git20200329/scripts/radio.py000077500000000000000000000116601364015200300213460ustar00rootroot00000000000000#!/usr/bin/python3 # -*- coding: utf-8 -*- # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import requests import logging import sys import concurrent.futures import pulseaudio_dlna import pulseaudio_dlna.holder import pulseaudio_dlna.plugins.dlna import pulseaudio_dlna.plugins.chromecast import pulseaudio_dlna.codecs level = logging.INFO logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.basicConfig( level=level, format='%(asctime)s %(name)-46s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M:%S') logger = logging.getLogger('radio') class RadioLauncher(): PLUGINS = [ pulseaudio_dlna.plugins.dlna.DLNAPlugin(), pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), ] def __init__(self, max_workers=10): self.devices = self._discover_devices() self.thread_pool = concurrent.futures.ThreadPoolExecutor( max_workers=max_workers) def stop(self, name, flavour=None): self.thread_pool.submit(self._stop, name, flavour) def _stop(self, name, flavour=None): device = self._get_device(name, flavour) if device: return_code, message = device.stop() if return_code == 200: logger.info( 'The device "{name}" was instructed to stop'.format( name=device.label)) else: logger.info( 'The device "{name}" failed to stop ({code})'.format( name=device.label, code=return_code)) def play(self, url, name, flavour=None, artist=None, title=None, thumb=None): self.thread_pool.submit( self._play, url, name, flavour, artist, title, thumb) def _play(self, url, name, flavour=None, artist=None, title=None, thumb=None): if url.lower().endswith('.m3u'): url = self._get_playlist_url(url) codec = self._get_codec(url) device = self._get_device(name, flavour) if device: return_code, message = device.play(url, codec, artist, title, thumb) if return_code == 200: logger.info( 'The device "{name}" was instructed to play'.format( name=device.label)) else: logger.info( 'The device "{name}" failed to play ({code})'.format( name=device.label, code=return_code)) def _get_device(self, name, flavour=None): for device in self.devices: if flavour: if device.name == name and device.flavour == flavour: return device else: if device.name == name: return device return None def _get_codec(self, url): for identifier, _type in pulseaudio_dlna.codecs.CODECS.items(): codec = _type() if url.endswith(codec.suffix): return codec return pulseaudio_dlna.codecs.Mp3Codec() def _get_playlist_url(self, url): response = requests.get(url=url) for line in response.content.split('\n'): if line.lower().startswith('http://'): return line return None def _discover_devices(self): holder = pulseaudio_dlna.holder.Holder(self.PLUGINS) holder.search(ttl=5) logger.info('Found the following devices:') for udn, device in list(holder.devices.items()): logger.info(' - "{name}" ({flavour})'.format( name=device.name, flavour=device.flavour)) return list(holder.devices.values()) # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: # python3 -m scripts/radio [--list | --stop] args = sys.argv[1:] rl = RadioLauncher() if len(args) > 0 and args[0] == '--list': sys.exit(0) devices = [ ('Alle', 'Chromecast'), ] for device in devices: name, flavour = device if len(args) > 0 and args[0] == '--stop': rl.stop(name, flavour) else: rl.play( 'http://www.wdr.de/wdrlive/media/einslive.m3u', name, flavour, 'Radio', 'Einslive', 'https://lh4.ggpht.com/7ssDAyz52UL1ahViwMkCrtfbdj45RU1Gqqpw3ncYjMrjhZofECX01j4nBufhCAkRFtRm=w600') pulseaudio-dlna-0.5.3+git20200329/setup.py000066400000000000000000000040131364015200300177100ustar00rootroot00000000000000#!/usr/bin/python3 # This file is part of pulseaudio-dlna. # pulseaudio-dlna 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. # pulseaudio-dlna 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 pulseaudio-dlna. If not, see . import setuptools setuptools.setup( name="pulseaudio-dlna", author="Massimo Mund", author_email="mo@lancode.de", url="https://github.com/masmu/pulseaudio-dlna", description="A small DLNA server which brings DLNA / UPNP support" "to PulseAudio and Linux.", license="GPLv3", platforms="Debian GNU/Linux", classifiers=[ "Development Status :: 4 - Beta", "Programming Language :: Python :: 3", "Environment :: Console", "Topic :: Multimedia :: Sound/Audio", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", ], version='0.6.0', py_modules=[], packages=setuptools.find_packages(), install_requires=[ "docopt >= 0.6.1", "requests >= 2.2.1", "setproctitle >= 1.1.10", "notify2 >= 0.3", "psutil >= 5.4.7", "chardet >= 3.0.4", "pyroute2 >= 0.3.5", "netifaces >= 0.10.0", "lxml >= 3", "pychromecast >= 2.3.0", "PyGObject >= 3.3.0", "dbus-python >= 1.0.0", ], entry_points={ "console_scripts": [ "pulseaudio-dlna = pulseaudio_dlna.__main__:main", ] }, data_files=[ ("share/man/man1", ["man/pulseaudio-dlna.1"]), ], package_data={ "pulseaudio_dlna": ["images/*.png"], } )