pax_global_header00006660000000000000000000000064122442002320014501gustar00rootroot0000000000000052 comment=2b7bc870cffc2ea5c87de2e99e45f95fba13c881 mopidy-0.17.0/000077500000000000000000000000001224420023200130675ustar00rootroot00000000000000mopidy-0.17.0/.coveragerc000066400000000000000000000001161224420023200152060ustar00rootroot00000000000000[report] omit = */pyshared/* */python?.?/* */site-packages/nose/* mopidy-0.17.0/.gitignore000066400000000000000000000002361224420023200150600ustar00rootroot00000000000000*.egg-info *.pyc *.swp .coverage .idea .noseids .tox MANIFEST build/ cover/ coverage.xml dist/ docs/_build/ mopidy.log* node_modules/ nosetests.xml *~ *.orig mopidy-0.17.0/.mailmap000066400000000000000000000012751224420023200145150ustar00rootroot00000000000000Thomas Adamcik Thomas Adamcik Thomas Adamcik Thomas Adacmik Kristian Klette Johannes Knutsen Johannes Knutsen John Bäckstrand Alli Witheford Alexandre Petitjean Alexandre Petitjean Javier Domingo Cansino Lasse Bigum mopidy-0.17.0/.travis.yml000066400000000000000000000013511224420023200152000ustar00rootroot00000000000000language: python install: - "wget -O - http://apt.mopidy.com/mopidy.gpg | sudo apt-key add -" - "sudo wget -O /etc/apt/sources.list.d/mopidy.list http://apt.mopidy.com/mopidy.list" - "sudo apt-get update || true" - "sudo apt-get install $(apt-cache depends mopidy | awk '$2 !~ /mopidy|python:any/ {print $2}')" - "pip install coveralls flake8" before_script: - "rm $VIRTUAL_ENV/lib/python$TRAVIS_PYTHON_VERSION/no-global-site-packages.txt" script: - "flake8 $(find . -iname '*.py')" - "nosetests --with-coverage --cover-package=mopidy" after_success: - "coveralls" notifications: irc: channels: - "irc.freenode.org#mopidy" on_success: change on_failure: change use_notice: true skip_join: true mopidy-0.17.0/AUTHORS000066400000000000000000000022751224420023200141450ustar00rootroot00000000000000- Stein Magnus Jodal - Johannes Knutsen - Thomas Adamcik - Kristian Klette - Martins Grunskis - Henrik Olsson - Antoine Pierlot-Garcin - John Bäckstrand - Fred Hatfull - Erling Børresen - David C - Christian Johansen - Matt Bray - Trygve Aaberge - Wouter van Wijk - Jeremy B. Merrill - 0xadam - herrernst - Nick Steel - Zan Dobersek - Thomas Refis - Janez Troha - Tobias Sauerwein - Alli Witheford - Alexandre Petitjean - Terje Larsen - Javier Domingo Cansino - Pavol Babincak - Javier Domingo - Lasse Bigum - David Eisner mopidy-0.17.0/LICENSE000066400000000000000000000261361224420023200141040ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. mopidy-0.17.0/MANIFEST.in000066400000000000000000000004471224420023200146320ustar00rootroot00000000000000include *.rst include LICENSE include MANIFEST.in include data/mopidy.desktop recursive-include docs * prune docs/_build recursive-include mopidy *.conf recursive-include mopidy/frontends/http/data * recursive-include requirements * recursive-include tests *.py recursive-include tests/data * mopidy-0.17.0/README.rst000066400000000000000000000034341224420023200145620ustar00rootroot00000000000000****** Mopidy ****** Mopidy is a music server which can play music both from multiple sources, like your local hard drive, radio streams, and from Spotify and SoundCloud. Searches combines results from all music sources, and you can mix tracks from all sources in your play queue. Your playlists from Spotify or SoundCloud are also available for use. To control your Mopidy music server, you can use one of Mopidy's web clients, the Ubuntu Sound Menu, any device on the same network which can control UPnP MediaRenderers, or any MPD client. MPD clients are available for many platforms, including Windows, OS X, Linux, Android and iOS. To get started with Mopidy, check out `the docs `_. - `Documentation `_ - `Source code `_ - `Issue tracker `_ - `CI server `_ - `Download development snapshot `_ - IRC: ``#mopidy`` at `irc.freenode.net `_ - Mailing list: `mopidy@googlegroups.com `_ - Twitter: `@mopidy `_ .. image:: https://pypip.in/v/Mopidy/badge.png :target: https://crate.io/packages/Mopidy/ :alt: Latest PyPI version .. image:: https://pypip.in/d/Mopidy/badge.png :target: https://crate.io/packages/Mopidy/ :alt: Number of PyPI downloads .. image:: https://travis-ci.org/mopidy/mopidy.png?branch=develop :target: https://travis-ci.org/mopidy/mopidy :alt: Travis CI build status .. image:: https://coveralls.io/repos/mopidy/mopidy/badge.png?branch=develop :target: https://coveralls.io/r/mopidy/mopidy?branch=develop :alt: Test coverage mopidy-0.17.0/data/000077500000000000000000000000001224420023200140005ustar00rootroot00000000000000mopidy-0.17.0/data/mopidy.desktop000066400000000000000000000003651224420023200167000ustar00rootroot00000000000000[Desktop Entry] Type=Application Version=1.0 Name=Mopidy Music Server Comment=MPD music server with Spotify support Icon=audio-x-generic TryExec=mopidy Exec=mopidy Terminal=true Categories=AudioVideo;Audio;Player;ConsoleOnly; StartupNotify=true mopidy-0.17.0/docs/000077500000000000000000000000001224420023200140175ustar00rootroot00000000000000mopidy-0.17.0/docs/Makefile000066400000000000000000000107561224420023200154700ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Mopidy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Mopidy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Mopidy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Mopidy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." mopidy-0.17.0/docs/_static/000077500000000000000000000000001224420023200154455ustar00rootroot00000000000000mopidy-0.17.0/docs/_static/dz0ny-mopidy-lux.png000066400000000000000000020165271224420023200213410ustar00rootroot00000000000000PNG  IHDRT pHYs+tIME'Br IDATxgeuC97&0CQYY95DlXkYزe XZHk(QPL"R "6B{'Tp{ tB.ֽv:k>8o"*H*""D"H$DcT{%&DD"H$pD"H$D;0q&4MIWb`/Ϟ}Zu3hF|vӥ/1#|^~%[5>Y_/z?j[f^$T p ر^{F1`s8AS: Lϩ .y>_g؟ХB@xg<gϻpg?K%}VOw+iOPqx^.I+ĉn}$Nl!+Cx+nZZ\x[^_SD"H$<|>9W7ght;D!JzcH$D"K}>,IF_U$AqG*̫^v) B`T*K"cAy!"IJ1c4"n=l͍H)H$D")MM'¶*%!vu &\mF;k S5zh<^\\LGQbmuD@Qno;sydž ֘śBPUA$"̳,RkE$ϳWW]:JUe/_FPOS:[(T#qˇW'}[hٟkߥۿo>>@w%]fF"H$D9D(KgNשz3Yyk5RQ('j^3c[!kv7uZ/EY&UP_~˞*Ho}[?OOI-R -S6=[M.O~6P$TVIݭd}W~/"|ۨIBpCbè("Fz7_}?1ՒCLLEfF >(13^H$D"wv) Y<$/tѐ'%t kp5|**ܹiGi;~ _ ޽s28x,i5{7D"$1 of 5Q >8;ᒙZ>Ⱦa@}ϵKݺ%*G'=oIm~G:)JyvuY~㒫nr!;{fW}g0Ыj.?^je{>ٽ>t5×W'"}i ?x䧚釿m%^D"H$yWPU^yeRB#l#we_8w/tT˶@PieY8WzBpy_El6+|ɲ̹fQo6zsyu: V1Ucvi۾M~ kϴ9m?#׍??YW, pe?׿Dy}%wt_x2|̭{ڥ;pWlz&t^4y$Dr{ov|?g`b+++++|U|=vXv}=#"z*p }xY@2 ,KfofH]3 PEei T&W)nTUZ*F+u߷R+ޘ[v-e6/[ӈZ+<#?u\ ܉x7\='-;_=;oks آL˜7QJ=7wj01e~$D"HGE$ ":6C̟ac8$__\uo{u:DB ""{ݭ5EQ:$GB"GD7xU9*Α^Ula(GcOO՗m/]hׇ]3=믍-:Z 7H:Zhwvlq$iS/l|w]y3ikR|;&<(""}ͻ^}ozn7MLĮD"H$y=M?C6۷@e$24Pah)i>cp-hccX緩JY_vdsmUPk)#|_!9q`zx3yۿ>_Cvf;Ǐ(\..{sϛs{?uخ|~ʿf =//ELkZfs5{#?~QG~KSDPݞ'o釿~>1=D"H$r%7#Ge裏zI,eȀ_d;JMMwQ͎L&밿5-NW#*H+''I[;qbik^jf"BAʲtΕέ՛ g'Ay@kφZk+KTPbo~΄{{#Cmee0?7\yѓ}` !쮏}nڲuUUDPPE9ۚ_ş{ߍۗԯw#'k_]/~Տ|_U1BLD"H$9я~{<9nw?H8k\ =yKcҖV eМhjy^ۭfVJWL&ÇfQYi.,o%4GDEt6ي?\/sEb3u# m6LW>bVa}IC ܩ"5;.k^ovoiU Eџi^GU0s ]E0)I^!ÍڝNFZI^n nD"H$PJ =-ޜm @붷Jv6ݙ^ YT4V%jZӯ\CsvO~Zːf6;_;^ fZTff)#H$D"7eBvN9Hto(efe LJz) ދ"25!,~osiⴏ!+4J8ϥ @D`JpOr]3l\8ĞV UUSG "O={Ӈx,pvT\\&Ȗw:|e٫G aؤiZc%ff*$0w$-}(kzQ>!dOԲQլ(A݅~D`6浯~gos}+ !ւt΢*(X֩#NḣJg" R t&3)t& R:c~r?)ffǏi\Q` 8EslkD ūAJ!`TQjb:^)hwC AA@*w V{$,wSէ{ LpyCwB - I7l[ֽCFI`EY K+@urf"֓K =ZIR@f&h6bNU hZJXhf%)lېEw@CQApcƒV(Chhv#7Yg<3ot,I!5)j$!<c-|S@|q-OJfzQ@@A7#()MFD![{bqE#!TԪT%5z%5[DJkdkN\Pj&Y^j獊RAAD:uV:]ՅY+@EEEt߾IpquEk!@BqH}8s.=H.۫}CǏ'I2<_=ypn^g]󳶱}vɤ\٘,vf4Dk9AjM$P l:KqtvO}1~+FNR,]+%v),B eS1<`jlTlɂj%VH%MSA'z=Y& e^(6*"@``f3&IR]J K>5[d8}Fmּ(fvQ}Qf$ ^G<*\Y55Trf0+l>+~7;*鏂@" W A7ݩ (TTD4PԪٻ "* BD@TCP "XV*.ǣsenw0,cŤ٪7gm6)qaW=o_j瓫^qIUUITPUtA{xPz/H"8 9#j~3 X-T u DiV˝O:jXĞdImiq ˹;l 5 V}$'6aR :5KkGut ,p>hKbܗѣGEرcYWQO#uQ5!d$,&+SoEC'~nNyо" ƉK!:[V#,ˆ<8)v|gS y) 2 %X?I$V(zs>µW7 l-ˇOMC]{ݴSk&|Tf[[$ ߰*ˇ4/:DZ0u@;bhؤ,B-..n[f1"fH"ި5mQ9n(3jԦ֕XHA{OD LTlF(j+ a#1lAJ :,A$!P"RU2*x D* J)! N#*A*DR ~lU%@Ar!C];w[(] ٤IպqsfCZB&HA T3<2"+hL(LDEUp?2KQt 9P*v S5InK@me0O~?j7LMKiFXKHM']X/ҬFyT:hڰ^@XeSe@,2D*DC4{EQ$!kekcYJJ7ڱP$ mRb.2i$&ot jK\1JV, dlIwXm6Ky yiD>Ef1[tOn3TV@pbL̕S[ X'bbJ"D"&Rj} )(jB+_ֽ|hVOrJ: O*TM$( UQive" 5UԐ$ؠ"1A،Aު6Ħ&5&$b`0(I-oyLw;( C^=UeI]׋ss 羉.Tg^ });;B=?~m>/MbPtzOt >XOGl}cȐVhgnp̖҃e2Eie}p|}5umZ-Is]Q60mAHm}[~ܼҷw˟]?xٱ}[(F{Ƒ>z_E^J$VC, breXrZ.-F-4ZܨJq=2QZ2^i=N(SLR Z`)8= ȕkmbAQђ!Æ8yZɓfS AI3舘9/2VlY dpt3p&d<VO:d2"l4nt^һzC%͹zkӚ hmZnlV"p4ikpA6[8#Ei{Lk @f"("4rU)6\%h2v%s-H)*3Z%{$xdh aJI+ [,5\&BTE6KW#fsQ2Z% ?^($a-S4Zȣh_k{uUXnnfR`הK%*E Z0R&`κwjA$V;LĴА2!*!V7pswtÖ%TD@jp˥V-nZ]:VI4K0{)Q @7$Tܞe3$L [V%$ *V%4uW_]mrg"**fx,WѾB j QP "Ӹ N 3VnADJAqbV*tqsf))zg I@ѲT55-A>>V|#Imy.M]^],[ɓG룲 D A4sډ/Kk5hE8Nc(L\E i?M? mĒՖо>觍tQ$PmHr' NJ3[)~"  Q2d5˞i^rΗ!6Y6N`sUSMeTTU=vlXt6 TH@kduh{tVͻbL^ n0hIHxä ))3Qcb \ S/2W\%c bSP *2I{եÏ娴'׺Nho/kG84S))K? 0){bueه,MjL`Rb>Nޡ>ُ瘱0wI7?m_J=s&FGCz2miӠ/``86Xozt{6mٰ1 VR'Dl 4m'6QԂJxbcbV(`q6Ee$nHN aS%^!$o5ՐH; $f:_YQo5{$Z}9?a1!x"bv4l***Ub (2YkFQo|22&Y&mbQ,lhVD̬y^x xUȑsns/Mw>@Lz> J*/sG a$FA@"XU@*2 ˄$@6TUQ*"x/ {QDLk<Q5AE@Pm;ȏ=rM\H"(xD<50e+%{eO%,wԻ.^ J="i15)0U'a 6 "J,{I(B! wʯߑ2)f9g>53{6lppjds!K|m?`l Yo\S?I &E%&+|fj)2ȃbs$h}Ak%޸rf? d&rbcI !Jh0s B8*VsÆ؊ 2]sG`hXY> GW^yܸ,|(:2s%4MKՒD !T@ڤUղ<8̃6tlVڬT *P**& USQ}9T5T*ԌXGJ-xi/m_9>S s|yY&3ɺ3=_xO0“qPaT暴ȕ I>~ WFR-Weef</*Se{D@f^M9-޲)Gk6Kf,vZAcMĵZfzZx/X޾rFCnI ;6b<Ra, A1!Qc`I6F"A;2IkШQ7j\ `2*$i6;P9Lqxj}~}{0GE&pB ‚Q-x0nF״v*9DfDE<=9hIBdBH`81Lư !"!"2":d6Ʉ>%7;r&O$ Lp"@LA򸫞v[k%TbXE [¹Nkz,sA%%RT ZŞ "P%m2J&h ec )3hkw߿2◾< |V"/D"rgfDDDk׮ǏݻZ+"֚-n-ښdł|Z ?{l[rнÉ7&b0D @R ҰDB]d-$lҖ-U˒tYlC%RQ @ qysOءZ{'U$ǫw:w޽W[gk=+O?1/答kkWFkjǬ"Ā=frecґ& VzCbvPl\}>hs[ϟ̕k9uw5>?>=ܛqOu.Gr R<Bwp` s`rx88mE*pσΏ%4hZr X'b#~Qak]`aS")@%."Q$IrD>%)6$pz9*i$e,Dc&u%E̼dG23bGFMUQ#3 mΪ`+͘BV12Mh<_vQ%D !PwDL1wdҡeЕ)$q]24qUUjc~ED1r;i(s2cHEAhҹG$S$3!Қ@d)v4 w][%|Zr8RFyBH@.Mnj dy-Pz; jGAQUU?d&n]V]6cQxD:]'ǶϼĠ>ŏdyc1hxO2cg%. Ԗ2+X[/fobU`5 a7ODrU5Y !ΚX/^ru!5Uàee aԼ.ּ1ʄ`sQLu~u5¡A]>Qp s]SMݒ]R>狈ډ+us; A=3c̎\ #"&0 2矟ϝ?S}Sd">g$ DP͜cr`]UYs^a3D9wA _u@aOn̩:$VP E`Dg8˜ 0{.Ā)q]E"f"'OL&矹8 DnnemN:uO|Dy4ngDn7(\>.mR~4 !5e??> @haed# vzPFQ?{߿髟ՔFv~[\t o_V-.ww{wm#_+?9؁J`5!{An /צgz17Z=ie4Ex2y(F@2 &9T#`m{ 86h`$"Vm`2jmFkT3SU )`W퇦uηOxDDBgf rj3SLd=Ms89 !9yZUwvwpȱgDLDJj&)Fc03Iɖy'DQ{inɖ ,u#܊i%͕QeyBCiӺ[:!CJЂ8[MŒlKyj9((" * BR6_K(ƃ"[j\VF|ڪX fsO]xg?/={wݷ1^ YsLŨ%mKt&KBi)yae`l Ċj Husi,e8O4!VʕU3 EڅYA~pJxID(KժG#Ha6֝YϪH[GRӬGΑ {FbtwPA cVʃ3k|xw[O?X|bhYȮ9;Y"r#c"/tU2!(>WIBӴgZooј&x{ZNsCWm"᠝O/0ǖV3m*6a yg?ЫJO.˵j us'Or W2Bءxjd<K*Ms,B,ztXydA)-A>/cLDd`+`'HK2 Rv&˭@XwGLۻ}^UGɑCN@&l :; 5*в 3Depk t: Kq0s QPAD)B@y\ɃY7{ً/̧K! 2GKT;mVDT\z@io;-… #T9"Yۿbt)J!^1vvf:֝(-A39'&3gȩW$W4JL,# LR(Y==vGJFJ){)˳x{w?}ѯzoz}׿/ݸrՋO렮a| GS'CF.a>t>>wS{:CHH Y#Sr녽i YD9O肶Fѐ V!BS__tlL:T[S?bh$9#LUT5D$H @3Y٭&zp[wUUUDbvL&]-);&c X'*DBkJH@b&$-^̃y巠ӖBeQH.F{b^"$D/%.`sR̒j)% I<;D6Tb&$IM ( ! (1`Dĝf a2z#Ub2nE`<[(YmJzcZԖ&QdHΗ>fskhe4p {g(>qN4缵9cbA; uIKM9?4w #_|~ss kz5h,nQ;"86V]08}s[oa!@omDz|m jl74'Rr+d8˽ABvm}Y RmWX(yo8M vIsi1;#fdY)%caH1fHa.>SpɀyQc#3#agԽOL:KZk+cGWfzFU "uˠݖM`vougy 77P3ΌfQ\r7A`'bID%i'(c&gp#_ B1`u4X]6Fm =wκڵA>zYڗ5_W;)ة;:| xoߌv{aY*S昂 Hr4ĕAYj'cj9Q!hFC1Źrø X,E2̯,/D^D'D޸uwUDUT"פ&b M0I )Ŏ@D@ s.%dAvΥXP5;Fxf !&0Dxɻ;Ȟ 0YOӦiRJ̜Rj۶Sd:XzfZ 9@-:g)F;U//r@'F;kM~cM)%%Ȣ (͒hQ] ,A'Ʌ$$ bhRR2@4dS]Vڢ)"{FVg|`w٩Hl#{:\0۫'m7>xv*&ww){#cn ln*45h.SA4X9s,{88Gj3\J{ݺ/5{%EF{ڍxuw5ɳ\.Pޏ}#d3V.vgO=H=Tv߻66c[6S`]Rl;Ξ4 (;N/Iۍ5NI,G;HɾM]L?z0:pY6 զ j}bȠ1cWNQ`^3!XL eI HHYϬT _^AM8iXW< aKd_u;n"h`EȐR#`Bv̎: <C\'L9?Cw lyAru-!\mf nBgN\\MmΟkYk]!JƣSYdT*i4K ѵWykW1? 9o8q#GVDzX5hff߱Rsey1Yc|wt._xfhD!N!ugPA8sQtl~W'4eЫ؀Ua.0L. 0d-j{zKjPUar0nNWWۛ|oN'7^YWW>=w>qd+fя^|  Ξ:wc7,Gj3?B,s9>/4rn ]xtf3W?Um6ݹ3O%̭xl]M6c<8&4w\JFԅvü,_n 0×M}TCSi8Hfjj SZ$Y1.q,b4 Pdƥ: u)-D1SQyDĖlKK%@']Dy#{&G7tIyt 30Օ^Ys.˲RUU`0I9f("1F&QUČ\QD_tGWmUI6m3De0V&*"ZtDں3X5 `ԊL@@悒ty hFfҥFș&5!@)FjqrPJ#U{F9:ԢMd\=Lg1/Rwoh}=UN]+cǫ(؆*EWFh|o*Kgn ZRy;{۝UC1hڥb ?sx}o=^GWW*%̌js*2m|#YI\W7Wtb\Bk&,d%v L QjCf6{6O(j@( 0.}^ϘgA2u:!xd"$"琐RDC?̲3Wcbӟ (I9$K@4D%52##.%͌qWq3͌`]*iS 6pp>//=m~w?]wnLR)MTuDꐴ;yT<J{ wxxoOxa::bȊS+B!x jۻ?O΃V%;s5z,AΝUdI&U420 UI{8wޯz{Ev''YӟTc8qS'O=7_W>ϰ{cogy_>x;:}F3'OUS_h$UHO\:{ѓ72Xm'6aekAW5UvQA C:Df@8˙z( ŦdWڗr ̔D!u뒒$I2Y!fX/f!6uJ303u Ks4СD_o֢ZNf}W^?J%L$2O?{מKHjbk֩pb5Lls}UKݟ $DH}+B 9~T+.6P'QZR}J}$h `a+L_FlkW.9y(ǏU8kW_@εs}]?A2<{:oDv$5 %DGpN[q@ \ZsJz$e|_k^.H ] h sÎ{痑1dNхUMY TY9cW13z;#"esp6~pooh<8>*.^6S"{ 1y̪*"Yu38@b4^ϙ]8~{$훦4PU䆦!)(sru h>7#Q{pZA@FPEgPHB()L33 IЩD15Jk@ՖÆ1vKb?"ҍ2h|d:d So#sn{Roc~ c&g7hR(c'^Q0SͫzbWHhY41DKn|[m: b5josM% XNR CN]ϼgT{hw~sQD({C齿ws=329>DvLĠDY.y@H،uWlk&CnC:Q(}Z g^BXxQóގE5tz]EdD!A(!x`5}q` w^",^Nhz}{`"b`BHČ֕+8F&rȀAa 1~/~=wjМue]lMK`eFԹ3* M -pY vZfP=x4V@tyj9lA_'բo~ˉxɵ[\mT\ hT뛫ޱ  :v9dDe^2un۟YQ, f5cfDT插eg4MC8]_3E"cPiRSU4clBs0m1 sY\o}n,RQ77+ [?_W_x?{gT4'2 '/_47r׮]M]U+{N/>py|~cyX)o<|kuat׉W*>yP_' ̺R) v$bip섰|u`$"(1T(%Nߞ)˲2[󀈼`px8g¤ǎl=Χ3ό[`qc}e<R98F@+2s"J)9:̼,("]Y!+"+{}vbY&"HJ)v.4M[Yx3 @*Ұr0뻇)̬!y# "@f`BH d wR*|m[+01195Ah2E6O&Fd`h(B?gNxl"QS%Eǎ`uut³s NӉ}pz޼l'%SrXA?gdwoaQ;6&(xYjvzl]`BJ(;=ЕprGdF@WpBX!m$ό}퉋?3[ ЩAoZYZ/[##ue_R.I& wZ54rZDQ %,~7D&Ďc#2!Տ~;v?9d"΄ *2 $#SDFî۽PoZG̐P :.2S&3~DdPaiY>ٝG~W*@z]ԇ77ç{&5$WGP0 D*)D IDAT&kG㬈C% HANOxoxu(1[^?/#hԻ_-f֜8w:i=ٞϮViF'.R 1I*2Z 1M1~1H|36fg|lô\4 =4Z8vo|`8{<{sp%G?29O|ྯ}+wu=P擟~'O??SM3;7v{F㞷}V5[eaAgf1ޑ̯RG6$i\R$b0t]N5E`۰W`!F :5@UUd'@ IZ] fh`].o18f3M,GRMIT,FKBL㱃8ҥbj΢;&v̸iGolX? )j |+{n_t*g3~nmgRU_wgY#bQ({Ύ8obs<s`$&Di#z1:ϝo4W_y*";Gg]e#z~7:~tz:IRݿ.{́iaҞjQOq$!%m3oZu ђ*"$͋U+- ubb$%&~S}:N`:?U0N`%˽,HJ9,MD9HW& L y/&L{׶:{.@m)ZB!fBc@e`B puuҥ@kk,: %#4A Xd$S8v B'Is&2.<z+giNps7Ggn5|b2Uօ֑+=?Cylc/g/ʠ}[Vu\VB f?}7XĘ;)iW^ ߞ=DU ۸s>/lY0^rѰmi! 76<3%sgօWh \9.E0C";׾r԰c 112vZy2)8vѸ 5hĂ`)F8GP1ph !*-n1V}1B $6)i1ibjʝcKW,:ƜCؐٱ1ˮ?Ѽ?e;#P@<B EBN ։fN Ɨ? fF l'>?]k>W~ɢnvP)S.EzƔ HPa1֏> ;sUfFj⟀{nwCsJ vt.cpeX]ݻ Mggk8cؗwԀ$:jBl.V(av]~KO=~Zč'i8Zx…NIN癧/}` p?S.^ IcD!/|?U˹{nVoåm^`.#DpDGӣbS7T<$e1 $#TP=8-aI-"%ofF&K)1k f\hĄ¾D9oΩ(gJdIL0s( D9Pd$ft'wK.5ϥ<E>T` fE`j2(#{(LO;T{qH"cTd(]p`HjJ`Eȥ^e:Qb4!jYɔ&%cUL(&*(IU4{UB Rk1 ld@ȌA hJIITVffmKj;ޱC 2"q= ):rBB"; Ε˧/#0:b@9LԖob@  L,GHlЊϴ@&ncfp 9 L`a͍< 75cx{ݿx!GBA p76S<#d0N ATQ(eDbƨwCnoN 6QTsQRb#;{B?s4Pj5{rDի7n݈ѷ}䉕<^~ՕU ϝ [073ϩ{}j_|{|o)|owY6NI;j,zIhw{w$3)1B vbpW_[`&m]8ط9gHNX =-Id>*bI'6]g{02e 䝩"!XLXN^E̫F[Yb m X+@繪:f aL)!7eQ0wԀEPiI@LhLOPRRTjuOik cb&tαBa\0BD;{w41֟ϭͳsH̎1dzDDd#lQibfu@0/e@F";L 3#y6z"!䚐\ZXZj޺|[3OcaSselλ"4 {' yݾ&2#$f[C7[d]0ʻn۾O d…YET?)e흗?o3w6n kplk7b1%H-3P ^0Ҹ<(]]wjuN5[??ܳOlS]Ue|+W.-w+xt8yE'ww6{*Ҍ\(.. DZi?r09G>N/ Do5v/~ǣ7I/6&R5>C= k $@^' 5~{LalfIUU'i"@̒@R"DD@FDtdfOy6Fy{);Q3Q>sU]!f޳#TS7pZNǻ}:xT@Ṯ^+ўYn䈝GQH (2\NDDj_DPc{fuXaf!dj-CJƐYЕz2#PcyyO}"@zqVK1ۓzuw.tZqq3E\LamM5EF 8Xi RKuΩ!:y1ʺ9)imZЅ! bH0&舌L'4 JW0dAMF Ȇ#"0GHȨ{H<`4o\quWVVZC1`ĀD Fآؑд :jiH  U[d=aA glm%0Έ Fpnpaƫ?mLӿo_KE$%0d3Gr ()"0h )Fsg"IAbb 6r=-\M&fﶻ70"ҊuYٟ/>S፫n;zWƯ8UPWiyiU;G툌O9sr\zg}qn\u}/.klH5kOy"E,,5^Cﱚ[rPa܋`:!~TRv3$Le &H"晐]mX丟Y,iMݬ\Ŗ_Ȅ_hVtHD Snrg CpK),nm*PdžQQǣceO. x,"ezW9n Og VS 7(hTS |cLF~ rxp-; ZC%sx`fh!'SkڈwZP0{Dj,C#_ؾ 1hBY$0!)"{H͘RfMRd9@IEHr b ((|yQ2úَ$}oU*S\^^+k9=s~_ҕ7_{).W5wݿ+AZ<):p_x'}hwۗ^󅵵wW[7/q^2~΂(:P1&6C~`po1:0^6<!{@ ԀuC !3Fł@akf&H$a ""b󹋃O}8 rO]_ʊ"*3`yAHKbQ31fBȈR(8Yds tLU0#FglN'fB yC!FBP #GY/=Ra:=W9p^}B@"bGڊfRZoT0C&d$jAHj IwDl Iр Հ&?/n8?~R#p%{0gĖC  &B;^Z\aRAf", 2ox6umwyƝgRJ83{F3cnf#zF4ܹ姷_c4)Z~uimmd4\y+ͺ"}*nlݧSU\?}_Qnols׮\G]||meQ_ꋦ|揎 LUg6`)673J2Hw0 {#g)E4EBQD˲S1IJ{S4#:LfOP3y"rpp*y޻qX,ˍpW=M[lb9WU"PdEY"֘̈RAĢ(Tu:"1&UVOm3]$LM!!%KDH yn<JG;}Huf*$D04"D\FԢ4"4I}Ƒp;њcojR5)]a-W;u-s2A5 䊲˯{߽_z_z9yrmi)i,iќ})cƬ͎K/|{㇝,˫tny܅u^H- BTȿۙs:PRwΞ2BJd  2Z"p`>Љ*8 A3bܛhn '{%*h`ȳ݋e`k2 T" Ad|2vJޒK dJY0ؚzphR ̪. " J kc`e c$AhF DQY)[a#j0!18#')Om>MЬXk?@G62OCg̭"1Y+VtjSE!$D$- `9eB5ֱ-W IDATȱLjȍj ?޻ǟЁC3VX͐Ϙ# pR5Jpme=:bD3B%".NBxn~n9@@KO)74eT&[߾< =+_}fs|™J_>LBĺ?v[{WhZXܺ}0:&ZjwRSc~3䟅ކkۻzӟ[7:ܝ.--/Lz++[_x>͍ﻰJ8gjiv8VWWn^ 4W#읾Z9z-q/TGJRg7}v>7w{y"D5fN-xdO^gSz\ άٕ2=[N,]ÈD-z;i "&⽏Mc+iTSPHh[nfm!@!(X=vn\q޼PMq[_tkڛd,V[:ɳo.gdz;w{sM鴊33%M!20:vm䍀)ƶgBio3Y;deBIf`CYQ D4i]:>>61 #$Ldfl·"䜪85J07bpD̓tk4OWZ ^;G$nh΃ļ75pu/}?|0~^޹܁LQ=;.o\[2;l|S?O= ĺ:Bpwg2awDYPU$!&2෷ܰ\4Jܛ9iaު,Q̲E^ٵGs^?|  xV`mx"Ӥ _+*ՃItdґ[􎠡Y3 I rIBHHHPّJJ3V^RK\R̄`=L< A >&)Zkkhjb87ef@$j]ڲS [#:m@}Zyu0n Wh)8 ~׾S~s V@f:c`pȁ! "P;  CVqfK gUVjLx7֑Ę8_7~gG?H6m85M3版 eFa~nn*@g߹OWSwen_ dYz qo3ݝݝ_H=0J 7ýzFq\j@%!9>qry^ڙS3"^>~➧h?ߊ6qͧrqtQÍ,;.]Aϟ>Q \Ad'KD-P@  ]kZfaNqyP>4,IdC酔bٵ6( f!DH3m)",4HY[=a>'f|~ g!Vd !+E^&!`>FB4 &!c@H*"3ug3S%f&ZE T6h;$gf0pƠL0æ@RK„8 1#$bRr>9$vmL TUӲqs:#tZYK׏Pԙh۷YJ-!BARg?ү|g8|C.iݩ ϻȨEY E y,9'N|ڍʉ [=svgLaaɠ|rᤑPsd0 j $`Y /v!q25aaǵ-RρGg(DQ#׻tf2Dr.#H!c4MgP`REU.oA 1&!g3"co4M$)(QV"t <1Fr$jdJdoo3wȐ2!9BD@ 2|}_{ @bF1;1!b"/;" "0CDvgc&B gwa+b&=pQfFQ #'h4~k/.(YnB+o( =w=2N>sjcsGVgvDcm$ *c> |/~ᅧrj$4s7^Y?""T eOr+7|r_^+Y#|*+득 HT5pH-ul5.퍽/=}8?saaO~~W;&ʉ>G_׶WNFӳݿ}+˃w8h{ݢ{B( fȃ^ԩkG ssgMM59"M^|[7;8Nr[M{\#r$945:n&j\f)yڤ'o@g~23E|6>3y'11#C`f30II 9GhY`cCmn-JŜxZa ss[cMI,Tq6xR5M\]_Dv-cͳx̡[GyA`w+r?R8Yt:j,MitΧڌ$AD)ѺL!Lu@b"b*]I1|5v;.ׄj4pT1/N;=?&6[k'o~$ :pk|y)k6L:L WES$4fF)'e0wluEDHT )'@mڸs@ &`fhqȡhBb$HЈ ~M@jyܔ 0 $`@2< 8uQ0ccbdΡ2SOX_׆GDCᄌj6q)LFFD gE#S fp$0*cDDwhjl0Ss;\ށπC.b?s7>twEQR< B P Փ!"HHFHJ5V'O">EEPCG(L-NXG "4{rKn_S hhys/zavBA/N-vHx2Fє$:Yl&u<޾}q`=UG/>O?uS胏zw`p훯ӸwO|byqK__}b 2~X`{> 0=Wzt{h)M)xp G=݆⽛ܭ&vVQy5h{ABrX"ڪ .s[*yfyM&12BLIB"; }鳝s?{ϕ*t4Ʊ$T]v:YV[ykk̙ba/q2#Fi]e;6a2o6+f_nM#";|$4)+vG<.`&#>`EiʫzھYY@}5F`JUQh 3sB4E4(|q&6G%x"KJAmL޻^&1Ac*r\"&UIh | Y- w@LM42":vn&< ao3&"b֥qkdȑ1 GZkZ|g hA Q$20fV5j1:ܼ~e 7o AY{srXaLq:M]q}TǪ'?YWTJ?#ٝϟ]<گUy2>>؇*j M]H옼$!LDf:{ZR "yĘRe7 81EKS !h*T7LV\EEie8QFy,/;`b⪱&F4JJ@V F>I`̦Nod8}ī:wA杣&E7.8{N +_~}t|jRw[[9Ŕ˫*Tz乎2S._7+x@! yR" T W0 k_+şƷٰL's!X4o޸^yT[] ⨖iRd6H 4K67pHTIՐ9#ȠT"s(}SGbF$1TfZ$6CHA3L"<#hq̌.ZZUDC@@bj۾9J8bö`HЛF:'z4zWT5DjDhF1nHlfZXLwF ئgDNdFmM `Ĕ/=O?ٌ\pӺpI UZI30@##bB!#8 <)Yt^qjT$!cloٝ4~TAӽLA~Ywnѕ(ɴh7>Aiw˗^|{gA`!ScL}I왘ƣQpRsA땅ȹ<'0uY|'>CNcۛ/OkIt2MBHdMߴ5"0Tdfd&T45UQU3Σ)*bĖ"<!H6owrai~yy׃|L&cPuq9urU Gݥ~Y-ԩ[G7_nMFM5I'O-vo^9u=2jj˗SgGGy>;iT%#Rk434?BhkoTe߂DUDs ;ioD-;sH&#{^z"ԩiCFwyEʙ23/BQ  'QxT56ŔU@̇P1gm#XY[upueƦ>wvfhfgN,`pp0uܼf49DAZ :`^+l(AI3"$v x;PäȄ^2#ϛ!+y5r1Izwv%T63:3$]v K㩙EC>gzDL:~3 XK:E"C64V ی3?yguwι{ `!Vb# ),QHv,ˑIv9$VdDZ%+eũ(f\*9ڨHJ)p7̾{{PȕDbͭ73}{{={>_!$ٵfRFTX^s?mgahfv IW'cZM&0!Rf(ǡ ly_bW C𩘥G>„^_{A1noQ3h}V*Ebe_:4F/KRN.;;&HEQqf YYCuEn@;W5k.~3xwy m{}+{/or'!dFB3j i%R ) x""BrdxLFTNDpT*Z.6wunD,Sj5 =pjŹ,>FD`#EEaNZO{YnOlOwm_A~CwNTC%e;صm*vP1Immou뭷|3_^^:xpkk N6iְi#RYVHў ͚k8p`n;~2ޝn,ϟ|.]=+v"4=~s+cq.->;)GN'Tii 8/D1 E,0yy# b[CDo#_p4J"ZȁPi״Ce+'2Z5U(@ŐVD`BJ* $iQb8(l%KAa}_xG\L1櫔P9u43zU`ܠwzqT;t*pH !6e۞r$j}3_zSVkrj_G-|ۭ/~g74ۊͤ^hlH;~p؞%q$T36;N_$IM$X\.oo?ҕ ƚf\(>E+)fkP䁕wIF>%P̔&|67w;#ccjQ;ۘjϺJ5fL|]"zb" $ W-pBz0.magXu'_("M DWDݱ͍zO}33 _h%L A/MbX̠0ͭ+*ikOvkcf-aV뇬QXoXMd,gzfanabbg,]hXK筱G)FTJ)SQ)+L#2JmbzxI(B.Jc,xR9hcYHaFPulP ViE·H'bT$KW7 F6P lP *ۮ.M,-rbj߳Z5+9oYW?Fk+O|'LMeLhc wFtǴ[pj|B+5K(Y{C" iuK]sY칰IAAtT{u{u7k,mcF P &^ΎuNOt ,{p \(}pG `c21綎!;1G/mtC b`C_Z(pH2{ >Bb.9a@u #$5 `^L EL H AP &yo7׿MTUP9 bzmx&{:DR 1"%D I U$GD H`/j*!R MU @D~jls?S3S4 "C\__"%*caF A(V+mH{i?vĂTQ1j=S6)>77~-Z\Sxz ^ o1oםSq8QEU4C\fA!^=4qWW\Ʀ;WrNw}yww/>f7uliN |C.8ן_H[緿灉/[G/*c=:1=sO|WnUzǍ(}𬔚BK4h~Ƙ+N&<>O2&l}] P\T͒h RV"eALC%ݢE nfgm^?L!D4ڡ-{I%` AHsha$PJ +^j7D .f66\[.^tıQ(?<ь "ZAɘɱ}`Q <;a>@)pcҬUC ^i_{Vmm5>3?%;|WNLE."ޕE1yQh$( `UT=#_:,DDk$"$9YEz=#qjy"'Hy$c9)F4&a( - 2"뵚QVgrށE-nnkWO9;=m\[#][jbtyģݻ{>S׋"O:&buDDEb#+{W {n vnm= %8(F9jRC)B)Rh{T lN'TbLe$H@ `( R\K~m$ڵGy#rLvyjFPZ+@QK+k9K cVXRb@QvCV^X) :#Tb(%d S!*$4 n+2*Jo#uܥB,~;]a!pH **! Y@4@0:65) Hg?+l'n;\W2*2tcbS·WzTFo0/ Ĺg?l4KMpjxtazͶ'8@(+e(7`P Fd yWyLylk0ik8dy6?5=p!4jl4Z/99|;ԩp0֝")TDQF!! 6F"Eq$"ι1w!"J/bo-\(Pz(<-dtAT5U2 H3{̻%2R| 0+2i-?䳗Wf>m6;Ѩmnopl /i4>>|ɽ5p(ϊ,Z eʳ/ǵ"o=XatOK?}?:F>ZJzNܘ~ ϗh9@V^=jx V6fւAv/]:s=wݜ3f zQZn!/|'V7.܁;ۨG͉sO|X>`?K}>9Mfx-L7݉rɅKQkt11U$ņ۝P?0kIgt,͐(a^$ڡPaA zGA˜54he @!5alʍFITU g^A^W+L=*̞) 03{cϘ_֕<xXe.ˏ陧\XXXJި7jWv""m vg7;j675qnuiu5YYNdԓOov[Wy K;1(mj.ljM_^߼xXnL\]Z', FAWS@T(6;̂h( ntEYr"a7\ |(I*2l4A2&ÈXT` 3n@eB́EX( \YFFxfVvF<+wthgҕގ1o.(ӗV<gW7kJzp}GMJƬ(Bp('AQSY̍-L~S秇;N'! NTfv;-_f &_[^k/Cp,HJW/~ӛ8" bF+O@>`ҠX{('ޗccUQo?b?[8`Y`bjP^}T?'0e=B@L%"bM{Vpk݈R[wz}.P>ZRߙȚ8rԆEXSȟLlC /!nj E&FTtP&//gxM8dKAȥ,JTdm#Njf/G iWE^E("T!N= zVdOUDPxU@d]r@I%ާ Bŝ T%Ǟ}QG! LuIO 1R(%/2"כZYQ.?o~7~ws;o&旻[}S-7ܘ~e=w؍(no7բkM/SsJ;p:/YLWtԝg[ͩW^^2v<.9hylY][W|ml5MąaEmkz3@ {\D` uM$fcvmsCGҵ2Vp,Z &ʓO ڜpޠE̖C06 RHVB$ 29q=Sήo$"?_ATVDhXB@=(P #3# 03p`)bӘVa<19Bz&l`Z`0JAfEZ/&-vF,Ř zCgԙm]}̢sGaښ|גa^ N{鋤ַV>XsrccʻBcFKZL 8YQeI"3̃ɪpehJSX(W DƂINe`Y""s`@!gcTΕz3=(C>r4ޙp:+W[N:HW{v-yō}G}/o+~C;/-&uCZ UjJkm 5X$L3Xj޻<ُ~[~ŗ==u noX#&EH:iZ[䛩(K^|QӢW7ylPJSKǧaO~?S_F]:φvS'ȇ>oX\'{~;{_8헖ξ왕3gǎܦ-P4dWY!D*rRH;L_p} 9&( P*@"^ْ N6saeqQ&YND1똃N"5lT@6@dohTo0T< 󂄁XǪ֮E5딳SKeͯDk[1[sK07>zdNLa $!JVOzT%U!! Tb ׅLxX/TV ATC{ڂ!@%tӝ4!fR R퐪"RJ Quڻw?_8.b.$|cזD_B"\pc7/ܕR90 bY{oVg{oq{(F;\-n=vl )xF  iU9͉o)%^9wWxnmy=wܞ";{q{e==!Si}FҜ{}wۤ~#qdHk 7PTUNcchM"2DqVi·v +K;5F'f׶܋/-VATDhCc3`b;Ѩ?@TF'ZÍ2O;I6oC>9V.c2lҊk[=Ҧƺz܉'_8W:#'(teiexͷ ERz@iT,ʲtZ+Q@g)wէ@sx4{w5"{Ӫ`nmV,* 1`! yAQY$4!R$= a`@Tu?Xvno88 /]Z\v~~l7!N33 iNNN,][^MkFuyu)Q?9qdZere[I xmskquDz@~!Zӗ.Xvy D$"kH3ajj "$dSp@Mm4D {qbzH~G~ډ)]yfo]pzaLykhYS믞IQl$RKFﺻ6O~uikZ};w6i}oU|) ^"Of䋗V@nO;>ˢKKcZt{^T\S+CRd+fjID-D ,nzS(RqE[9(03kQaCEA2u%b˃u& pQP@eD(vWs}ɉ(}*;$B$ A"@ UURDJb9V~OP?U\(fN @}Iٯ=O wu8"BQY $$$p`f@Q) yџ>Ȭ('ɿǞ?C)D0_jȶ,M@"\pc7jNB7)?PN)A((Hf "LY !piTA0ɃcgXxb. ={X0pƻ g4yR+ %XvM9 IDATo;jFE"\nXa2/roc}x~[ѱPb4~-/QJ@mQ8m8|\?HXHS#b*x׌1Rl(VIW4BRQ1ZʇJ@1@ @]A haPKBj'y"FiTFA(Ň~;>qӧlnn ˰V|~a|vv^q&:Av兓jpp{WsO<\GVwvE~{/e*K4n:ҭY(pe-GL싧l) ;r! H\aYIJ֊/8S )$/ P܊ zy>̞l3VkEY!={"(BeT} -T, I4 Er"!~)7hÜYFXZ׷;zC|+#q'"GN|da~]? <0mw"!8F*16ϮiFxqq;4;QK/@sӉwF98tls葲"o:ǎ|E }ZB| vRH/lM^g[/ڼDg=4+g?_Ḧ́2ܣ mX>Nh>u_/^~nS'ԁRe!R ,Il9F-<` Qi"l9K`BP @aPXƶ`y!t(5m r;<fEQB1j>*C* ,Gї||-{P*B]\p#E gϖ cGg+JtUS@X٤r%AD <(^X\-A$Yw1NtgT;$͚,1h7[֋KKKW//^ 3t^"ePR 7ڑ ֍ig3k/~ӪۜiQKئ;1]XYS;Ekg/-.aZAsbo.oMܮuWN_M  F6,|í^pKLW1NA3k9APȀkDpwҰX,zlQ/A#8ɑdTDzb(C@Qppd+Q)U޻X֖Eh=:#TJa+'"$D"f^Q?(BZbj$)qYx\g B@H[ @`N D@ !:q ?/ǿ6sd`Uq=+~<0GQLtXׅ@vʯDŊ7WU0TL_ޗ'_tO|kQLAQɍqcoX*S$a4MԪ=Bu ަq`fii拢jOtVYfpֻ䎩#'g>vL(b{ `id/מ^V+l(♉Ƶ0MN +9%ӓt}~ˈ0\Q+}C;jF"㴏 ;7"!e(^ +.*[Kӛ TΌ2&bҚ"fIE\^FkBM"iԿկrF٥As,! LOk^2ӷuR>\8u]Do~VW6{A(Nc͋lDt/C!aVy#*";d,C;RbuO{^onPk͊#F@f/(}(EXI&0@ "2ϝsV'EVxN6pyiy?z{~N>#4ըdeorݱZ8=L{!K}Khl7t04Jmo[dNV?B (ښgYpv-?OdA(@} P@q>+!xEJypLҳe=dW.:U**c$!PJQ (Dʵkm[( kLnazps}nn^)CVo4lBzAw+7(o$Z[ySΝsͷi?p`.pΑc]xٴJh4v$E;62o&+/e@85" {_,Zkb#xa% @PU5]EPW~-A20( @!p,B . %'"e#eΜYYYGz}*"Q 8ݬv۝veY/a_d]p 槧F`J WFF K/>TN=5HM#cEt3z“M7wfMJH` WQۍ{okٕgok`E&M<- YlY  d@D'erۑbɈeKnZjl"Y{W8t+IUS}ι{ >RI$84AsZ3-3S՝o,ƒs+{4|WI^rӒzgoK>--/EYeUhx.ܘR8@@x /^Pysa /n|خ>=lX`FeG .n6 hIZ2VvWV rEJǥ &MyMdW.$ylIcZ[W9FYc{EMJ0*D.3LɅH ޑ "ETFVR˫ nyP m7j, JPuBBf(8. !@NK-iFpUEJ捷}_W* 4n5PQt,,.O}bqZLzI(Hf0Xǥgd۽;qӦ;);ټ},6Jʅ.IJGhov;.'xVeRN l3a^2-| ߝ?̧W7@+7鹸4A9ʬ,",bYJ'jk^;D:N>sϙ7޾x̩vRk҄=}jb|=\9C;>s o:XZ?9)x1A2uֻ.3fb1W\MUOƒ:,:vTvZLr#0:4;k_٦qs1ZYHY03@Cb 8%`!/XA`oE~:WޘZ#}%zhwISgmdTPX 5WSg3 XmDA`u"@RL -{c&dT#0 ԍ g* (RHHb8ψt=pfP"""\j}һ4 n_4 W]?ؼu-0[GHY%766֓~d&^y+G;4ח;<=Z0PH`"[v6/u's k O,)8|S-?99+ fwXÕ䒨L)}П +iBWY6\^$6M'РGUAF)ER{+m<wD djG(xj,Z;Vf@NH D{`;f{?t0E^rUu*䕷\gڟ46V@ZTJ $I2[{\YN5;s7onwʐݸq/=8Pe5`[Z\\\LPsQVJ"XaJ.{OeLuBwG & 8zL}6Ȓ/(&Md{NRl F╫G`ϝ{( : RF&GVӋdɩ[[[3s`4γ>{zuhOI#f'~d?ǣ, ^ s[EYm5s@zwǞ:u}{w2IWN*eP哧R$+*DA,Yqu0X9}R)BJJ'7]n~0)&1bm+Dy&A /mWur##+jko\|;3\)V[/"F9-JϾ0t*ܜq LJ_D?~cXs vzcrrK*'/<>8:ՕONYӝAQP"bCႁgTa$a5av6wʳ(`NGCHoӢgutFU "h4"9jL"`3u 8XǴ̄:<r|jc1h"iefn㕋nwZqQ<ǝ!N¸fVg78|Y]x+mYx K٥kćt7Z.E-n?nߘfںi΁653ʵ[ B(4)2%{xFe̴+֑u CVޱwRJ_ce x@Vؗ9#c99`s[F$(F4 h{'.j39szV2s۟_^82/WbȳJ2+^$o ȢGk IDATP:A "@XhQ 0Rc"JJFPa!L8&8~Ν284" a˓ieX"@v&[BcM!$(n:Qqm*$PcI-J 7J~흵~\:nÆcҚMx5Ѭ:FĀI47 e> C Tk<'!F 1. ia^Bc϶ B٧Z$^4/;|1]hRa0  R+{@5Rb К@XFlo#S? ᥝzፍk?Q6'µ[lg&ڝDGV[Mr4(eYEZ?Yz'||2I,=hAsu^DF,"γRS0;gV5{}__E<\Y%XdYh$ȖʫnܾyƠ?2wO\+P"v "F(KQI O8'eYj`)Ts~[26N\:q̅~=?,@HR AOPhW2G-%jnq M{fֲ(j& |ɺno=yU@v;xyP` 7HCJQRӀmM\Î12#c;܋@Evhp2NTmLCwsL`0 Me~6 R{{lj~fkcW7%tksךah;ѓ@5jq6 WZ֦* ւQQ[m'"NiQPs4CVG⊍6Ah˒՝(BFSkYik9i|PjT(P;Aid5i   (>kcbT5V9xPXGBPEjJj{=eiPS{otglye 01% d>I$ǭ[`jO[qhu'kpT?EVCshBKkkE?8&lb`Qx(n6y&{isJW0 $@˅u Rġ8 tN˔2Sl-Ylɔzϖ2aXHm04i`ah\ I-VB7;sΓuڇzfh͙^9O?tw{Q0^Vfݸl/9DH:LUhHW^,)"V@+{/;jf=gE{[λ="eH;uEtu;Ҫ,}^~4:n@f!d2*VvI?Y馣j'90DDXP'<<[׋+*Teq; BsnX_\ZxΜ>}sy~f胪]Z&3*AokmIA)%)m+;rkW޾G IiE;9~GZ EqA(}E FxO*'U0<+dy8S:A2בP#ƆD(% ds'\\U8JM(.XƨN*{}m_w{/\xsnqvubmS mga¶º;_:q c- x&К Ia=$A`@yE @*8""j2`8q{i8$E@ǾH"B jiyHF>Uݙii2I X0~^H<") h=T " "23!H-|EXj*/9@yUhX{f  Af;v/ Ibr dWͅ%${773ZDD3wP;_O~@'_ w{w?-y;;6mڢTPsZWQ?l'ѭ+hfgg92~wk+(4lt"h+$a|ae.n4z|ln2Ǔ(=.9A&pl Fqw\#Z #YP<EF0xʐ+ N,U$C%Vj{F縲sUQ T1gb3Wv2-׹#|Zj g&$DEq8U%Y@!iR $̶pF͛@'-]lEQ xΝusfxVsm]pynv{etf^y駟&GKWno>{#{xr{k'6E)]#G_VdG}et~#A56Y"cE:ϋf*c(Ϊ|g(~曗|SO#[޳윳,x$+g7?t}3t^ws?zyn>.w6o߲j[7WUFA/-.i¬t~~t 8ǩIi*.\|GNB)}zyBϭTB/ :iʗ3(sP.3A(*g+Cdp¥gIbt)(Lt׈QLRfg\on|g_ɟ}wG~?W-c,xh+7nAsjTQ(E҂)=yi^P#F$Ruٍ@ME;nDBĀ+aD;?rn<`0# x8cLr zJE!GT(NI Ba!9,("J4uY&8iCeĆsڵ'Ϲ1^Q}08@-@Y:R9(9y㟹Ӿaf+u.:pG YML(߶oջRˇ,-/i cuUUO~2r;D@, `wbC<ؿblsc;uGAʋŷ.^e( lXτq0:$k(A|>Mt Ag.GANWZ:>8 LL*Yàj/97'@XDF-$DTd)^L}Da!4LeiF331gȐ4 [= Q󾪬cE Qʃg_yQ ֵ|˞BJp:1 C`a$Bj}ş}ɇ_|MD3}kwueiqifw/Oݾ+I^}=9:}:{DɸƯ^?Dri57?wѰJhpq-bfPF&4F9oсQ\"cλ[plfF@""0 Ff*Ab.<8m[dB ?8~aYz ?Wn4--TE> ,Q:\Z^Y5,G1f^Oyi+0)sI׮qZ%қo5[^xvn~AcgK Ͻګf260 f҈ $ p4FӢ2q#c@Fs4:ь@FwnU߳_7DRDB`QB[ Mqd\Ɍ!(t:q(*%n޷`&/4I(H<a`ycHD"jj9yXFtuOs/5mysWoCs-|~o!\̛ބGET`wXO޸~}v6;:<4>;/jY^\;>_r'8֭hvYvwFE!jq5VIhz7O\A-..a# Qe닋d)xa &psH!.]hvXM3Emhfݰa$ндVˊʕ'? 6ьɘ#:l'3j9QA Bg^-;`wXLrq1!]U(EJiYDVwjNb{DQuD< i_w`a﵄{WEF=R )RDLv8cºwY 4j# #xej@kR#ޙj¢,  !G>E$@Ab@tiQ'#EAH*" ۿO| P;N "aiޮzgZޣ" _V ^;Y$q{cLTtID|ҍe/T'Y [~k{`{tc/,-*Ptbi4JB1BD:cG3I@xjPc :".KuTCM` "gē,YT ²-xR@(BJ4 j: + @L&0T K&j_C<qJ:;EiJֺvךw:QJƍ /\zYVfhm5{ +'z/ T.%ʉ0{[;<~擇˗WǣA={ͷXʅ٤ku:"\멧<:: W\#s^z`ue87?g㥕_Kotf[u5cVJcD,KF+" zʞu }wHuIIiR&Lꪪa+ia0v4$Pht?333WNĉS<*KdAv^ XDɳL, Fqfyg(ml*s{G7$,|u{ݸa#=dY/Hs@8,– guNm,㣣݃Qܳ/dqD5Xe's#4AlD*Hf7VVwF7ll02Xe#p!hr;YbXD`ݙd:md'%']5Z*£riGG&pTOCg<4DB:M׽0ھ>8Xi4 jfVㆥfZ.9Y÷{zxma}e_ï btԵr( *cDWF۸my*aai37="Q|-qxƠ0(0' XMu[%(&A/A+.QoȁM^kc-+25AF@KIJH᪙ HcL"8?G.]z{opJZܽyP `~v~so[X\vYQX]ҵkW+_`qna\+oF+x꣟ 7_̯%,,mڳL IDATy:'=сecԛgG" 9&ߣ]|jWN~O;޻j8w&=1u$GT`7߭2 ``j|ը;N&y^Ye>*lE^hي%԰?LWgo}Z X!iE("Q[޳(EJqVmULF بE(6sDy,GIȕ A`He&pQ_Zq0ՉmHBBOcuQ {PyvZ?&vnj "jv!*m(І}VQEQͤG}d{{{eem ҍ{|so}osul<Nsшў]$^*mL Dߞi7IF;wgy0 Iy[ }76ooA! xt@Ǿ.[mYY[j":h0nw~tUu0 #g8-wQLl>xW_$UlZiiquqyUi|O'iUY ²AzPU zB!.}x?k*4* =|֧DDlUhϲ 7r~GzZaA˧ PR ҜY2$p0E au2!0f4M (|dޜ\_\jPED5aTDA\dX dCTƊpx,A #鄥02dl!l9YTHDc D*VF K;&PBD$A 2"JfcF2ʢƄ ~QV@$j IzדPuHCjQhE '7?pei;wwKp\,-t~0EqpAoʫlFGfYdin I߼yO?_ΉcdžÓ'4k߸5]_Y>QV IDD~SjkO^YAIEE*9r(0Uҕ389}o?TȐDQ<`l'㽽doZB8vWgɤӬ {KA>/AÃ#'N Eւ?>TM lވ&v)0c!bs`dFaT3?f`YY.~ョ aGxvȢt]l7Y=k Ԗ'\6\V%'{#/N/+7ޗɠԹ3L&h{F^8ܝgP+/s2f>ⲸG|LbjՐo{  jd#f5 zQe&D]~ j"D =!&8 #lB 0sbRr>R1PC# *r!%A@ A@3/:Q4B@;BĬ9*V^HT )@%,Z-J=?I<yU/'I>"ъ'*,ؓ " `w`hc%wSGK|oiUf*"Sڭ"Rac {Zv*,\Ș4mv{ɤ,4(IYV%B(ԢUf缉E` Xo]]jDqV:h EBW($/&P@APkԏ $ kW Asnm]-jL )k6),hP6MPN*V B{q^*o#P1jEA@ha*| "YE Tj h a%y2micXEX*cL:u[#GVq8˽>ỷ?7Jg Ӎy QqzRV\yspg_v?ӗ#G@5λt;p?6FZkڐnɸXdAϴ[8p+x;r75O[n >}I?Q W'p۱Bq55 t1lm 7I AdiXa7p2ɋqQ ]VKX[ Y@ D4QdHP*:̶ &D2`uUDi#` P *gJ*Xd)̟@!( DT)+W Z;ԁ%O0ObSpHxQaBATXr@W<xa?^:ޏxUUUsssf̃@>C7ă#`)/4a`llt8>>ؼw{:.we%$)(ԢȨi-&W*>r·:4퓁%5a" FԁLjT-`-pi}ʍKܜQܿ:[(#k4ȤA,lJLN 54ǑX%i Cۊ2VȢc e"Vmb=1Y? TA#U-(FY52DD">X+"6 z# ˆũ&>Lɉ'nZnZs ,.9luqG!5W Э--N OE݃JR8!Dr민tc۝`A/[_8޿s{+zb}qm>Md.t#cp7jQE1;Κ:Kƒ!cBUqN*/PfpHFfG>jFv:,{;7nyg?& P!i9٧.-k[7n @%Z.1ŒI6ݽݥ%_8_ T33SE(stp8dzH<4xԙꏓ^A?qA\z}3.-Ko~Ɔ%݆0: ;Go] CΧ@군^kǭ6Z݄ X!?X"A! ¾&c_tr^{+W-Dх [E!Br0'Znys19rtT35=Z[̈́ވ?||ᗞ7o?q1ֱ2NaŘӴxO ȴ4Uϝg?-/4㳟P'p['NW;8s; ^|SSOujӁn(pˊUbYh4@w+|sYC#$"L`Rx2+rc#A`B(Zl5H@T?^lh=K @ $bĻ AP q# #dUȨ"TIBSY*vuPzy좺R xHi*C-%nšG,AfBRQAE@4: 8 jYη.>1x8]ZB\{k_{~f8,@“@±aI he9e @eoe?c2Ae1Ay YB,n %#@Xk@/,@dAupSQNLȚ4iQ.vOOypc.@_A [_oGG~h-u>^O>Ov>]LV:ݹNz=d:}|5M0Ηړ@wN?Zh4mԂ%ӭZ=8zȠ?@Nkw_[Fi̊Ȏ02AYQ9jXPIJ sg?{FqfQ?sUGzwpti2)RE( f]\;}晍#O<E񕫯)Kg9{E &\%d2Z^Z8r# i1U'.=) (X]X&n[kGdnݨ ÃQdW~闾W_4FY^䅱\?W._^_[΍她\Sef-A e%/pk_i4urٵ&ܥvW': h_[ _/8?__O>OV6^4NS M>q'&9puY-.w9BSba-m3jYYz민-:b=d޿~K/.QS˗zw7or7n+ k禽0ܼ'A|dBtx7YHP7xo `5DAd9 c=‰eX FTVs 0sEq^"sx@BD%a1htfϡiR {qT(BF#"B޹yp҇yiGQLbتd,\loIak7_~)B7fUTc{7d|K_'z=<|xxp4j,O?TToGi`76v0lg̙IQ>|3(ǣ*A7#?*p7AȌi62N׏<}aX,Nvz=)#@ُLRatVE\v6N_7q΁؛Z aVs5ںש:`=k6ӥvL뷦w6IQ$2QA)p#vsksB7C= [$RhЪk++1@+2J jhĝ8 -Blƶb32ݘ"D s?[JeT HB EYD8 BR\I@*3$@BbV("UbvW%J RwrqZGQ0 Q#YFB` ː* kP/w׷Y^8uTwV㵵kϟ ;4 A<" 8VJ$ҷovA x|D:*s*4Ȍ>ߕ@ZPUPHTы+mS IDAT"8ݩI0y{DeNo8e@9& @P8aL bH Z1`5BhT$B`c<`Aڹ ,=؇46-B؈s!GJZToV\"JɳJj- E`2C. Qa^B1a72S,3IO"0X{`Q@YQ@,˝Rgonj`Q,&Yz{bg~i)\il1QEM86ߩaus?oxG<=:/qs܉c+oqVm2EqN}y*#+\s~իq'Q`6 km`EPٹPc "1peG%Rq+]@d7.J\$Qŋш1`dݭ=fḞݭ<HVoLDy)A0`"zy0ssv{Nή( T ̦?;I\SoNXk(sp{ji ?[ܽ{rem0 B@kG7N淿^`x3WBX];'VN}+99כSe /IwGi$[Ogn_ykhVy1 Յqv[uSKn(_,^o? bة҅~Go5ùQT~Oh^;Ʌ k'Fiւ,K>ֈ&eδxa%"+F7ꊍ3w_zSKtkIY:f vm߼/esOp{oQ㩵L6+^ͺLz-CimBc͐0(d$d2 CА0@,H HʾqECOR X*4gRDD,*DQfh#U`bU(AUTQ@HPAQ+F("xBSE]r)U$[UdDFe"a V͢JXAݝL 2S:1qbi6^ĩҒwi%PAEujDUwE~/n~3#ÏOEG^aG^~}<0hǑ%")1Xӽ 6RbPK` 0!@ 8 QR@, 0@ByAlRsZ(8PT –Z#٬wHy/Mi2GѬϷuM&߹}وoy Tl6Y#$P@2kQ8Z,FGXIT`^ P5@O`}nĬZ&  5Ta09j볆X <}ⳗ.O?_~zm54 ~W_Wnrpp&ciZmMNQ3qܹH|N>< neA!qVk q{Y?q1yi? 'i>нG~˗ok1HO6T$ޫzU/T $`k{ -UU&\batőcg?‹zw}Fq;8)$a)%&i,MRʋLaNfxTFqn`i%v^?+R6@ofsQYl0 i2 h4ܺ&6&LkQw{i:')'Y!z>GpKw6#3dsݣm?ݙ wGť͢Lpa0Eaj'~<)({C$}EP(X NzѾku:۟zk)NkQ_춎tFT Q50]]_nm?Oo]>7hq1 虵)d 6;VhYÕf~ ͣbhHk8BEPYjh ERzĒYHU=ZCJZg:w P퟾,09e֋ Pb ewfVQEQB""\%㆐WPT "Zy"R3w`UbPVRDcV`H"WQDHgJ8+WupRPTÀi}9vdS{,{;^79˧X"^A cJZB YT|l=֝# wAI% 9QG?~>|g9zED3+]U\~BKEd&p7{xr0Ms Dz$ ,cHyqhM˩/T[AE!PabT4Jyΐ Fwv}YѴ:ܟ΁7 K깥QƝXi/A|(V!P@$h"2ÏV*TP@0g3ML`Iмѷch"1iV7ff!DA/6вkZ$x  _gvvԭ.QUNͭ~&/Yh9w#t2A\ s##;$Ӡ:S>).Ir߼vkwo/NWm5zmgoW|z RomonݍdR_Kg/|SB(X[]jw;+06LSgŽ߈z r R("sALG{s~?&2>l7ӏ?ϋddŕVjA~&اO,f_ci{يB_ZȗgY^xUkP;v=Oݼ'NCTQ+Cd@TzfdCTUS#,V$@}χG YW{c@83:'ց֣ P6H*JE3(*(ZXUTY4) TL|%=j_‹~?^[ Fmnmn".~ꩳ;o:{W_=Om-to[X<;nÆag<'E9s/}գO;{ՕeKQ۷vj{3k,OܼvE~֝;@a\2h{/d YK,Z+RP0 +W!GF"?[{}?ōd6J<sݥ,{p,KKulT\aQIZ*eNA3vmd`TTIZDHFl#'h}Y괷Ԯzg'+u٬0Ҳ46wʇ[{B[Vdln駞{{s޸(֩ı$i+*,s cڢ4ֈzwa,}ozfe,F\wYbP]\vqu8IKKoVVn#;~ԟAkʓ`xӌL̜xm5A;̳<3y.2j`U$z\d,47BԗZV"q)(Ki`+Jd$wŠNg^8,y|;矑o|Ϋ/D߱Ɓ!CuS-^,YkҫZm8VW/P 33%jK|yTp >W,;/ޓ***3Y64N ]T ,DQf T0HdT gvDT%dRU*JT/(4#b! VEs2`;W k_@HQd)I!{^Ug:1^{c]]֦D/lxbPVTegɯ?[D O~[[ޚigr|E>y4M |/>^8W %TR`yT&p}'pf/@,!=0CfP K^W1r23q^$>Y9w3q`Yo:IE=m ?5_՚i6vyYEQ2REBRbHdHB -92"A%(Q^BKT@}UwTJcQGZ!A`UЄEA'ݾKt4NBv~iԉ+݃{aD# ʵO>y}Wҩk6sNoc}qi}:4({: ޹2qPwnL,w{\Y[ʵ#__ivsٟwZzqO}+^s#E*,dDPrL| * ?Ivp4O͵N67!G%0WZDfqz{_'dЪ0Dc.V%5ƨ Q B}H!pGJhq#{}Gq++x`MCGu{`˲!`["&:#o˃ݟȇ湗ο]z;Wrvשtx7.ݽp:<sEQy1%bDrH9$(l@εy*I7Jߎeٷ<"0cgFZtoUrpZU5D 6֏1+E[2b@ꪪkC3%$#db2S;N,{uv.\9h$QXBvz}ɳwx0OΧM1;=H`ׯr擤,ՍO}?zXF;Srq׽6^d8BQXXY>{y3i>h/^1WNrz׸,B33JYfô[>p+b![{OO>MW~A4FaPaH!$Yƀ u"FgZ'WV.NP-%40sܹh[оS# (4E2 p8!Ug<Ӕ{\[?;u8'╵/>w.^[j.؛ﯛRԤ;r (αD(l(܄HFf`DhVf{9D[>t97%`ГI{f*f%K,T#JY:ʱS3o^Ν{/uOlna`0L]AQE#b;H ЛDWNݻrԋgkG3ҮӢY54 Y'?O^VS}1?_s#a~{{?}]ނ 3ɟu, TQy{hww3!(ѧ!鬪Zo;O>P3`"Q@h-umjPcQE%֝u؃HPL5(+!1 D=s+bOCJ&0=;_,JwG,t":B'ԕW8J8Zƅ`%α+huT5{(~f޲>NqD;Ot $IQj#C"n|Y41SO,,.@O~K׷=3O?qS'GIzYɟ;wqme;Nw8&I><}z: SolzD]]WB:Wϟ/Bcwo¥m0 'I7~^}:vLli/Zh}UȳGr@*`vnd9p/[ĉWfP7ͨ/_Z[Z[,Ա QEMi6:";*1i1+%&(IU:8L.a'4 MtE|%:h|#<xGjgqqqu 擧/"Bk|tr|o#.MX)ifLѐCFDǐ>B#JD;оj@@$CB 18*QnmS?" *0qY CdX+p#v%$k DkPA7si[ӿ={?˿+]UL=xGpid#3 hcHuc9_ R*tv~]ʗ֮<}^73{6_pz>p'X!Q`CRk{AY6+*U1%pI Q~pҍqPI\;|$RA6v'Q<"3pF4$je ;G,jMlD:lzMG= LL"q d&4$W??.8qO~7ճ.]\T`y;1"I:O=ZYDO?ı~ogg}myڕ6Ld/⩓'ln^__h<}[XZ~WxfGŰM #;Gl|*29FVVEgE_e fy ``¹+ku]GC#&XRG?wPW|S<1ՔCJ̖=p[ZA5k5fr˵~nYLoofWW -+s{?þqv-MAlu~¹D" bY1FBQVX$Ʀdp jK5QQSfu#y'"VT-8,!v5Q@#<"-(м x#F@b1J-Q%RĨKo>gV4Sk $usKiرw>eUJ8PFBI*ZTUS$cFf:+"cZ=BH4J$vn(m"F02`u:wzo?{c'N_kӽ~ĝgs_ ^?th<+yS'ONGQut"[[]_[EppQ;7OPtywn<ͭ*ſ/o=կy^\cL@M-XmQZ!D<%LmS){dum/Sg6*e=ߥ=b!|h[,C)NgS:O4kB $iBal꺜&{4TOosʹuSb,͜ö:UU:`yQ׮ ~4lmE9Xuvz]׃0$쯭wapwLҪٿx#o~h/^y;;ACMY B1|ݕEfp^sh}s]V™'ms!Kѐ#E0輹.ܱ%Id0ǠjEHtL B +=Db4?Џ 1Yo~{1 QQ|Laq]Сͺ9f}*frD_ QHp, 30L>bam'E t$ _yFf ֐Hd'e&V&NFA$M|Er7h"DC4Dt@xS H f/b@MQ Mp1i \di-lˆ43sfU]fgNLC5ݿ6x˻OO|ɸLs}+_!E '^55?o6i"ʘJd>:VZ%]5=!-(cLԾo|*"!q]bpu4L%M L@""G^֔1M;& )!C: L YJESQlLH$\L (#uK+Ӳ:yWz; ,.5&PaH2g&vT4V%r.Г335`3@c&4B1:*ɷML[qOfvfj1=85C3#٬xɧwww.?ۛ≧'}gO|q>73wgHC]e,d$əջ$ϲ&F2&1˲Ɠ+/G>3>,#G=NC^}~sg_)!CV9b kI-Ck.Q' 59 1OFqUuRx4 #M'{ZĶ L;yd7H\!L(QUW6MCK󹤜f o}Mr<޼fy1q36""QcHHҎ*9r*ao{:دZ{r}cO:Y {;sP&jr/b6IF6.C4Ǭۛ[ɹpRu'I'{uCu}Cr(e, ~EڇMBY j{:m7^v7.^ W F썧9S:1 2P3e$S2DܺFUS5*>9"C 3:(L{M{g}:j%cHnTaZ6`4 $bT*r0]T A [" "<C2j V6P[nC(F&)>!Fc h*ᎈT!?~gwןS6M3;(R:dמ4K'6VGz4\VW/ ?0Y qõ=-dx3cjGmSc5 v4` 8PDʦBDblY݈̜wU(jLө$pZ4Ece@ AP2N YD*Z'ÄCsMnGHR"$͊;%k\ڱխ6&PC1 F1Ӕ C$4b伪"P.*]cH}DĀvZ.!! !#"9"bbwkf*шc+oiH5~/?uǿO~ӃɉSw5>q8K̚9nwc7?O{ӛl-]XXYYY\]=^GORBD?ES("U0aS#n}~p F0aQWuz!M&A@U~F1l;DI+I1WVWɕؽ~M%<}IQe!F*N}觷j4 1nρ: BhL4Yc'TtHH#QzLnyםg𙫣<1 qem^=7-&'?\ɨ)ʉkWwqv8"J rΈy>.'vl!Z-DEtH"X$i9`&̸FI,$Ix8:(tgyFeqP)K{K_?KH#0M<,l4\6:2 QkYvhGCMZiK#H$Y%dx'*#@|k;BjvDEwUoA0&q&V!hgn"P(JQؑ@ W7ψ :,(~cH UJc@=Ǎl-/\̬ tRA- )Xbop0_}eN܋Bz=o/(X,<[|٫J "kb]VeYj@A:K*L*B# @`K Ϡ 'M3R2h`@x3#I)9`)Feg$Al?OI)NG jkb"2Q#$@2i!Uef3625 10!# =)&fh@hҖ "FBҀŎpArfD ! idޛ$ni>W@E#0 F"q>K=屙}t( S.W_Af~p0k$ɗ^Zn@Un0ǟ1M#U? /Rt; .._}C(TZTCCP@&%2E FM-`-@dmA@!V,F|FFHdDLNb0Tctm#M3[^[\KwFINeC6MPI1h=RCDͭwϮb 9&fC@Scc$Tf`s^voǏf½w' ;G_t'Y M'U;v]y"ቓ767dzs|m=d8mm:1 \2)G˻C8}&Qf|.Q= ٱF$ hȆryBTPd?j(ʲ1I;idq@ƈ|b1tp癅z4fESN_&KeYgݹeW{iJ uCO8gxL/]F' {olY9:&@IbvҹU5f=z~ng0nYxj✦?s2LUԃ6$P78;k/=~u9mYɏ-/Dw1 IDATS$I9x!:(eREƢB@F$O-jU 4D-Yh|"z3 )ou%g6(j~+y/?ϼKI}>O,NllV5W.^_L{$Sbɴ6%3!GȺ:jh ڀ; &-:R2Sb!8TFD"J@n^ݺT$MIʪiTvBhʪL@5̆@DG,6QDChd-0GHց q<9BӦ("A@DBT]Yկriʪ'9;sb3~Kς/4;?ݧomr}ӊcC w_yz~{4~&Rqo_w?J(yڿP Sb{gg0q&X7&**4"!2ŠA e Q# d=oz3˩ׁ(#gC2;$RT54d)ʈr\4 Ī`0z$ViN0=o|~ammrбo& صdQ2ԛEѦ&JK1#2֬C&riwqԣjmohf&"KFXH͘;g&12q1ooxO|O>ϳOV<)ʪ 2MgvFEUG)c KDT4qqI^6 r2ԈsVO߲/O&()tyYbְ|~Q; pDbq8`w~R4ָi:Nj;[aQHkg^.pvr6lX윢:H g80bP.(ee]H -eP Նu4<$4PTH-&lPձz; Fd(DH Ќ#Q\i#[ARbCROXF*vXTMtPJT@8c2hZ%@cd@H ƄmʌZ)V:H ML h0dAGUD>ME)cɎ[7+Ƭ8K2eWVEݛ=q#ߣPxo'ޞ|_^RƆTo+ cm_}ڵ QMO@W#.` eeMIXǴб RXB"}Ϟ uؘ٘͵żYKf05CVJ Rެ6)U8L#B& EXF/{i3[^x=_ ZLM9cD!J4MF5@ä#M,MegG{5nAK AD [^; #l= v s J$"\co~k_yjiaGXZuwV\>MM$qyjԳ_{9x4Ldmmp\PK{G8)<' n֛?<8p(4}_K@&zgwHӯ}N"#9#@4rHDD}GbSQ޻ߺdXۧw:LOMSJO4C2csday4K2C0ź.]Pѕ~K:W/S0GLس<:yR syqw:b I GcrZWid)DP&`c]r[zyEkxQL|3w .&u2>ON]O!69F1hbR^xvoMNfwwz`|4?~i5~DޜkumͿuۀ .@Q$\b`ĭH jפ4b' f>\Dr>4%8v h=z(:_$ŷܳxcwJuQ&9C<9k:=֏>UuY9 ̊Xl\ 8h C$$Y)H-/_ύ*C< _m e@#^=@=DsDuFb؜s{ݿqnxFwb!?O=?}md8J[7-d\ 啍W4ML]RĦ(bl'5+JXTzӐmo{נݒ\ΙMha$IP+)08E E%TT *|$Jr* 6P .HBnH3f̜3^޻Z3 9.l_Ηt^~ބvǾ`,a\јvSADNFP+,pJb &CPnZsRza Ik[-?x݃Ͼ5Sbxoγ8lzb&X*%Fs۶DA<18&t[˸ &`nnYL pwWsa6'B wC5E1$A_?;>{?^,Q0@l"b^\cZj.u:f?2/kG>!n뗋G7ne'_+xw7};uczUL̐ g O  y}pcB`( nȕ%f̩W^uV7j>ޜlll½T\s k:kW_CP8NijbjEZ5hV/6=U.MUBM7z :sO_6Y)Znm"" e_'㆜JI!٬w*J]FD ]`+6alDnd7Dܪ~Kpfc,HApS˪ĠqIDB؉Ad@%6$8 `bD1 3C\(2P1ݖMޚ(Y&+Ψ71oĈFvjI1HMT5,Bټkb6}M?]UG|b~{gX`jpc`sSs.ZKUSsUR& ڋߟY?_u}^03sWew #3{uP1iN׬닪SҜ愞t(җr )&$SZLM]9Usdr|sso\VK)qBFii<{G~sR0kZmllܸ&9iܵK0D DC8)5~8މ3[l}ppĹCIlLY$s&inWհ(!t˹BAǗf?XaLٮțza0gaVNeUs%&qTj.+cJ  V&397"PX?2+z*r?N"λefY!6%QRr6hԼf\){g>C1RgI{uNAՂdC0sj(pVTTغTؙ B44u- @pt80'02_DX Cu1Uf b")[DgSa؍\jDd!7&g%v'jīd7V:e&eSVwW*Ap]c_/d ֽ:µԈ;%' ?=>ynu|tԿlЫЯWm]Ṵ?zC;ҕO}/}o<ܺ՘.WfR1.! WUaw'ocHVɫD2gz,9qsjSp@&" #VUWFl`ޱstj\PКS{M`bfrW89`wU%=ҵ(;6&f7 i8V7IT$ =F$k"\kUU""F)_}=aD/xJXN|!|Ztu4n^riҙg/]^N6lv4_ ӧo?{'vfdFfBK F$D 3ItL\-wJhOF LшWBnedfnUlo.׋ܭƓX94nX̖gΜy?]fxWL;4%5Rڥ)~VhKo,F@ą܅d<SJA8RMrM_׾;rS{RL6 iwc,B(tc2v"A\-ؕ>l\>yf+bWynmnqEL#S3ⅿ$'ᗗXp( v5.jApCe32RC!y5eg¦B V  sK Ϸ?N:āK׳xʙ׿KSO}7>kDw׮]}%JDv/Y&$U8xU&RGA-ڮ2j0sEr1{`&xdi 3*1VUVӔzŲKHT̼uU8vE=DfH(XI,'W0Q">]39;]5Zj TiIUTDD^<D5rLG[w/]9=;Otq1#EoM1I'&z,!\ćZ7>^\lծ^[- *)J27o ,|{s'6m"Uw'DAk@$ZZkV i&FZC ¡*®Zk"\k6)u3_X6]Q5S!mG@^vJVۡ$2X"1QTd}(h6;wpAd֐e IDAT8j^E߷MժNA$R-g֩SjW椖l^dE}vfX}_`EkCd@EZYn:aXԚF Qi뤚DZX ?-wpQqrxcPO>yĊc_S?7|A܍II#Lg|vfvb?kՍQ|azjdzS(IpNխZ\Lc v7&ssLQ\̢ԡB y>_HD׏t_KlRh؈"GJ KxMCb$"KS`,1Itĩz@ Or8쀛 '!i*1 uXjҺgw<,q[H033 y+Elܘͫ"ԼUp]}n_VgW,_u}utv}/ @͖s>۪u*4 cA=39-;vlD4brf%n0iu@l\\["E NL7x {ُZ[0v&1BpNp+Mb͝j3yxσ'l(j2Ë8sc8"1To рC6K1~s8\k5Sfr{;ߡHݝf6Prngk,_1S=qZwWt^7f}ΕN5!ݟM ? ]rʄZdjʥ: E!cjRK1UBb_Sz7vwSWL#;܌̎d͝dgo/ҭZQ5h5,}N['GnV>JA]QjIM#!f0bԮ8n,hԄظ;U K16a"I}ozlׅ"N&djٵv=C.>Qv]_r72zꐗF.ek:+~YRkײ=tb$j&!Lx|8mBEZ"a\ܴܷ&QcG^$rܿ k:ΔJ}r]i߾_˿>kK1aIz.^03gگ~gf0}𷷾H< $uafP6Q)ޫA2ossw:taQs"#8܃W3a Ε<I$ΎZ*WjBb$w5MIb@ݚD%'!v>ILZX, XHؙX&Yh,7֡ ,<\ <;z١Pp D*I=VΰQPecyS͉kmfk[~2ʯ{D[-;]탪-'nU'7)~y_kͰ͠8)54EJ3U>grņvVjU]FUb09FGFh6˹!CzsGwc(H67, a [ !;@DnVADjQϤbѱ3I$J. 5'-}%MhLa/F[拥}F[` .I~Ւ|Pq89`&۱|_r)Ei4UHXX"!ݪŘ0``f~znnm\~}{kW͛*dޠ_Kc[u!Up\p# 83=wӟݲ=xe~ph$G?yx8qO/w=w{%ucb݊Rj,pS)EM$ݾ[ZDܪPv4ᇥ! 8IhrlY8jIiwgg•.lln-zQkbR/ܗwZܸw0bp^0SlD1EU$c BT@('v:>d9hs-Xw=I ѭhQ\.xkc‹/DyO}=_t6:ww/ _" A5rpW322w(qʄZܛZ.}-յV!!jq/*k`3w5ܤy04 1-gDNMHT,h{yrcttpm[ٿz=$aXf]m>::*9_ K/85`!Q)d9ld|k}?w+mKBRAD"^ꐒ%?| !LwN_5wɴz1l`UB #[zG)K/3=qj=į+QU2 ]SĬ f"vw3w;Q`cPɾ5w]^e<3m;2'zAbRK-$#b}E[ˣP}?w{gj]Z}-hͻΰrP5TgE˞s`Z'7"LnP7B=nè4> (z#FћQ:/J D̄ i}- uo,-50m,(4@l^ݵ˰G1<{Djh&p^f\/uFDHF`(MQANjNl n[@ݼýPq1s 2iW=Lf<ׯ]MU[EkuV-|~.ݝW<j˶ :S5s3usp\rLQ HkꯐbZЦDfIg:ev4jezdk 1j5UH1/v#$2蔚Ѥiˣ}qyم\DĀDSͮݘ`8WG/=Kd]4q 5'r +ܪ: q1 Y˃Z.1[5Jߧf\vn׷>b#PD$NN50J{$Pԫ>$ qpE󸯗ftEiW)Bڀ" ") kP͍^f^O}p=} _Wv qٿQdLy}cD;j Qp'" Nj69C 1qS|\?ǯ=(_*^7.~6?zk:{}~Yacxľ.''>{?Ox;AۧVO{: |0x mblh9FرIB3d/ d>NĎ Z %5 {50 nVNF:X <f$DPc$a&d'>XJJfsw|UF¬lҶ}3MҴzQCAhqon!h< K"y=uӼ:fhgM-:5}ιӛL$)3%Yl \`FC]tDUG1 2tPچjQ1 x ˲da[5+S9|ws{})ɖ2|2yByoa_NJ4sR.GrfX_ig,\>.c%Fd1RIq$yk~[%LaSQW.t92kc3sUH!FH1DKȀ2)L]4QNNpqqF@CG 4LQ(|ɞP\O6Z_mOķx#X{nK^y !RzCS4 yr/Fu1A F iL f iW$F<|i^p>R0&5( B@43Z,JTev i۴2C.UeN& '¦i; fΝ?stx¾hVIwbtw;9я"Qډ)z!RLA5$sDUUjQU= i2x/q<;F@jffҗ2!PUə""O,c9G:ufk܌GK=Ѥ`bi4P&2i- 30˙ H L=D*xsьItYN?.);$7{YB0Mɢ cUuS ϯZS9[9=yr;}3raOJo(b2”TEAIw7*O:K8r9 `j@7 ! XL @ &@&F` ,P"E0 2iSb(m`m3S@T'fP*SrH'?}Wt}AeDһșu }z]T,fe[HL\YU !:@јTc`@kgϜ=r w4>ٻ-IY:Qɝy蘼/\!2mO 8kϮihtn4M 1&-*\vĬfRt 4i-|yp']ڻuI{o}ܻ D,E"@WTTݽ9eSdaڥNf ,wJR;M1v"blQs!1C4\SM9C(ʶmE%1BJ@՛gfT*Ե+ /Mpԭɿׄ*_~57D{~O]s:)ϟ a l.t4 g--d"}a><;ʈwdr c O7F犲qX5?7W<V=>n4=_l?cLz`!eh R2#h5Wuqde44SS0 4iXU0jw 5χ( Qk:-lfX'(2 Mi)!Li^Yq.j8MMt,4$$nvrcjmg!4A)\81¼j0E͈L T"&O|`BȑRpĥdm‘7OmBlU9G2CtX()`S0Hv!%J/o\zi̾t/;rk&Ay骽z\\I\AL0[v&`2řs~k3=R Ljȴ6$$9 s`)w2Xr.)I1 1v; !3ZP= Pcsѽz E$دf{4ڙ^ڽU1 wMɗkɑ_B SBfOEL7I#":BD FMmT3/} g $)E$kaJDUQn4=;?CUw;msc;UeR1a QDL&Fc v!&df&:Nh}_Gj,?◽3~w e 8@7jbo葿Qj3|7~[>;?33[eA 4eCk]DΛ!#LS"9vo)!e[}橷 LI5SD0TvztaFSsWST$/U  0anj2T U4cĀttQ&A@(v j*pv@1 Eh``$ ,s4"S&U |{j]:gWElz.uɾs7|xx/xܧKyvGHel$d& ԒBRis%tdUswW8%fȆ3F =Y fjLR׆2-Sx-%cS 1R3jѠܗc`9TV&c4+/)BQHЁ1 mP 5z0qJXQj]Mn0zOojT! L@/'7vԂX$R38g@3$R՜1:,IJ^ߩl !!A$ft;x<r!U^X mq*{*P~{<xs{ŸnqI)5Mh³m_qlj3pT5RHzCzG;U,UU34#BT/7'o!(z>vei4=Ǘ,ݬe{v'p7oĭwױS7?O'TwC.> 8' .pg L,?bR L4Wk,e.qJPt7 TQrPTDŽf.F*3(#*N 65 ş>j` S;k3`G; $C*e& A TU I L)7E 33fB2$!hPRy5h*HMƙҶ3 `R cxY*s||MO6F/q~)szbՎ\ѽzqvtzeWu;'%δ d;b Bvَ T`tRgn_WˢSEY b@6*hJ*IE4D1I)@ΒAB"e9++nW:\!6˲޵7uB pqoo\ww|_?ﺥCD$R{ó#SE9xoG &fvJ}ܿo9w1 GqsD q˾w}כ=/l/yم+_|7K^ҟw?׽& w$*`m{d[M&//ys 9Migs34m1(MmKem9?MDlJ4 N[*8&Te0_w6fnl:s77KM z@`_ݭX ->H{G0i6b'D[N3K! ^3/Ď --I Ԕ C S.O;P%dƀDw JlI}2 J2'h h䢢)"qTuպ]R\)IOvrq^x"y2}(g9ŧ?"/*L-3'ZU!AaoI3WA  Y2HFs,Ib8ZB{w,TǪy3#uh@U1!akXA! k@$ H Ոs F(> !%G=@{Ԑ쉭eZI9lAF{sǩp I OxO:zZL$ ,I&yfD962PAl* j"U5wF6>15ep`[:8p6{I:ž}Bjbf:Aθ-}X[vub)%ǖW;~Umh;HUG(}U6[ok blQP02sAL( FۆD5ӠU͏HSJ"&AL(ɀ9dk򖕳'?῎)K{ eS;[wnᆏ㟾?o~GKȷ4K&N2)uBȀLCk~7~5GNF*mb)fv 68;s]o[?N~3_xaŒDv욣_ongb=%+-BD-OM L[3y|??W^u^ X{h;W|h?m<纗o.n>~ePgrdZip':׾s?g<-Ã8x4M^ؙiz,qf`Ğ1*G=Xwr@[Dqm[tBHGo˲W*ָ O ]V~/v_:ua=ǙӓĮk3dE"rRk$5(9b2``ED4"BC#4qwMv'/PJA Af"(Nɀ4Ww-O,ّ#Kr#1Q([ A+ ۦ=E >)?U5ӖWYvc5}BCeFDrݽڣȼ"β@3 Q4 Z|9S+{~oHAL*A5 #I?1f*hJ(dbQ)MS=i BR4V "@FpA{m59SeCV5bd5goJJ' ѡ*!tcDFRmbq8:3we_څ RL(ʤ[t sW/x2)bO$VWې:eֶ6WǍilĤ-U +33 ng4`Ҵϥ(z31r6.F  @DTaJ1Mw)ϔ+T"MEJfh] @UBk};"'^M?P4lFpA"O}_u+o&@3S"zʻ"(S?AH 3?wu۷vۭ2ZT"V FmM=;whvaOZp5{וcvυJ!6ꕜY Z0hڥ; 7A^?U9^Aۯ]Mox<k_w ]m͝X{5{?|=NՠrʣVw? /*"AvJ&bD\?c{?3?ؑ[LMcJ$,tc:w6wbI iUg=N>>Wᗈ Ӧ3QsD4ł#!WT Ft|_p z;zfNUhB1Z]!)$h빅ɤf yJkMZ5M Tjj $_ )Wa7.|ӟC+kgp{~kwEяxl_zp-ȳ{[CK>ww=DTˏjnw̽XpbUHJ[Mk"Ej;/8)W/-?qoOfEBȱ-]I=~xPZS@?@XX9^_pˋ˽~ovGǠ+t|o.gn?7yz7uAG 9ȴvf7E#:&XaT*y&~n106l@a*F -],eTu.ibF`BnhTs mW5S0DBU#4bC4ERlg$=n 250Қ58` ͘ 콚%i@M m4=yҰ//r`@Zk _cù<}D]f3$!mlmovw0[8 B`\ءA[YD3PQQ  !]¡)3noo GJ5 dLV$fFB˪O)1ϔ8gg@)4Q) &42ʑ@EUш$M$!j2řA,f|0 )!H fEnQ]f\Tr.,95p*bFӅFUmո IDAT5fsx4{RU>v!ӯ$x5Q! -f5褕jLaRד+E Ɠ@S4_cJ J"(Nn'[9ȃS:gί&3;;;EEIP%Q"v vM&fuE9bo) Ӑ$@ ,+o}w;t@q{ZD5՜wm۴!lomYD93 >MHlUA&@3K9 "%UEO|Ǐci7g/^?&͡3{ +{~]?_jT\5;N:{{`U , U|ٸpUuBw~jnܤV% 轶LC3 &r0!d9Qo];d 45ɖvƭ[锞?9:F1CS4$:-ecMhdu~]&20!LΝ|U*/!f\&HB0B ˯yj&``43!d(~˒{Щqc:A\=Mb'񆯱\"mW,"1d<(gd[6buHM#&H MҨP2bvb`D4`pQs\B,M, ػX-tx(?HPWöCST3* sQD ,5( #O{Tk6Kj&%(hcӸU!|UDIK<]2!LɌ@*a"b6o eɋ{,ʗd_LDSrTbR3T$檺yM7=5G:ʂ%1ѤI9\VFDCjhQt8B@tو9_һ8 )z.B3 1$1ժ*m!=sխnZ@K@(&+hdcfaSn%M)}5gqv띯~K~7ߑM GWӧO^ʹW2ϱЩ3zʯV//_`̀R (yL#HJ1UAdЭ,_xS${ " պU&6f̐au9~淿>OWoo-t3 OwNR@Hl cu $R2&FΛ34`mPP^?'<~ iO쌋:ɕo|h|}Voͦ\~TΰY8/ܧ+/eDӽ&!vv}՝4㪐Sw[+W^v^Z8uC4)~(B&$8â(elAT@PG#o?>kV-=cGzK~g_[D ~eŽuz~#n3; bFT4"ʂEqI4i*B&2M%76LQ`FDcUVU@,*A0 s*jvX̒1$E," ,VRۥ,ƙ&\] Дiy=DYAҧ0Gf3K_>.F55YACQ0~UbN$5HH0@X" Z}]>5Qkƍ87JZٮּB LAqD%C͐?UHyJf]% gbUHT( aJ*b)J-:I)$@rsg4! |DIʢQC I'6:~*ĺ !%NdۭRf{UQmVH"VU=t]\;M3$鑫%y\n!4J1Dc$f 0#Ք!RQFSJ)Sf0Xj~Z3~Ɠk3E\Z~`]Zzྜྷk{k:vnu~rk+`jޗpp G}˨B}P +SظڻNonN~ܷߦn1 Ξ0^z{֤Ng{N}![֣IRt Ov͵[۽A5(1ZRU?&w DFB)Tܴi(a2ܵw[3*vv`)M6]MM U/2Cq* VY sDLMaf`2;$)fcrJʻyU$ dG$TJ3s  @ P.(JOOtO$pfv ;(#T.ETdʃs=؁$I4 1$Tp vm itk_;J;g_GkIY)hؐؔȲ@EŲ;$)M,3I6 IDDDmƝ:(b!I$C0S M3?PmB;o􇋖>Kh TRGDh9K"x1S4é9hVORڶz5W,..};s̜B L&5t96IDRmZlH3nj/hϞs. bT-8IǣĴن^X(VUgR0w?y5GO<%A\nG?f !pE)y33ѕs A%= vݶ"MfTDs ^u~|zc.tj88x`WO\78fg5{m]1;;|'+ ssMMMl=ymm*J"۳ܹWjªϷ!6M*{Օ=uݶmC3mi&W]aLW=s|յ=ŝ$)#\TцqA̔$0dKFR}?-fG'#='yI㉅CRܾv66ϟ`;:'%k?Cw_j{`ˮnL h )$Dz$qAR'r/?'-W*eNE5P"DQs= {cbhPvި^iﵿ _.%p(K_p)E믿1(;}JjV;}N/ܳϾvY؜eJj2Mʼ'pxm7IM_ s;_gp{%h3ݶPmwMS7]0E1Ybv%zX/SG/Ƞ3Ru汣49fB `<%jJlf]@Q98 VB,IΝ9ܟW5;{d+3Q s^KnZ hH 㴬 A!pPHP{`{^cBYK^4kZNU,Z%@ ER01D "XU8pOO4xC}]99+kʢNPa}y˗,,Uu(kFܡlEZOʽAW^gZ~Eʡ̢HkktZz~neuΈp3Uۛlneا(@i>uķxJ/DW`u*B]uUUeQrZx#O9~_Dm<, t@tN><;vh/v>^}c賟~W+_1---3/.//+?_O}~SGyص'x?//} />{\EǏ.}z] 矑|.\h/g>}g/?Ϯ-Ο:sj>,sO@Ͽ_W~WW,MVV~o9r ZMZP@V$ c> 3 {'Fo޼ֹM7v, Jm7Weylrܼݣ_2n^Y_Z;̩3,,=qny")& 83߫ݼ}{9Iqħa)f63ͧE <}u)$Wڽ=zg.Aj_;_=}]lyVqXOwADY5/֠*!BUj3Bt4)|ygr /uP$H`,,,*8kwckLZ8&IY$IK$11H5֐%D"m`"Rk!j|Y$`H̱u{rDTTamȤQfjYE+G zQf h<ƣ1 t3t~Ϝ0h *XN{mj JM8iYD i-,buZ"d\L81 ͷ/} ߳76OhŔR|!f2r%DUc(IG] a (2UEaQN&eIK,TUj|!ʂ(`H{Ut$ʢF/ˆO.`T@90*H  ɓ'{+i~dugw  6Ob^S㽽2X4 2j {2̷^5F"Ŵeuv*-ς꺪nu%O=*s +FᎪ~珿ϼ+~,K QHA2&dfk<iQ(yYDEIAW7n_}jbr]׮_ˋiQvʕ+EQ~?[W}O뷮w;'zrk{ͷ'9V\w{?$hë/flks{Nd~zw>s,?wtq4ciw=Vle}AnluWt~/%in׿ZP AȀJ@D%@AgDau6ښ<3~߸Z˥'&>\Z磰 nڹ`4 SNBm+Ko&M؍o=s3lЙ+WZʦiިv m>SgۻsgH"ܛ6lUN)do6ڙֽΏ2z~R;=wubvAVvhzH8>3PU9DA%bѻI9z"737C"1fQ1A Y MqDT#h,m2$/v2cc1UU5so #PXcHFF_ٜՓ@n#;@a7xP.?O=ƣgp'F<=;b4z8XKLV&k´RP%EGkAcɩ(K 2hMQQp杻—o$Hk 7BiqUc/M@"Hh4Vy27ߺ$r@yP$>E/^'K4HlB EifQEkA 1 +׷;v㽑(¼Z @sε|Kh,*foqI^}ߴ֑1*J E s R+l4E D(h MڱW} 66nw5md2$}nt)nڸ}mkokp5p黗zPJZ~(P#"zӶEQN+WmuEL[nz*&oڻw5*:*[%t~L6k}@1Mo^{ ^`ٹuҙ5 ћI`)栁D#$ JGkQ=l%omm۝&@f:9 ]߯~SnY~ecB5oMy~+ן:ro۶s9;I9xCgrQ; ǼIrvZwTdJ">}K߮). ˆv{? \CK{V:Upg/dsy]?՗^?s=k;En (n/>YNj8TJhC/ bg}ex ~YEA"09$RD&0MB=ќ@c4+! U#iXcddyB5!aT(uJ %2Dd4F&I;vYϸ.3O^fSB ,J&Fƾ,%"4>@* @JOΕɗGx4rFj4P~u!#J8n%mjvJBjM@Ek1I1*,@ӬXV3ij.޻|wcaTҖRGFU YaP2D5P m_$$ UQA=AVTPZ {[YgxZ#ibjT+[cqW/5b@UDEx{cAEşz]gPy ĔQ!!@D(ULF5>Yj2&kR"@M$.~ FPRB03nI@ƅR@D(0l /-{PaeFz>:߿P=Tf 휁*YqnB#$H{$` I Q%z )f畇͝Փvwn2V]]ub#0YƑ1i:BeaaDUϤ(A56  ^'%J}{c5t;ݮ @2/Jf+cs4~mUXHp "Mw{h#;D{4+shx "3fQpU'N8rnn޺uce0%g^E \+M1vo0Fb,%iZ..tgl}(YXA5(0n;tZT>TUWdssJg,[?tho0 cDˡ^?**8Y܇?UcۨlˇJt}ko>٠(6z Y_75!`{l5A !Fk`/ZAX5nd! U RgmUeCSx$ijuHjjĂe<0/i(B d$0Nwv7Ǽe^+J $ҲaHMN*\ ZTD3) g\twV 0O|&w:ijWܐ_Wn3Ϯhywe0Y;}w4ռm@9K)tNd} ~~u8dGbz/Ɏ ;7e2rwÝj%Kf`hxwe\Bom1=26SN{}z_\*̀( prKbg ,@>"QeKd Q:B=ȫPFF@V$pEUINm%eao|$")1hFPBg;k>jd+@ 1;_ =+yُ`d,x䀂F HBHgK"s!`ˢ4w}o|k/?w3 * jIQ&Y@h4p:ʇAQA%&EeT%S3yf!$x+_RiUkR{ H,`U~TlդrYuE`FZE$ .@v(Zuu{W^ڟc6˒PxB b{*Mej_%^'J.y]?0^;I3Y^^sPFSk VY'e2iFHV@/1 (\~k׹rsӼ*cVffu ).Piq8Kۄh\ TQDL׺: u<PN5fzU @Z 5XTu~׮3A.+g܂5mݹ~4Ϗ?Hٜ& $P"kܔGϞ؜\R(sKGC IrKxWF7Ć3kX*Q{2r,˩q;m2!:75yl6mt?cdυ D(E.Vpg_]QIՅ&I;ƛյa9wN|ckWۦmaUҐxk\ނCKX.Hs2P鎮(nQ;+ZyswwUrݘr]lv5 }pQ]D]]{[;vM ,/.!bf5AUEEѹH9.hŢiwIӒ 1^(n_{!ͽG "BHʊ( "3u@%J@Y 6ƎIJQd!İ3, 78*APwV@lTR25=,ƣh9s,;>(=T^2ȾJ=f;g8TЀMXTajjgw4 ;"C'b:*߿t{sԘ)FIMP,3>:$U  $BD XU!ԡ*28XU4(AZV;޼t}g\j´I A2[ɴ-rD#*V (_z1Fa`Q1CQU`K&_mYmwC*;wv&yQ{| hiq=w绰`(͠,sY:ݶ0^!h> tZI׋*֨ ד!gZUY5IRॊ6gSehҢ:gC,$`[M,-c$_/hl%P)mu*hxAh Dk@U~~H%Py7AU)!k3O?֙'i{W~aaqIT9qg^x֏]Z?vkk/ý-$ JQφ-(pE"Ƈ~}U/dZ@vΐF?ƣ{Dq* }`a~"J<@]{F"jd!V5FHA1ΚbU'EB ${S|Q{+F_okI`cZd`CP;c){D!B9@tXo]C6ox<%5QHOJ_hQQ -)Ud%aPGcӕf, (뮲p岮!Ɯ~}i94+Wn)WW%8&Y[T2H8ke`2sI2N[Y* pP1+,qťc!z{kC$ $A6^ ۇH}$"Pi.{ A(϶CM,:s_/S.mRQCBB YVk[n6wv+갸< Qd(BUU_s9fY]יA5JQC,ZU:s=YT$e :ɲ|w<>//:pNMM2Z3}O upC\~niUv2\Qؓ{[I\U8i ͘Uˆd(hE5 8f"uȈ0H*쉨F 4$ %EE4cG JU"JE ]cL~BAX_ldiPc(*Dm"  xP?JMN*N};}_R!!Q@UP`fdB" մf_v|UŨ % D.!2NѲ\߹]!$֨`]B7v*G.ZDLH) A8*@\?CA7ۘ*(QvM2618C'9S/_JÑAPu<lF6s,{[U+21.1VT|ZhWW'e\B?^2*Jݸ}o_+777&|4"%Y:?5el D\/'JN=[Ѓ$Y((g>^Nnunݺ:LZVfiNHdL}&K{atGJQL/OZXX4/c>S\N{K;wWKzCGumC뇷fY676\S*פv>}z}nĝ]`-bi\g-T/+~T9̶vp.aK˼-X@H\+݆HFlD9,Hf"j̛kbˑ 1мa#T4Ơ"!y |ZWDxƎU543e2:8LSŘ * Q"MD9H#8("[ fH+;Lc\.?.韆xt/qWj6g(ZSX+ehT5 1CQEI$Z Z$RN9*77~k]?(3iy QZxP K"!C,@$V%((a11LumL;G8eQnT!L &+fbĉ#L3}ߊVj%I1,Ԃ5@n;vbtv1wF+G{v X H!`RgZ$v sBiZ$ 6.Ԧ6 #Bb-%BJTUGb"tX5( 3[c:DwC}Riey-MIcmIUeh[-Bt:k5ƴ[$\b 7Xg.+f@+"qDVt<U-|ދ~<"P`aQ"RpK SD.;$P|ʴ(@@޺H,wnw[[;ė7M}~WE][7nvGEM"? pBcȑu*zw].$2QfPDYC*`@a4Pg3b|ђB@L4-u+qˀ[ٔkL3ivvsIمGWUxds@Bf,#ݳTg$_6YbQ 0Rb/=e)I>S|#o;iϩ 'bB[(>9M[_޸8Y?{d΀v2:7nͭ1YJȶ{fмaZqlz'd*._۷m:-U25#xԀ3Y QZ)(3ZH`FH3H","Hı^'@F\m\|H| BhH Q"MR h;íy*FI y3@D!4?2 @l,`c 0zb$迱Oi?71Xӟ IDATz yUj4"xx>ͺXjaXX $"#DT)2\S jRw[C_| W]_ܳW $Hz(X($@4qKKo^z;De9Qti߽}5 *akhކ&cpx<BUe Ц~_-3^(^g*˲9cmnڈ:77wvwOc B[4Ej8|^Q$r-UPxHӢB:PEKծ} 6K\9}J)Ts@ JfF$2U! 9BbU0D(cDWѺ!QA-& Z0TFTU*^2*UQTn3>e_]UbU1[#7c9}~UC~,\3\C! p-{93ŲR# 9ֆF ,FQy%}^w~ѐ1C e|?K%gd%Pq`3{u9Cg~w OZdXL⭥b{˪; ݶIK0|N 81v}7v؄gί1؅Ue V-z+.nl V 6Vw:8U1w.^;?Z_?O8$B)!l "u]$2X4UaQ`l;wuNz)z8W4^hX j4E*̢ ?@UF0*zA8:$ jWՍqY?l-NiQ|}BLk¢02D֊s@٨ "1:&`cC@L$ =0#)F{x H0$P PG¦E(dd2#M|D A`MJDTB]( c֦pCeP#(dMC` rie4Z(WrJ9-(tx{BuPAn'{Sfki_|4H9O zؚ}vdql{?szmaO(4vYg,;:sNDmrJ*YR I^dmْ,l3C4nfoCØ͇f6laekIUJܗ}ވ8geʬ̪w'n܈'~wr 0!06ɧ<$R:wj _x?S}yjV]c#DxX[[e}Kku.#Sy YT `˻m +g"*w==y4?ɚthnfˮ `_ ĄEeA5,R.3qYN{\$r]o<2[. F={@HWB` 3CETbZ hE.,X"l  $r3 .-OJ;^J &3c#G6 ,8,U-s3@9lV'Z3 (!\P*8sI)E=h޲؅9n D+Aب-Wl3D4nkItOy~IN>QW-[ȱ Fwin{bDYV D#)*O(D$ucCÕed.udlrjv-謁 Y ]jT9 4kv]7 w[w˻MtK]϶XMrN@7IG 䁩ٌ4N OLCC&7¬1hȔc4Ez{NEY$J I9Lj\zŢ\Cm\f[V^YD劅PVF@3{,YPz"T,o˙ VG,x(@uhXzCI }1p>Ը*VC'.dd y:+2oq^S2g(,{!͇40XQ3'.BzDEDFأAP")ROokazm/.,hFj"MJ)ZA\fO-t~#1-[DaZ0S%)"@۵o]& w "CաF-OFOLVU=s#e @Z(qݷ.Ģw P^sD UnDH)ʈ#g!@BT&8HUU#< 9\X/ X $ɓXy_DW ;`,1@ %HBeE =)a$@?Zc%h5 T©Iy ,?CTe`xGU(;xۉk\}P2]=ͫ8vD\ f&8 Qͅڥ,v`FXYfB521}&F5h,vuE>K1AAd" 0W {_`:H'.W^ ]$Oלm싈_2G Rv+C^&S]F Ϋ1j$pͪ׆m9+3\XgJ\VSVJm葝=z^+o@;e22y)C/zZ"'g, WMi9+G>gɽ̵:QWgX|Of xjZ_Sh-i]Gq#V\6҄ Ba=M8W q(/Xde$k^- 57P>|;02oJ8n7b]ml`8u[d^_ V8ojl+u'ZglV*:cI6K}#֜׿ٷ:Gָqn̹sAT]׈sd^= 'xKL/3"Nr9;NsC~͋W< kS ΍qD2ph - .̺ǹ{L"+@ $ `<_ {)L{^6|n@qIDqzí>"F6=u?kc6Y{px~35*ljأ`ϸyp0=su~i0 {/M-ЛWXO.1ρށ{w%(c)qӟz^)೦0w`ށ{[*qv*y`ޗ I@2 d ϥnbQ,ȋYqMHzY@2 d |K@]' ray7@2&nr4{{r&bҀrF2lR,ie},o+U(=ќ߀LFY氺gYV,nP8{ő"ŞAx1OD&B|9HAQAФDP%y !z[bD ) 2t@^RV ;r#Qex ry@ DX@A7.^tC/oUܦA sW~`_|;n_%u ͮc#ǾTy`F:-Ixxe5bK'O=cGqm¡SQԒc=7rWDѸ,k 5Q(vc53݁('O>F/?5wiVMjPҙnhϑզ;8x劷w#e5]>3[w\ m06{VФ]eSK;U]u[{2qa2č݅m 514=4DFg'GBLcTT4i X}iy_yqv 7]/{_* Ʋ]@@XK$׾8B("K?W=I?^MȋRJP+MXƸ<ϺX'i3@qŞA06WJ!g avYAD:PDbvbsq.s.AAy-GG*LoC7v Q@I R֏[Utz+^VKY[lgV( 6͵1 Ay@6Ava(=bJ}0u܌ H\}.h(l/6KAaށ/%{s#J_b=!MO/.ΦU[rGݜf;+n3Y`Yu $P(Fѓ w;]d"oUࡳqq'ʳĵmiO'b˒ Rr*r؝| 3% E{0q<ОjRPa`%41q%Xi=Z@$M|1Z!<ϭuKm['IWU( Wق l}~. t+ 8w2pi8Y% uڣJJ}>Il[׶fZidZW4ȧLi^{~0S\Eeg/N*l8Bc6TxFUX CZMq@\`Hk`sl;y:6gQ3Nr8 hcbq1Ik$RoYT$6t=Q(9q4m')Ġ/V8e{89"ɓ y˄$ų#Nk<͂J"t ˝Fzlby>R|PgAa`#)h6/V/}?Gb NL?ynBul0ԛ;wm7/&{0AA\fkA7u |@B3ﯪ,WMnh*yDJL=IroSYfbtZ?gNlu}cJc(ئME!m1r$@E@R;gI|uP!! IDATX/4f3TP"Vy{6kˡ~lC/ezgudz[`vnv,m} Wpd D̾ (אuHoYҏ)̈=1|Xpr|NJXQ ?Yq^L/;g3P،Gme 3 `VGgoM'CԣޣV: ȏiYҖZn%wT,Aɫ4:Py= Z.^ C9| acZ#֏Jkmg8u`ztOcuHnC~dH6Su ;NuF:Kam/GoaaM!2`DH36@^8,p'͡Ly+=yem74TƩtK/eA ?#%(=BAyFe9[Hy`Yy)Xٝ-^EDʟgcE$'   BBEjT΃(G`XP Vʉ7 J)˞QP#)Jc') iwɻ> \ځѦ*~_ |$N; ap20F&U{CH/ZJ@) A3<{A"F _~'||#?{]~P +äDVk_C["Y=ZXLI6ńnA%hFs=SiŶ#lL7-ϙZ`,eKB⠥P/ {^\vVy H0s:LEØyKeMa!{w[/^ue{]  ݰIJ isͦ12mSf鯃JE,ke@CלicZb1B5phk+d4B @"DYg^P5.X0($BD (a ,FN]?W; @bsh0s_1R +w_Z TuuVGRrƌ^ofC*[1u"u1Abklj}nEĴT/qՆ\sV)ʁ ׂG.u/9eJ/j 0Le% Ӈ0Gϻ6e1+<G]Aʖѱ}v\[\cGO<|Ljӡi.@kҝ nw4iHG] 2!P!9`–aA!Rx8>fG;"¾R1 "bffVJI?6Jp ItYJ@5 AhS Lpˑo} *Z[\_/V̬g`ឃ^6lxP)X+.DH<[耵CHx^A<9AO Q+rewd /Er{eOB'0䐽ΫgdJMm{vMۮ8u!I68 εT@syǧƏ-I^Mom<9}h OAVmtLXQ10(VI 4̊2`Q LWhk"a:d^M\fڦQKn8O@=\}ϼ[ N.@/Wي˴/i·iwƏV^(0Q!eZGBL_yW-GO>SwXi0EOTiJWM.%9fឆ%iH"2cRj9捈׳eUAr uzY _1⋊ߟgFk+ک: zR,@7!3g g@etvxzN$'Ojn40A@b%Ԇxrwd~nܱ?hdd2<:\ ' xXMWy|!g~&&OAOC$G6L&yd%@Q<$4G~׽9Sڜ<'%go %OdB@"ubbeW{/յH_B A X{*ݰWzs:eρ[1VM0Zj\ # 2B (\QקeF[*b23hZd9u kOC5ǵܝdOUY1FL<{Y+Uʸ[ånnCaNV{;VD{@G6 V*Zt 0#&Df2 TΉYwUcBRKq_q ;CEN]YŐChRT^LV{lC -9րU17\31dxɐ<1sLٱg Ŋ |RۺhcGgɍB&d2~ aݙj-]T}{ G[~ï}$'& mSGys|oZ?rFROLdbaGeS=|:LY=G̢k95]{9oKMRoejptWu!]ِT@@ O} ?5Z)k 7޸s׮{vp {޺tc+FeisϞ-Y{/}izz&  س{_D}bpcNSE@]k흿kFF nѣy#ǎ55B{ӛ׿^ !+(G~u!vGڛ/L+=??p-@mj4Pj]oAo}8t(N)DD 2 " {lo>uj}.L\VB[DW^q5ww,Z@9Hy) !XP@ "DhuoWon ߸+w!tZȊ$ y "d*up}~ek?xMޡŀL͸w<ǐ/&Z. %:Q* J,Ă W7f SR n3З{97*v((H1p+@P Iw{{^}ze+C#ZެDU>T'#/ɧ&-A6P9_OG·w#OV.٣Zj/~7}yٙdX/z}n<|#~͎㩙_7+TisGF#J5ëJ]oO%#Ow<sgFq ʂ+:zfSS{07uW]/Է'Pk:J,j4PB=8O3ss}d{&cǎ}k_/>Ѓs]yOOn:XM{owE<Оٟ{[CObF Y:v套MNN&럻v/\}V:= 0*% (11~d$o}3s' #R(e.:,N2>6|ww6{Eˋ6Ay Yf$@œB}žYG>7>WqWnJNr $D '_2ԪOI|0xxP#6Ga!El"CVKF1$΃eX`7[5%BlyDeZ#=lI3W8/6!I/_c=Β8A\+5ji7]GF[^h5߯g;ONuR\9듛sn|]kww_ͥW;o۳ۮ~j6Ffop}{6c}`ݷGK7kyW{ӫ4 &#)WP׹?}J.pOkwxQ*qI){H2;H# B_? n3o@28ժ.e\ QP˘"^wQjet&sy^C$;Zq O%-t?k}M]OfӐ:ԃj˦@3TxɎ^kT4w ,Hx'و mzBwo]kC|z }f՗eX V7N:IJHe VR^˨8qe 2x:<|֥ $ E1t v^/JѨG ,;JYV8DT8DdӬzl/~89 /{̼s׮k_ZZUZ;k HP>=r}lxdSXAk]Z[ƆGRO. Yh ٹsO=|;~C7WPDDĉO7߼}׎Akp怀Xk;]Zg0NzaE0}T7[n,􇞮>yRv ,C ^yY>}[pkX嶝m(ʳ%sںcK9=ۉNnW)$XQQ;CHBDnvz}p7>;CݏDK !3P+~A<z{}㺛>/|q:4UyO~?߾2 W}[a'/C;TQz[Q/Q4Ԝ"G'wIO;@5]ybk>2Sjt{B?<?p@0dnѓtb=z|c 3ɿ!c 6OĵGNܢ{w'#ӧڳ?g[:|W)yzzRSYw^leߺ_8bجr2MlR?2 tp𘋑@ssQ֋@ @8D"y d Dx\DDҿt{_Y.q(@Zn36P<[S5)u6.4ylHܷ␀#DJBr9tixxm9障.EAs  ve Xc54 .5ڨGհ xAEއ+;Muy 1@$_ggx̣FgKlD n8G!e>2,D g/ي}[q~;8z??OZT?zG.̈e˓eϻpd8?}o}[ >?Ѥ:l6 | eC7sx`w{f IDATFfw_}W6D79RHWthL%ت17~lDT ӯ[w] ~_,>}I+&4qW͌Wk~5qBwy~?-HSOn>':7v7n!ȂT&oALA(Rj"o]MOkOw`7~U׾M+~O,\wMyk 9UKߝ͟7EװwUGfY<؄cB a $7/%wHnܛ`a2 yے5XsK|juNwK-z|J޵ko7!T^zo5uïQ 5i)@LuP8RDP̸m&Aػې;<s{EqEc{Jp!sG6'鷶W/|[5G3yTq <|8#~j>;)s@ݏS^{D ܑ9|-s{g3ݞ60?#N+Agyu Vx $080@s pjl.w]i+mGmsqyxc I$dQ:rѩF@Rܸvn'd(#n)g E]ifub>iZjjůMg Tlϑ.Nn߱MCl\o{80W-6qfl"@͓92Rnp<#?T Y%>:~9kWo. !-LN>x sG5%H($ 'Ҧk^\sov?% w@(;[oU"F@jշ}k_㾣x?oM?|y;/d{fwöʑKeW_ۯ}jyf2.5RVB B ‚ C%#2 SQt%[X$+EqYN̎E J,pT?Ű2+ߌz%?b͒{'&abY(E\R'd q)1k` Qf튃"*84Oc%nQljj `02ǟ7dJ'۠Ojpg68ݻ}+]c9vǸi?zp>{ҋ\R\"&9rh靕| 56Bf8Z=PN,O P3Wv Ubiw&kveс'.%g#2Ȕ۝잧HP:""(!E L;=?7 > iJLgsy!ď\m4H@(/Eڶ3{#L!x(2vF3KS8_}ë+ժgV3"΃cGI0;?wfg(,ѯqǭ36Dt>y˖ ˜ȹK'TT#;!jjyiܑFN|*47;zjgǏ\y2kOO8Қː:5ɱHaT) :b3W+Oܶk ju4oԺ}Z;j̨o<_{y##$rgX:?VZ]hur88Rs^_x6Dc?q;5~7~g^A2@T<>ٗ1rzޕR==?wޣyl5ǝ`T~_w#Ew{.5 p\x>`| ~dL1 )̓c%eaށArʃ0aj6F9D$ѫt 5-/LNYx+:p-O%d惈Whd 9b1%%X0+P3E.*hhr"@!6y:11-3ؠOfnߕ~?wq"?桭UU3 Gy̡U!C`(J|Ř8btQI%TN  (υP= u"@[fݖZS51t4א0R&X氛71WKy!&q.꘹vBD@Yv#3@r WLf*"&=J ٜ1"j=3bG&yL񗿼h01(GFGvl^uqΥc7==}nᗼ^% 000Wd LT (3?w^a?9Oi ōD!͎Jk^~Jdy9ry]mM%Y6wL9\_XS.*}IxE5LZ;/Qihp1c*{$@"فO<LGgU&KIlX~-+˸hX ¬ݱ;vt;/>wOcϮB44业.H֧lvE<ԭ~w@uog.^>3|[35 gyR*,ܪ--O,z-ə!(j05EPSS3ScP5*,Jj؀>x!`C!RrR@gܛXβDJI!kmM .5SEu!"!`Ov48QVY*YE#PuaOGLU 1r,2|pfA#e)VN!Qf3sʕ !4 TфJf" 6RhYԲFpf8.WQ.IKi7:HJlVzG9) Ib* td<50`ySPjR) V. 4F6+cdVԜ1sNA$ED@U n1[1b'Z" -Y{/F A1KBg_d?F=/¢y?7~7M/aE'ʗHhfwTT 2}xWHDCʑK;uk?/N*n~ZmFV 3iyk׼jl#m|Y2PV+X,.4}@R:2S1c;;?W_H0`]_П?~v *0t.JJ%(v.uW?wǿxS{8n]Ed"X|g扉axO"Gks;73cRSo{/"7fy*߹sW\}e45Smx+Qť|]4=t*/| 9&EU≖cVþ*hD jA+Q95SBeL (,t(2R1SVD5,^b##.j(喙.8Dz'hS%@BE0 LgDH \E  #uSQip2VqWU "j_g?J ;402z'OJQ)y.r {τFF& !8S P0yS.+QInD Bjp0fʹ~ JA'#g zA|" CwD!͹π#4Db/(!Pg& fFd.0s4#ٳӜZi- zWK;"RAI?tzٳy,.뮻˪Js|p ?QsFuW'9#( xUK䦛nzÛH}Əfh4>,ˈ{Dvdޠ%&"'M Jy9

eS(sE\}oLRfO+0Uj֪͹zM KP`=bC} +Y@FgN1x~%Z䟎n^T{`L>;YZUz_v1Fe/^ծqx3-Y_(Q#Xlⴓ  FD 0q:FDΑO☍9ydSD4u"@olH`OX39€Fz )2!"s\z+ߴu垉ÎC4w:>[Z E*3eYZrNvuCȐ2*a99Tt bބT XPu3O?Hl|R0D$)6B%p}qȫ{ G3so:x֭."g2p`pp?__ݴ~C7KCa#=twqvvvtt2U\B-)1#1 ;@@gך2=5}Юb;|y , {sh :ڪ[@iO=aANj6|p4BmFYbEIbD RѥʰP cT-~z(`#("1@Dƪ,d"b h.-rVhH,FRw-&"TMsM!ZL-о31 DvcwN9AW2+mmv$H j*ϡХ8=I8("5T3HFjh>Iy"9$ƆL <CP2@;bv]"ȕِX,I׌mIJ ؍0C]|k.\CnG4քj(#{:iT!Jyy;_[PMBѕQbT g"Pb w%w϶f`#}%kpI Sc'xT)x3q'w?&;FFFB/}隵k[F-4u1sg2z淼OOkss(_rKСC֭G|Oĉ9w wCkǕxs5dk.".ys/sF6]SdeN r)S57]R.rRUf{.ؼyu^~6<Ͻ!V+|+\T̬?:q/BU|ۿ[k׬Qa:To~wڸi9r .Piюzf$vYIAƏ4{t8m2jZ[_g:iZKZ Ӂ,yMK<כYȶ]C@$'Y5kfj-\كs߾ڴBӥ& xmĎn1֧sa>7X UCOvҳ~乣Ɵm @5=*hqb;*jfE)VQ Lz̀pF wc ` @fUmP<:07l6pD O8z1oP; /zȰWHMy²VJ[*jT4˳N8i 8d20Fl bZ)P'ؓwz6ȣ:8k֣ӓn# 1Rnʎ#6 ЀC,TFo$*ENc(v"ӝ{Z2|MA-bqP]ݝTg] =Q$%ں鬤=0>9= 2pľ"Jy0%#$3$(FϘ1p]KI߷ SQ2K=hUT*Z]\`롏Հ"v2>>~7BHnԧ>U*J,?~?<ϙ9pD}C w릛njQOH#wOLL0h!\Ǔǥ)KA2g-C7 8?úWƂHNliW TUsdxÄ@.?Yxm2 ,L5SvF_8 9c`$vu~k)BS65*2BxȨ#݃O٥ST0r Ns@X(go+ 4o<>36w֓g:}1E%LfDܨ+.;bIk ?ah Nfǽ'_%!@60U`&$@^xljH 6ljd\`LD@120BE#4u  VQ0gdTTYPn%1VfF7UC \MQ`pqq4AFp75U#f.aAr+ӽnߕ~wq!!۝ngnnS\.4"j.s;BԻ! `tPwQ)3IpHc92BTI-"/9< sF8LM:4ɍ#ԙV7 >vMhtgjSjW¶m G,Zvف8 vENFhm{w vRZ1 04M!Fi/_=ƾlNjC%*p\4s  uCD<:|Νk֬alK~g;v hCC{v 0P??8rf Ec/}񶷽m<93=Q&/#r՗|E*)[mKeG[tpdC;WT9(9GҭW OC*C'\8IuȈf|6}tY>ۚGlY4UƏN2 ˳a(eoF-@\uV=/ɖHLfjkևO&V{$;ꋍɩ9GQΠ;{n2\m'~٩5.8Z313jLG;VJzc6Vtc}ԧX[<ΧDB!w8E/r\ޠ0hs蹓Ց+yk֬Tۏ=ʫmG;>;84{/PQ *ѪuW(tӐ:vl51rq94AbtJgJESmxzɕB7EPo'}qpƱ'uhYbv\xyOqL.Nv$b|H6t^44ը0jt޴2H0U0YFc:GLf!붫^&xw?Ѳ Q90,Q\J3!Z3AZ #جJuܩ1' R:0g; \$'vʈ֝=販 LdV(cŀ牴m!Wz̅-epzG3n4Bg(3=-w'&D(V'0 zfdf H `Ȍ=@ Hb3jyhRZݨ($[Yre{E Y54S0c_@TFPtET$QGQ.$  @ Y *f:f}ft3[WJAqKI%ͺ=x>XA ^qRiBgy.G.DD#,fOj겼!A3@89v}Ïζ21Vɑ4CI5@û^:!|\y2z&: x QDIy8Y{Z{T5,Ie1Y:"NatC׎=9`V923n=y,2jUZ5/xYxvovC.GݴijA^<}Q!w]NDBټ_KR,OVZ$PXM MT{X:0]}S׿~;z6𖷼9WO2Q&`D$f@}^׎ !-SF4vc>ƒ_Kge Znw0ө6y~6>Yw`ím!*nC*jVͩQh&5kV)+:=i%:|pv =Fkdꍠ!tb2߰y*tlز'\;r152̻\Ьi曭Y:}S'ssrw3˃ى990Qjhl;cԘݶd~] J1.\+wq |錿8Y.O{HR)L!k`ʈdHEٸi1>$,`/@}D84" $Q 0REA$kk2Vm?o2Rdv-:㽢HjHPc!wD+*"72)2" "ъ&hldJ+#C5L>IF~J}yr>Fsp$W|@U䁒$)eipQ H,7 \$d \ `2 0DLH:62eٵZ )Kj,c[6čx\hy.E>{'gT61 Z$H TXhÆ}s5c$vLAMK]uf2A4kޛIvTg眈KfVf]IFH M,f `ac1`Yb<Xmla6XZnU]knwq3EfvǓYU7oD{ݽ7~QJwg[C}=A4ШoT޻n{Ӟ}M+5 "/oǁ0^?YO<=xPvd>r~?빔5fϖixV%nCD2b&dVDsF@<:)pwhHkuG/ѻ>7@<Zԟ~bw=! ?H 3ddўz&ZnlxX?tY &3aoGkuk'xs5SdZn6vjt]r7s`&c(J䂗66~ؼcfe¬nyRo@xRm^ֶ i BcL?E=^OV[6f3%: pd"%in郮逕AAb)/)x'C:d`K͓Vo{BS}u<~o.[?o(Ml4b |*QXF!zt,"ŒL(M@P,6p{pSN/~_,U@<|DhRPiHi}Oz׮% J'K%TS BPq^c9/W=o>M6PE RqU!s/vp{ae˔e{m}Be5,xƽ2i3D6ޛ&'nwX1.!27ׇo4&`%aK 8^cް4>[X(kZp룞hdrK])Br0!-d2:}>^q ,hg} Zu)BL;dM[cjvꕋ" @wr3K6tܮ-Gt)^KNpDg'&9N3;y^#ieA?{oZomp'pקǾ[~hvܻ͟o# IDAT=78Opb:4e;WTN"IR"r1"\Q\`,(+鷊\^D0?Ȁ"Xs5=EqOD>л8[®7`yQ&Q`jT+Šd*Y(`O5B(8e U>Q8c ^i3}uu*pWJU6z Np^O5P(@wz;FTx <{+yleE;+!*'wa!QDHkRHB`"Eȑ4"Pnna!QϽںM]2 &.|bN>#ȶ-G~ ;.<.A~.2[G $ œ5H\+t F2 FSCԢ&> 0 [q`*A8+C`m ÚZ-T|X%+#^( 4Remפ|eX6K=֨}I]/xZ;J\#RvӔԄ0U_H2\S?HV:,fr q@R,$9Bzeշs u/G~X:g|'m@˓>O|C?֝S6^?W>b%Oy#;UU4Ν;w_d$Zз~|>v5~Ǧɫb׿+{_ϯ<[n[_;.[x5"'YS7mtXO`J@e{%BTDI*Sl yh/:II@xXW+P | ?{eeQL51 עE`?"g4 8PD֪<fxP܄CتDڇV2Bmi XYvmE/;WD~F<)$``_9e 5Bes%Y(l^~,ݼ֖!cߓǶǡ,B?PΆUıQ49"BDRzUڕ" A@!;hg3`^^_&=caf}4-yU.Tbt+ĩLlRm,QhJ(DF]Q.J{tě_~fq{ZQJVHjFDET+uhRJiHiJ銮_R7_CO#b/"SSSg͵mQR#G>aMJi">E)eLT}"<5 xz}=+8{() s^l޻<я00{D&Z3s ۷l#處ёs׊PƖ|zsQE"haőS|QDUGI7,fT1PŋLiui:ŢVQln:,:cRdb0DB5Vtj#(֤c 5/q(OT>SԩE Aguf;.JH0^:Ū&I" 3N/҇[3 ^[0E@t=^=Ϲ U١Hpt,|O_yoO|KF;>}_]c_{0 ,?v—o黣7_y.{k?u3.{;}eOoxyV>>-nF|3qЬhʉU?/}Eij` ˆ@b@ApcĿ+%T2*CgJRR'%2@ iwR>g=Ğ4%^s%iؾ lǼlH r RtD b`bf X=T -4P+~"W RD j?ζ{EQ^+9/{8@f[$ %BNGRQ!zKB @I s .HQ\5Sj QH[D A,BB`-kX+NێZm_dtKaLI>d.1si-+8Kڠ&D zZswz05"f &V|i`ƀ~NZ%5j䋞Wz3fYGD٣rKss˝v+Ƙnv; 5_QUZm';961^ml=hU6_F?gO|s6S3Z&"a[/{_fHPrGj>[\起Nm?]?p}{FG:}޽[,_a\E{a. V\*`A%Xd=(r ;͋egI@kh<q`º}X\Ł\nD4[Vp nGҲl" F2n.edgq77m=|}hkM@R΁QlcP`NCPvA !r mCUVް0'$yXNqۑCsw5}:"׵D,Dfno, (~;CNF$^}'ǔ@ZjwUti'K>dwᥭ42hUe)+oӏڹOG|՟Ė׽Si㯻Zo{Gv;ERf䳞苞rK,͚-syW 8v@+5=p.{A$RYkRnxZbd  *: ٌ<d@-xhG;D5oKrx׿9 Ǡ$G+׵QDlY5"ޚOs4ePPVL |p/\+*Ti#UAP*KҠc׳l;Qds!Z-%n{p!8 '䁫' 0!Vڸ"4>86JX4:#d J AdAʲ+m]i0>&,$tbM3zGL@nJcU(XUD(G9-sٍEPh[PRX[9⅏y(kޅÇkiQe؜2HB1EuיQI`],Fb< B(Mkr˨֭CB (VoqQz"aMlA <lYض(^ ] 얇oeĘؾMjʹl$# \E" Ĕax*d#iBKUtHNsRzm롯N@"FOLQE^S:4ͭly @2Nj#4#TU$+g> excRϾl'W "vtgw8;޳0U{ݮqʲ(m4 NHԊ0#JDd;InK >8q9ߞkQ0ڝk8F8XsnW(0GBT!0*pO1zYZ;pzގ/C= 4خ֨5s`&n8 (\"])_ĉ6-cQ9C@^(bSXLû]ZUr/ -7?19J=gUը'bw@orLl=-Y>PbAX٥X'-_AT앖dㆍ&+s6mJœyI]#q,'Nqi&h[@ B_) מӮQ9^k \  mۙ !APˆ"yC`ψV_\(S&at{Mt'bDY|)dʚ:Q|i~Y2ڗ\,\(@{h܏1ALk2S6/:問E!;ou&C.V v,Y2 r雾N[c{nmy꯽l+[㫞Տ6x#/[n_>ކyᘾia0f Q*+&$a@!D;TI<|C:,!Wxl#0u5^AW"-U!R_Drf(AK}G M)%j aȰǪYD̸;JqP( U1QHP:foC=t#w<p;jĽ:E}Z{ă a@WJTE>l>[M[K9H"h`;BE@@aŀ*{|N"vi _4Fbtl@qZ -G/4L>o~]8}~5Όټ6f~瑋Qjf7"!Bܷe+ l1uL]Z$&Q΃H$I+L9"bHGB֐O;8Ad "иvuթ,i>8ƄY {佯 :ee"8ta=*% (6 qGh[ILyK:`9BfV3vϑ^gг4fٮ !2f}׏PZ$ !(1™&J@F,uXMI)+9 dsQm:)gkTCtU!҅,ƛ:a!P %:PDʖ6M5WC{D`}475,F{P eМ~t4^hT/Oצ@@HP01 1QU#j4`@Vk\*JiGHsDNjxgQIc; ʙzP"EIQPA>g|WLmUjaFZ=dۺO~;^^oET>aIHb|f++=eut$#7}|b5 ?K]0&w/|;_~տȗߨ075;PzL+޽I{td}fe6( u?*BDo~0W~? kk/_OK^f|Ĕ޳u]BX)NQȌ)̿7SPkxDQ7~I}AI+~%C"E*rݔNcZc$}8hiN F!` Bbf1D DińjR"xA p%ˈ 4:bTTb xSt 㢁F@ !a LH_bH x&A((Wy+Wޡ5+!#jmp?"toz]<gpɜ{0p֦ A8[ ] F' !2+ѡjĊJ?7qjwZySejiS=]*Us$ 9|kTBu6n=e?l=7~_X?nG Kw}[;}׾/WT=Q ^ p׼Uxnsj}ێ2PQ+L :]0ke[O3Q4+i<3X)*V@bԪ_ZQ%)i(AP jv:(0hbjt㲸s.JfP;(; $ܜsb ]# Q>($ `TEH` V^o` AVm͸qJ :T5‰Ш reKT6/Mja$>_~FW8,GZWz4uKP^3(eh3TýiCAQi34ѠÀC;:SBf򇎧9*PvA`Br6[HNQ-=7 Wxuf@i79;&rA1 ( ĈTj#H( ^ hÚ $dECcsSLj/8iA"J2`]=#dO]/{w8;޳luHYW0Wų, 08Co3H%}_/.(D~>:5{`=b{4*XH W"L3nu.G`(hN=hUlݼoIDa4-}o^K veگOe$Η"{qO!tm#6xw`[FTeBg Q׀bE1PdxdƑ&ofYޙS#?:tӊV|Qdw*eaD:"]w^G=?n͚5k;NE3LLLVfI+J"̵Z[oͳxdQ' 5$!WU`" B!Cژ:kMKFS /puk/yzl<_  kR?>rWc_ `֕## "㠔R_6䧜߻{ݻ>3 g޴ӹ*d` ՑN1o Dý p>$YoȖ cs-4FئX1 kgAeL&4:vQvgoQُ֜Xy8y1i Fms{Ǣ)f mV)hӖwM%`a{jJl/ӢY`%+q|{+^% yU Uϻuhi% QEE:v~G:ꦦF~?*pH&w:L |jކKV2v|!rxin\=푗Mck.}m vHj%nxo>/}u(,@h37_3vT>uحk5;ݴz= ބCSf*1]{gg0(Ѥ"}׏S&RdE{y8 {_EڣGnڴ)I8_;vqk׮ݻw3yjWd{T(RJu]A UseZ~?;;o߾=qn6J;cttTD<߽g73ڵk9PDH5ҖX8B$)J8P=8fK`5FtVZ,APjY@VOy/놞GkGe9dtQekֹ÷msgE`P(<H[Rk6مۋHR9A4AuXa!&y񀢪^qhQ7#X)QaehQI\6F\[tQ~+'cG@C- rgw} BBCژ*]Z@%NhQ6X'SfZQ@ 9T]Ps X1±w5y-EJ@{ O ϫp=t:WR u (F P T<+W0`ˆ("AQH$*]FŠHPНxDDH+T@U,`șPh-L, !K$ (HAQE8THOidUα *km(֐+^`)E6-xN}}. % aD #}+UJiH8dFօ9iI\iMuS*C}ud%DdD^^  v'0N@8VDS5b6!SF6f횡#?I-#h&2F++{5Poaze_`xx -h!GW7vTDĂT4Nk];D(jw ;$nJJTM9-'({󓪵 L^h'#3 32BgB~Қ+<:z JQlI"۽tVŃ>4CХRIDD8c췋Y5`,uĈ-S%vO)Ny Rs_hxtOqt|4Z.+XibL' X< "PIp.LmRU`SsI8s:An˷ :+ǟ|f:pҳGWZ. ->0e Q4G,LoM @4AE umq%Z*2 ʹ%w,͵TX cƌ*)brl4)U2pt ɯIx-v@@ҥK.]4kmGEj4Ld?aV;וR8r0 3C뛛9Ͷ͝;W)Z0#$FO@g`1LDP[l-dz0+$!8_!S=Sx1 BTc+J'qo!A}fV(MiyMG6yQ#YDh(?|YU|&OAZj}HzQi)J.=7CUk@fWe8Sa])Un**6EAElw%yMLlil.oSyhIA+I<z~(8lYR!GbHajϞqJ*@xcS{~ELe䩌JHO$ ;iO{ڳʁS*n| ӠDDB؉TR ފh&(@E"(ZTjհ'L̂ AQYIN(H9usPar%Wa*t L|UPG벭rA!X"X28ƶCCΩA%IdBX k‡&_+F d‽3O(@~{!Wg .'qf*в۔Fj-BB$$mN@4A3"Bh?s6E1ٜO*[Z34d5qj]k^?g2;2}$37_MH`;,l{>V'`]7/Yɠiu;vW|?(c޺=RCeYLٖcU1QjZWaF=$B0l(Tzv[Uoj5qz6SO R~dd|X,&IX,5ihhPJ%I2::EuuuqSOuww;SU|>y'w (T,qh u%m4k<ȕ+Wz뭎vs9꯮'IBDJ;Q )UtbfG%+V/Oqozӛ<J:!6#n7I Y*N0hAHiZלG!;,Xo-&JbtLҸ]}sr^K]HdV467:VnkQ)RT'*NM"Z5 8~<ݽma Zv vA$0>AU&LJj05S%QҌ)ԑZ?ڥt\Fv;Q%^hy Bh5X,*u괢ńcfB([{F<U, IL}&m8IvzwҵX}ŋ/E5b X e@ օ\e08 P"L@]WJ0% f_x E,(Ĕ  AшSXk`TY 0Md $QA (aFfOsfP"dfوӬubㄵogVUy?~M'ֳ'Z1m IDATw?̘Dn$_:!hkO"Wk5j=0 u:Λ347EQ̕|kHEMXC: A8Aq&%RYϋHʱ&<Ɔf4CR -D\ixa *hʈPR)1i@A)MeA@8kӚ I1ˀPOJ*L9quQ\OmMX&D&ByZo۶mdddavww ]vQggg\c殮.3L&6mZ>&Ԏ;=w744qZUVAp1KDu֡E566 q,"CCC/7oْ5O*=ɧ{_pm s^ve=X___Xk _oGq(ޅ31rgX;Y7t~i*ӷnKTLkә0[JYr}kuƏ=%A}Z]ir|ٔg+[2~+ߟ2  s c 3zH 5h2A\*6(ʔmQ{|pS3iYe- yۋ[r36oxxc17i}q ;Viksd[ mM3-Qq8-7{Dz r4擒Q!4˹L}I i{8{ Xl^"0V}勿Oj~ͧ&]5Px$q~ƣP3}DI*^f_}L"מ*a\@_tta7˄h@,ab@*P`T }Y$'2d)c@ҘjD[0y l9/ {BkHɂ TY; #XNrBq X O: D jM/z:>zqu٤&Iߋ*gYO <Ϙ] s_'3'D""SV>eNQXv387= PmZea'kkOB= DcqIdR9rgl;}v2cR3D$1S s, JyX!RXFƣZɲPD0# 3sHc (Ad ХMz^֢vi"ch-va"{W_}k0miiUi9wf/f5w;dTc*>Oyag-IS?R$VL6B1fѸ$ ;L IXp6S>2#l`~2lڣV^{\b riF .mKmS= aMA V siqUFclcwr=>mbGS=BKH}a7T֥рm6c%>׵.x*-ǚk(lPe'> $DJT.ǩ򬫡W &Hte96"lJ9*GvE;Wpv^Defb9z"L`su8#XJKszT՛Ö9ab+hʈ0uJ;=B" VQ [dRM6 p>E!"0 1]ϩBQ@l=I"$jo,`pM*{(`-AHy6Ye?C+Bd e+BTW @áqh4"qdMual>SlKlĕKE#@5{:1ؼ}' N d.AV I)'qA|.ܷ p!@ J)T =/1JW]; EZ!Nn(ʳ\Ac6 Z#`LFQ%baC o7 qaZC;(#F**3 I!"5֧0=;7L>ohhl6[__?>> B`B+fܹN&2I1)?=_|GXk]Ϩ766 uvv[X,lZ;22y^X>U=ض:UW]u-x≗\r WsF$("JĤQk1Zj9G;wUͺkQT*7wKXiR{VuK9GRl( As&jl ”yv^$[[z{gs_+5uns\6~sHrCyce]ZG%q,( g,mxў,E+UC6T)SUFL_aF[f|VNLlb/g+I05M+ 8稑-A PnՕl~lP. K{d3Q~^a]q ƲG.oM`;lATlxWqQse}?ֽ+k$G Yf_^Ȫh!047:}΅dp|d?x)ة<01{VU^}WgTeor"`X&^uwHQjkA;#F\pеsTb̄$D #XttIٙ+($C#"lFS,}Po Ac5WM-i&,-7K. LDEX3#rCm[iγr@ ڽ򍞧B+O퓍r 4͵qzڼֳc9C&L=եbɲ oyZ|AO{Y|3` #QջNfq}EԳ஑qrDFT9"`@kW)WHif ײ>k9Qhd}͍L9T,^xҨ(P#IOڒ9D'ζ3V߆q@)ψ d*FKk %^2vAy}QDe`RFaPB,+7[QEp.0Iݑ1^x^Xklllojj EQfsl6n:7Y)dɒ5k޽;+Wqv#Xk0tX k)Ĵi*JooݻEdܹuvvq?00`3{T ^z饇/8W멧o~g~i:$I\~8ZJ:kʔ)U5W|ܤuٖ[0rǟK}]\Ck*}`-)Q*–4Q:ЫǥL'MGW288Ga kn9_<4ޑ GpﴠF+*niW9]r|Eaypi{<a ;elCǛ35)й΂ӧ=75Vfaۇm[+PoPᐊvoo߭@ӻ;/#d"%ia``G7Q$šrPp+3<@wiI֊H{ܮ:o|xM]s6lzaؠpξ7D]IC[{*гl9y-b$oԇ|~{NeFKҗźӚ=vn_LW7{V[-EaÌium;c6c(TG[=ui}iuAgjZsgNƭnX[ #uN3/",q&l q>F?|޼+n^S(?u?_g X(jN#"%l:|<徯5 T82'l7t8WkiJzP|5h `ht۶M߳['rqrp/8oI~ΕW_rl=/YGwpvY9o5gɦM17e <ٳz;xL=+ήh8{_qo~Ո.<#w\ջf_޿Z-焥 g3y۞hF\x8 tz앦] /kc:WQsP1`ΠJ{KmPXjZPVD+ŠnQɧcUbv5e>ż &Rz`TUdjU5A@גЀhm,UyrU*F^56WC3H/ơqhZeqtGDB w~wAXD1 ,hX1%@ Y 3#F:6AJ2["dzٹhnF@c%5#@ߏ*<$6䅀 J#_+Ok;m/'GSҹ 4iV&؄RF`qdJy1V0BX@%\m(}'[$n~+ ] \ALt?\MMMnyQɵR;*xq'I! HP(رRDQZʕ+ھ>K0-[Ѽy̙X駟H.رcXSSST+Q^SEg=5\ݿkW/|۟7zɢN㑟ů} w4"d,#+c'-8w%+.D`?vwFT@PˢKnϿ_|=WYvpǴΘo0cg?YX`!/@˷;ٙ=K]t[羴sx[\7)x,BUF{ؼ?;o߿sW-m8,1G|דw >pç1g]W-MNsL &H =zꇮ?n+Ο>vά ts)M[IXaxuoqGv=}`^VMWncUsL?~uGoIZ`k$~Δ'?lK; s՝I9 .mz+O8a/_ھwaA v5)m Xɩ'J T5ztR-uـ{_(J11KA.M< (t\AM&AK5hcKg]e@%VNYADiVd$RUJޤPf3Vp{'yޑC84uSSSKKf"b ff[U} cMW( X]۩bAFr@rʸ. >7' Ԕk$%RÅG1xr^ ϐ0h͠#ULrp6g vle`8 bX$% aHıՁטJbae&@21,ʚ)"etY*0 j NgZ֮گn#8IM$/[l۶m'tPOOOP=s׼v&J)kؘ#͇a;r&q_ްaݻ[ZZƻjGqOĩ9oF1cFWWΝ;lْ /ˈ}m۶͞=T,Yk 0!_>{?w%P#[o5W^}WG J@|ћ/7?;mΗY̛θ/ yX8n׹˖E@4u|u;u,8b;.?y'G,=??Rr?MD HQkΫ>|y)-d2+yxڹG.XwMo~+)*$&g#H7͟oUcaY,6DaYPk7m,7PdI<\,bH*'CWaD@ Z-E$An,K l4١I)588裏q|^k Vkd]s'XUeڛ^p?~}_zǮ3ݸm=ÈXo7s~׿أ񓟿{/T<q]9__~%g")Lo>G^w<3/_,=VmxszӜ|tO/+xO|jS w^‡Xʫk6\Z쎵׆+dzr B^ƩXQ1Z*$D5{c%?)C-WQTN8e@_cEAda IђVOURw\&XXPQΨ se.8F@z3&ZFgEԝ%v L) xKA~Jl?oMAv1NK7>lqUc :kjc^u1+soc, g|;7XqI =\@@!*\Aaa6sܢf?Ԋʁ Z]^ZcO5db'>!ҩIܛ S91V+ !Q* ) @ZQWG ,h0.ULX Ϳx%6e`F  4[Ѐ(hF"eA xBʆ' Yx"IDĘJ̀S@ D BA IfٹsEt2=.YG-^ <ޤHZk0 R]]]{ַr)ͭSN=:::;ljw8c0}"k'OvGpF&fL~ll,h?믿{Y`yM&"---3fhiiADcuj2V%;( |g6}ص~횧ME',{# /|vX>t@W˞Kl?叇ن=={9]5szk̛䂖Y/|Pφq>cb3j]|-O?УB |cOJ6,:;_z#@z;h O>O}ĉsgO1L315faO?nwӠ '@AK;.ykקЏ~vygkMʃ-BMP0gGhnvlnꜩm .XphͣÝ}t෿CMWZy*06[I],d]{Ty/շXaz"hA/N( "*"#@#h\Sr1(V0JH:+ N~ A:?'y"f`By~BZ(X"D!@%BTXu+bwH N=4C8ضҊ\M b7 탑$ a@e+iil[ W$"˖ݞ,\8! 2Yt<8'ϢIYR+V|>'.1CZK̢۔k-rc]|v˜ VG(+hR2몟H=kD"bk:^F2*-^&)$VX`r9"e,Ilԃ8ZySq"vsPן~vD,J8}ƂkOO=ٴeЂA6pq}wtn^sG_cs'eս} ~s酟8yѹ\PA_ U<=hrO[?ᆬ~y|7yb{\4WOǮ~r~p|M ]|SޗhH;Mo&_;,߽[=CStk{C32o*5#Z֤vNeTD$A؉lj$T ]RSD_SYzD@^j 'rnHVB9O\W`I_6x$cjNy1k12)z V Ⱥu1b=P{}9D&X@VecơqhTwv8͚k #bE1H åJ% W잸*BP!*!$4bY +YZ>%lZ r4Ʌ8c`%նԒJ#g50aXSzM~Oq(DYNIVdDAPI1 z e1_L*RrT3t XJ5Y#F[VxjnN楡!Մ\dy^$vm۷oGķfr1EQjժ:T* h8C5k̚587nܸhѢ 19f{zzvuYgyg:ujOOO2Ng?~w5ի_WwL`/\nŋwqP,4??x=|uuu2qI@W~QY?Rhٽi+^W0ya}Ks?Phi^_x~mS:z*=hV߷ijۂӎPgK,|FG뚇(Բe-]Ï\vҋt\Ҭ@oЂywl37ֽfM vÏ%d4٣nCs70ҖϏ qӢE S@8yyGޜz3̶-8-Qє˄s6m[5s{/u;ʹ4dGF0ǣwkuk"37?oXy s|߭f?:V~h3?{c? <A!'~oz3}o1|E( n~ׯHia@-X/Q /#  Ib,rB ar9.==f4H37$!!싍ml$`G۟A{8?la3`! H}H/w[uGuF2 qǪ?x};Uu@", ɲunv2U'&'q/-H lRjT8cj!V0alEwX$͐rV /QBk@'{uPٞ- DD¬}q5wI>xB,lcMG ("2R.860l6(Ef'j&KwN@&ʖ*mryJK x0vpRlQ, jgQX𠙈(@TŎ @Jr1.W&$k|R4ذT(Vwe؜ >c)$+X$T!žl15X M5Ebbȶ6$g +ZӱXQ*@Zr}O#եR)r\*w'OvHvccG>,]rOw^xDhD$iӦ-^AyMTs?ʝf_ǡ߽{ڵkz!"po/wuE]e˖m۶kM${qM^+VX14| Wo V!|w'52COl߷wɘb2DI.L%̶r+*d3D]=f  aɚ0(?nQyAjAP ̀U픨t_jU&Ǘ\rIOOϪU@= db_TD}{8%˧ v`־B/U*ݞ+e:_ &ՕX,w߳xi;%X8="]ʹSLn,7 _6oѷ'O}]edtUT2Lvs.=yũPk{F1L*562s_{v(9Bl`_K/z4sOغeߎm;AM^-[]eKW oZdkAD]s;J"ZlսK'*J}]=v_{wFPȜYF Xw|˸ܱp>A/]f5֊i~{UXQ U `Ȋ"܎vAWLYMj)fbYV;",JhUb.+)G&ΔY#O([L&2yaubQj于F31{ʺ1 -{r9ϲglXXA&Wȕ Jqp`t(bm,U 5k  Hl?0[A&TSI8znBMp2X6wpnS~sז^Og*6ơ>ӿ_ܺMe ='.ݼst4y^\sQ!wܒݟ͏k>sxֽ#Ã#\:gܽv[,ävc۱צa;6l\1޺sisWjB;n;m)6LmKR;г1~y+@Vx߿3808 Ro0xd5}#6b&gW)2 E(W 㲲ܔ0*o@ 2U5,p51(v/E>a` XeOKSԀd- ;꓀*Q@,"302( &>xAqDwBT<O)< A棃kkxm867+Z庺l6 Fn  (Ӊ,Px㮽JlE DN IN [pAP$elXˈEB}B!hfb#z#(M(Cm)M_Ч$, IDATqBBOkt@@'w\)*0:(O L%<b6!'2[%x`8ќI^HHZT20(Fnد = r}}}E͎*3ɘH$(g?ٵ^'J>8/^uwGyd\\.o{޼y ,شiS.s33. a87j"K^ko߾[nsG>Xp_n߾<_v۶m۶m[Tjmm>iӦL&L&ߜH3n_×:ضnڌiS9BZzݽug߶yxԅsgA[c3?-lZu84ȩu|߿ 7V͗R"jt(0tWtAaB`Rو"U%B>د0 "8 =iji@ X)WP*s"A0qd)8:Ym| t;2TmE]!;Hz "(Qٲ "*BKc8ʡ˖-OxiI[c E"OUbjűA '%q=M ,BV=WGme@R6&6 b_+'2űWHp9A)DkE" qxe T_THˁGVDj#e-JG@![+'R1xJk`%6dcP ĂVɠEA``i(+26& (=q ;sz1c=6k֬H. wy]]xӳqO}S۷o/| .,%kv͚5W^t練tvv\=7Mu?E644\r%ڒE]ۻdɒ;=7mڴ| l/};wvf^ti]]c=v5 ׌xD=;4X۾_O[u?t;};'Yhk G~;YmSZ\tտݷ/  7>e敗Pa ۟JR'wum9w̜v(e WnE?@CU º>ws`_;.յw (F/^|3.͝'v7ݷoxwSc]wæM{v\xR۾ f¢[w7}':}t`{8Y]rQ3Ww^ß|ggPX8qrܝey{"1ցV- dtC}T@R)ylh:ך`o?QTH! kz'qU^;z"}T"H&BRqN~bJ%jSOuR-[޽/HLzC81Ly7'|ZdɒsZ3˟x≮|>?{󼾾O|r)柺w@ݬeWyw~],[a$@t7ۙU9ӗdF L%6-{ ӖДP3[`r6>+.?nq@oyr6fh7]Nr[,|yOylYPP,oWnӧN6}FoZ>phk"Y7oƴų+ۃ,I D?'ڎk`,Vl.36(F-[=U M=J| s"{acR( AXE# ! "a`-BV @ ~D` u)y,o?6?Ľg{=|I*ow*ghA$ <"X$cH9&rzmf5I[ad$v"U#3[DaؒV$`HJ t 3 H"dQ 5H4Z8[ПU1 D,a21:ɀVeBVZҡ1(܎)e(M$F6% P Tr'&9c7:>;<y''o{㎑={477_wuw߶mO:0 9z7nr뮻ىNQD{i͞=={mSO|ORQmݺ5xK.=s;;;Νa[[[OOO}}}\>V8jL;U"Hwr~m;v'SN\tJKkK>^o#c1y7|Cg?zǥ쳛[瘲9 }Gc0iֺ{e5{b[/<夕6v{f醺 ο ,2 >_A>TUPF:8LJ1G 8ޢ׼$ොKD͒#"U(#(r utur5*@րSGdq:0U )D@@Fqߤ gfzj5n1s<=r*ǀ 5$+Z[T MmUii9s/br`?#8 vxU @Շj$j_+(op?r]86UM7ݔH$rɓ`y bĕ.q*N @) #a"%tY)UNr+X*w @ iRB9"P+\&/"&!, C_>oZ[\mO#e^DL5IΤ@M̄bbH8A$` U| RattK/rWX122rGyNk֬y:V?є3 9s$8mя~tƌ=UW]5oAV_"92dZmS'A&>zXju 0uTg4eʔ|T*͜9sժU7of~{u*W 0͘1#Ũϼ_b28+5C Il~r`S[ IRNͩo[4M7h TJaEձX(b'7DDJ'We@$ : ؀{wݿ0 @4j,働JDZeA4րB RȿnNnlDʕrUTl`; 4XeS`-2 0(],J7]nR˔8T5ΖW~f#~`UC_}ٯ֘J<THhTٌ3+Az(A+eAAd@pʮU p\Auָ@^W04#oX%dyp+F)u^ #?Ғ)'kb 9: 9K*$TfXEPTyE!q5aD'( T Vq!u3#mo>u"_AUVj(\qqr&áZ2,L@"$A"2"$Xi`*_%x`,;5X P"( BL1`_QDlo{ , kRHH+'J1[,ZcP,HB hQ"MƊ($ |詄`u I)@A-V"+5X9$y՛O^x8/L&3e׆YfZf9/|k/~W\q,^_˿oq՗\rɚ5k^z饁:~o|@@DlP)`:TZe(q&-Aj76Chir_Ǣ0.oc–X+e" QlIyKP+_{:fJ$`S*+D0sn( *3wDA_KdF)@(ɷϲ[H>JdlJcgZ"KgGgx4f{;wN]r"כMM6l2#3e*s o {l3q}sɧag7]{hĺTR#>23!XaWrtL` QDUhr&{voDbQVC>Ċ5 7~@ f!rAT`PAȞG+96e"&`њAʶ9+k#NJ"3J *B"12apG}'~_׏N:::vؑ>^xW^2eʦMfΜyYg%/}K>scNmG?:?q|4DLOw.P,ʳ:g;w֭[}|ꩧ>s˖-Y`79s|ST*|>{Μ9k}kl6La7Yrúi?Kv@ *xhDӍQ%D kT9c!wTS F*ЁG%SrǤ㼤'͍6ɴ_x8]מ+KnzV'0 u^K1yhI d+:4;/5cwe`_! i>3ۿ{,9!4Q ;7%D =#D\ORz ̜\ӐlGu:~Ir`@NWLc*Ecz!^]?! ^N-c idsxFP禯p1Vd'vauĀeN?BAj 5"Xv ) :E#8< 1 L5;nGCi1L*VXMCٺASʠ HN5"9_N0`ZKU vd}:"u+ӱ*qӯɘV5ec*s-(|F-sCseR@veN9hXwADQDF,a5G֊VH9С!B a_S{ZيyR4 ")s$@ †E[a&$BK" &i *_,YN^B DJ)Dքl"1gekE@Z,r5 "0J\ACkV)aߡ- \to}s[nmll `ttthh(ͺ\.#ʕ+1͟'R'OvO IDATѯ~l6J}K͛-Zl2Կ˿dŋϝ;sŊ7nlkk[dɓ]C=]*|衇.d2)ު՘SqWWoou8TODNfڵƘ-[̝;wǎ'pΝ;ϟg"2k֬n?f׭[_f͛{SN>Ik}Hw԰޳Td"ocQooheim9OJf,n@/r/݄ reIP|Tjʴd2yI.6 °Rhqfo Evɉͅ0HG3H 0NƒI%Så!S*cfL914k)NLۗ j_faܿ^ (+o ƺY {$$9. "l8> 8k?n.6An["C^aոr)?6-fI_ㅯypqo8,C'蘼ޛlTefÎ&$AE XtN UQI8 Uh]^%͠ zOԂX"_#ӢcuP;3HPZEvZk$lqS#Pmw[uoщZ4 ( x;SBOԃc=6'HQG"Eٲ,qULӺ.#`0*! S%dTI Vp}825`ي5 !&$E3)Pآ(mPR@>.,FCRg12eaQ P!LX[lY2ۆ'MH$d<5V΄+ed16b1P?ɖR;\x3u88V޿nLŴ+ 0N6%Ô+LvQůf1Vv#A*qÞMc(*Lk{^*5ԧbS"5g$5NTdƢ2MhߺBL`h ّA܁RXIG76EOQ^n19% bn@TRp(5^?Xq:6~3_棈bsY1)J9DK`TUWwT9Z+I~`ږ SIP ʘ6,{Q^ȔxP`B XD4+Sxڸ:F{20 &%"UUpZM',B< <*1俉8H=86c}D]D,"E ovc-kw_Tq50:0E!XCÃjőX(F,*Jq"ueGRD yHd@M'J`GV+k|-aBh٢h,(% P\QZ)KNbt ^ܝWBHiQ$1Ǿ1&tZkJ(6KF:hl=0q`d\V?)Hb*LVⲧ)7 j5h8Ât: Z떖8| 3wwwaq\];G'|rڵTzF}T( 4iR}eN 8_|qǡ8'Ȏ&BsI'}+_ַя~O޽{3N=5ktww_^)H$.;'\[nQJe>cℿw`WuW6%AEbťLsy$ES3cf=buz=V޼9:H  L\6a ?u8xhG jG+ ^h aa[Rͼt=?sKʔe3r=8uk=|_@ $z8Z鞔*OsLf f9bZ\,X#!5YcFee {VHԗǞ4FrcGU +47*L"!HMd BpMJ%t0LeUMy^{vQIH `@ZFT^L C[׽ԩ$?m&դE$ƸBH5htĉ}{⧟~hb9K 檪1z[??_^tΞ={ʕcҼ߾ ^ m|#}4v? >'N8rpXU Bh?kkku]uڛռ֖#_ÇW?S_ww|TI%b}a8hKwK˹+=!Kag_N% 󷫷{ڇ3J~9U% ],y|o >]07د3K_V=ᝆL(:όw[z|/,c6'l\ϖ7Yq!Sl7֕g\WwtSC|bFo~h?isnwfm_2"23&bf[O+`#z_uת˧WɥkhMjn)r8Ώ56C9 )qឩ#?;=3[T} z^WN>X$T0bwI_LhK }C'By6T62/m5l]#_"@gA`_$cKy0HDEf cH3,eR^!"I:ޮZ>}J^ҚЕc]R"]Je޳ Oxc/Ĕjd S"X촬`٥{8E  f<,.( Q\L J0 Ƭ, F`] Yor3E W槏 g;Z9B:* z"B嘕nYR%/t8Pb ɋѱ˲pB'Vz] E155nR>.O367Ӈe=tʍ7x<'&&Ny3eo'QYQOY8/ٕ~wP*ePv*=T>Ϳk !cUrKy8nJW;ǥ_S{^gr(Y^~뀯'?? Ce"F@ ԌDŽT@0FdjL@bRMi*j`` &f(,#[Q'.DGdoU|@l·*k_צTd4i{XU*)$sZK&gh,C4CM )P#*.z{ E08@{nÁ9Ll5APD#!8.%!pԈl1ҳPLa!aOM]s2@TX7&G/6`8 ꂋsckK;p@C&ݢ| Cr;Fdۣ! {vbfNFкkW9vgb&hS cëGPB)(5Sʒ]re{{{yyymmԩSu]'G>w @?EQ TO$BHjxޖ WW~EDTu0={zi.\h*;at}尧UM4χB6vΥV˗O>}ԩgyɓSSSˇ>}OZ=gQ |OWw^xǂ߫=02 )" !"1 I2X=__٢x k+0CCPM@P@AZtYQȿ Wd_u8 nX@.5Ĉ(M6l7_4RG8;*bʀ(7}Ad3$RDb7)@{#oW?:Uz~x\}ǽ\gD3q8"Q`pC@f4AD@@_9e8BLm& (!QYQ|ThbF*206YD+9q.ϯ!&we\UU =t;܍#`S5e4BQ2 K C-a&fCi2.4 ɨ[ vR"@׽;e8\ri,q38pI3g8_nB8~|cxbo٧xk">wԩ`/}}O}Sۿ00.]o~'|{lqq`011;;;'O\]]M$,S}0$%Oa?)-TUw}7?D433sС[y:`0B.\H/b4;#1?~$UQ#?uYƘM.)dSH9wzssMe@!%7oۘfE# zepͨ&V!ď`+˗kYq3RFZ J ZmS+p G-5a&py~ k$ >Q6u3 CieP̔H0`sd1"I\ʼ?О Q"ƺ=d\z杒O<*5z?^ǫ>F z9ZB&6 HudCx/&J$UӢa*m'77JטB"zWgX` PK(jY$"TD+b"'1*3pl*j )A@##2R#QV1%CrBUߧ'`",^ruyvp0[jVS ^-h r@P-SqRbI Q HɥkE"02ftH1FTGdН2JlT#D%R̀-F% A# MwcV&uCQL&snqTBbFj@fj61 "6Jp4sؐu&r88==(3a4_Cw|hO>iZVknnre"&[oujjk_}ݗzԩ|#EQ$`G=}ξw?_zYt΍F}~ssszzzeeG|fwMNN;v{_ŝwޙS}4:ujq\n^Xz!"֊(̖o$~N[o=}tZ+X__Oۼ{~{3Օ쁿kq+G~dz=V9{0QwG;p{$7/0PһΆ?xsD5%@5@4"(D_ߎ1:43FdcUƮpd Z D5u0 IL1펙1"&̵`Onggg[Dۅ=Ŧi~,G}4UC'=<oqccs#<>a?2)=9xǏ{ソGGG;777ϟ?077/~o~s"omm!"^xqrrr8pٳgO<)"Y%Jɲ,\#gggash<LSU-Μ9}O'677;|0ey]wo\3?3++ˉ֏p5=,'kȳ561k_>?YͅX i>f9cX~v8}cYx`󆕷#d}llqwDA v{7D@lϝ0waglsa0qj\r {m=w9tjcpysahNQ 1g_|[:Bt L~7dL/ZJ!'~s7XL>@3Do]f0jviZűӻ^TDrdSRSg)2<`CU IDATA`fµ ̜!YakBCRD4E%D2A3^ p TMh.j ̓yYF5^sRL9# QVDd()PLRX!# jՋ憻iFNIU]{&_dv]o]wFjS0Pfc8I Րv4sٞarIG0@@Dv Ĩ:KEIE "H0423V@.C5hAAAĠ""2 %25Ei@Uc@5D2N2\nס!#Bdg!`@!E H_{B̂8$&g@͠kz.zo}[nRSRFV eYE"/--y+++7|se1iTp8Loɓ_җ>zֹs~y8>s333~uuCI p}OԴFpk0@gL)IBr?jbZsޞ*FKJ)F u 42ϜgAnۣB6hDfXO2֚چ*!1d,ϲ[5-wY۽; nZ<,˥TY1v~p}}}qq{/"G뮻Ff Ν;rh4|zzzrr[~ҥ,1DڙJ4jR}0Z>rQmfwk@oQ'P"5 f[=FWs`D8[Gz,P$ˀ$}}5ժP]`Š2Ѫ4֡(it:ygƟ[o/'.y }nzꩧO>]>~M({x߳㛽c߿Pf܏ZCԛXg`;q*_:\vV/2w 5RguwwB- F[j1+n-vjнRqFq]ǣbB!tգQZƨ RRL!pe:$[;Lwҙ:.t:k2HfUFݲźI  MjgP쥸.+?' |^nmPۆC5ec<A!8DmڄF mueU;MPndn킞RABR #rY@ !3"Ә<y˜vU*^nbW~^x_(N{Id.`_9ZE$5($$@#4T$23eF"{ n'ܴC[a !ZTTDeGށZ? r Ƅ \ہu 3 Y 9&u`LX jMȃJm,+zBZcF1BjjC%b&;cA5Єs-iSED j!Ji/`z/b )-ummmkkөn>:NzK`7!hO{c'>$ {2ax`4g?<ϋx'ӧ?nmjjjww,--}_nEQuh4SrIQ<*;4HoSw&=3 677Ϟ=s?sg>o{n'xj9 /2=~17%0elM9T"',fAٷn31&S`}YEKHQ5ߔ T֚7"p:Tj0Lm?YlKx Y0R plwu85_PɹS>+ @D0,0\'BC 61YBuY[|̌'uLJ^9vb2{].-U;?{{| _{,:bmnM" '#`6i3(CȟЍ;8/ZEb1EϹyS:*a Z"l7ѡ[þVA|>Fm;:J VXa]SXf8CK 0YE;fĶD%@n)FÝf`d)irot*h_U;={9v"#-:{%hst(÷0u]oiO/5x`%W@Mw>?ֲs^U!sF:Qy ,\16vV bQC @&!x{@4P-F$D#&1Ye3C2 #t1xfv+C2@U͈FfM>QL8jaqm;+ QTq SmnG^-[Nr~nȨ+-<#aDn{݋b(8"GI8nPB*1ay|;*F\]3c+W$ɔhiqbbb&t4s.777EJXDIn7tz"$S.\X+i/^8z.3Eq=lnn6MeY:ʲFeY@ 'v{Bf̉!c D,̙3)?i~>wܱ~ԩ}k[[[/__ſ˿񵵵#GxÉ/^ӡOR5Bgkj͈0I]b+vmX0E/s:p^"S.@Y$c%Rhk̈́ĵ{!,[فD>S+nzo] 3yNfǶJVX Lf)1DN0L86y@izj [@h^:=_JGD$p cA}EأgMT_{0~{:ǧRyy"$"Yuog x|׳' '" FZ ?fqBd]^cY`d%~FT&%s>Ίh2p@.@8lU/ gGC}X6 '0Od`HdzE!\F V l"GRoY~teAu'ѹ B瑟'dg .`iݺ#+-dd,#PU<{7\צ^o9UtR+ &_ %:;\pZdU`ƫp#F ̊ie9٢y4G{k+G9Bɘwڕr&&NyB#0GD"/\Pf`3%  :"(VMqrΐEb UCk. UVFy9֮mY Z EiPpAN?gEMAր+iIW!5=iֽ}x%}_ȔW~2oa0P+7td@Y-I#"&42mطpnC$QR(McuoQt*ȌVY%bY5&J˻cigIeLOtrfiN,DAJ&60LmW3&#mn$E Fs]ս< 9T @F`티œyO{x7őZ4P}_gY &iyLፍ }zH2UUl:Ι3gΟ?t8cFNys.Ƙ y'*h4*oI@MeEQ{IyN'y~ӟ||I;#lmmF٧~_G?d"yÇ;wn4s/KsKnmb>pi.Q1~7l8Ovl{q}*Sh{ajO]VeGB1%F2.&,27 @h`gr˄22JD$G[\NRe1*;G^R^x2i/οKI|iڙ/ʼoc"`QA)Ԭh5`DcBjL)+QJ02@t Ґ!FCըJW2@m3m[/6C岝8N `+c 7v3%U@`&oYv*"8HQk2DJEH@42{5ű#Q 5DTʃcr%Y^nRǗd_Q;?x>}Ლfx,S7v(.fȓcnQQ+ѪiyoDϲW&kTnfrr21LIx,c!htرO~ozӛz꩕!\xyWWWEDNTOvmcg?Y%Gft:ιp"}9u>}C'@8q`0W jΜ9󖷼[n !X42ucvQ5{(˝B+[>ԞFwz^[]m'5@56S@H YDP;[.:G)ith[C’E l7Zod{яU' ~ޝo hÌt$\y `L-?1k-З[Kdx#ĥ= X;|\uo_״pܹ7'ow~3Ie,SӅ oΛISiaaa!3}nZHD\IMgW$$N]o8я~~~pOgYv?Or-a٨ IDAT[[[Nw}^ph4VUIε^M LxG;kSCjEpm}IԼvŃ)z$C"Eѭf~#m3IMX#%=gGj²rA 4CCo^y{݄)Z[,$c>uos`U]ˁzyU'xoj/զ]-,`,a6, 4 Lfi}Ƨ@zp c,mݖ,˖eY/_nTl4~!)+ffė_FM fR@:;_,<%ଂ!X2w=^[_+7.Y>),"7u fv+ϼh凜#p%iUx/W4ޫkJ$ 1 | :8 !Y(H HP:W<졷~쳟yO}w%=|^uwL祉M}o=wG-:űKnT!N. uJ 0 1 @?.!p Օ@@%i KKf\/ 7fiRS~@FZwEow 3vdPE@҈Rr @ơ6bIcsenwrˆL-@].2t6m3U(y@~%:!2gwF؛B@WE (,\ hdxq-ӽ(HU#$Xk/eR\08VIıE&䞹V-1@k#W Җqp|$F%(롸RAc o{n CDJr=tݩI$|-s1nw#hH׋4 |x#q<|S ꠝ;w:t:tӟt0n_,4 2X|ʏvؼCJU9Nze@V?T]ԋvJ~P  ]^+:G%8b@cX?qu? =BY-i)ui^KOzދ@ {{['-rY eK&@̴vGx:{WxO\|7CXicn4xldrO+'#mH1Dd.:>qMwE  a/x/:JWz|\\18^PqśY[rXH \fRADѯxpn,DCD?Et^Y]Y}=3O'e7[z>Oy|?_Y95ݿ{vnٳsǟVq>;ӧ:4vÍ'?e#$ 0yWHFBA#`i\PVbݹP"'|CKfl'0I1x3yr^5 m(2h$+Jˮ1VI&5q:V q*"?}ծ]o 8"(hѲe Wf߱kA+9ZƲ$yw*v7 0F^'Q_zL̔b,Fb#djJ0Eͤ*;G%!EIc*E,WA#"zD4R4 9p_Q* Y%n;[US:` TD{ЊH0@PoHq+Z|啯|eRi4N'ܺukV4Mwޝ_%fs΅1&o~n 9+ =YyAmC$Mӧz]zѣGGhyvO<E=366p ᄡZigG5, G^1Rf?yre/a  ҊeV]qX*˕,"'ڰ<+T)4zdkNe} +?mFrS/xa,Ke4[S[4;xY+k Uɉהϒ"kJe(ElrD=wRcF8V P4e PA틨4eYHնhl 0>H/ |xG^I1q?'MO}sT޶mkn3s'O>w7{Μ~u7|վ?}h E* Ⱥq0u Ċ,l4hQOxe7fl ]p0 7IN- EWﺩHOJxv1hR?JҺS"LS$(Ⱦ-xN餰V2 C*2*m_QM*"ex1ywKqKB"`cq$%#Op[7xvGb1Ҽ )I>lMu6Q5V=a~@@8w=@bqiu㯜vTJ2˲>Iv}fyٰg8qD^VAQU՗%R[5==rO.sιx'^ZE6Iz2eY:VWW׾w_Q`l۶m޽GI$ m ?~B@r_]R|A؋$bVkuj7VMum~mr]Օ:BHT "2e~Z3>k[g T7$*M+!OǽtAU{Z;kO4Ⲵd3klmlTw:͚LZvuѱvo=%5 <3z(rRk..[[(h5]$)dz(r61Ft_BǠd_\UFyt_xj-JE&AMS(!| Pwk>swokvW~]SzQ|׭XyG}^U啥ly4-ۚbU7ٻL;7sÍ7=~([ʖWMmSg\>yZiRʢ>oF~! m@T[W变TvыODDvynrmo%=Js73)1L M&Dʲ<"D+-0NJlU$% \#;a#֊u9\lB}Wm,-mulE@RYVZp޲,LT+ b!qw +@TxLE{ 2x`^Y^Ó*ιN#|wwe!^D>66y[ZZ2{׻;򖷈]w}zU@ !v?pDZ1&˲$IVVVښn<~xÇC~RΞf"J쌩y29Qe@S3unD}6jI"1y0N*Tq_,c!Ye,D3i \%/`k.jEMvu[ilgr<62ODszmW.~29ɦו풗sV"W9 DT5ԛ5)o|U&̹4&l5P֠VDY4@5uD+/8G!d4߼Kz]""^pDλ ":( )۸XՐ|hޙ2dI)w%;zS_ =lݻxq<ٯ\t矟4z{?;T>ן޾uyWɧxi XxJ;]xEZK9'5xAF >(IZ}l!]<0QxIxuEv]1ֺp{|%mf',!bP@PZ ^7m~-Q,B !\N.۶EĨ1}>?MJzl9n[[#ƔDgZ\f|m5Q3v2gE[鬁WQXQJD1H^ 3(N" ql<[V(1ZD"Z8f@Zm[tUaDk}w/E   < `=أ"J#@r GD0zeg7<j@Ç, VJNz[:??8OMM}G8qbrrrvvjuݰU43<ۆQ3xPZ~aaVZ]vu:kj8qٳ>M<baa.Z"^PFOD|Z-wdK.RQ6[]'M]ړ2DuqeWpbL'Szə2QIncNT- ]N[êC4i鹮inxַLԵyKK'v??]7;; ПSO|C5tiuSG_}wdw?z5y~czÐC-A3llш`nث $hAH/O3\;pxC6ǻ9onf~Dm>O _lĀ!"vm /LhJV/ Rqϯ\3j[MY)9P5MEYnsrY*HY)N{UGO^0iqShK-1NifZ?Z2h?v~NzMF{0Yr LJsRlc(\qÉ.W6 I!ydR+QCŐV@x+B&hJJ'NhDQ%L7 ;p."I`\D&a 4vA6Z`v*3"oC푄fdGc  vv333N?W*b[nY]]vs{8*8CU@8R~Qq(t1k~zf3x2N'^7111mY\`|G:'dky}1c5Xb+N-qdIZ]TGxj]qdqbK''1[o'4.4kř8=tTOؾ2gl-+ei eݳډ.7ZT3̵澸߫jS{M.YB⤚^r@Ù<ϟ|; ,8=,[Z}hϞ=;w `9777۳gϭz]wOa|ZTj$IR`fݮASTصkz۝nAIEl6Ie4ҕ?WD뒃j(۽ /;_(붊'ʲfeqo)6MnyFͩJ[ ]r)[=MLLNk5h3sӖ)ۜŬ^GۑLcSȾ s'kY?lz[مSM:~Xsk[ײ;yy7({Xulq-5~92.4'`Xtn>;j<߲eTi$'gBIk(1Q|+'WX~9;PpXx #r$qdvx Ϗbio_@rtAZ( P@  MD%(L,hs2aD B6۾7_ϩVIՄ"L9@=LrsI gmw59엧vl[;/?kc_|3鵷]ȗoV* `kRf}U MY@^fg(1sōsmo I=Xe|ydcMi('F*4GB_8bd"^;0r}ʈV3E${p[%(e&&B`+ZׁWo)~r<m+FhB@yA H[@LEy`#%ZrdB@cBt@i_ I PlKH)2B (>u.m]oT^(T,E^X$ 1XcnЈ@iQjXe˲ v+F~n$I Q8 lx0`v(E%#Y IDATxyMWu#aT4a V0 ìM$A|*yD# q8>䠈QE^O~/7F#7E/W*"m:L ؊Em/Vi>Jefm3}zfΝ?xCYʎ;&ѱHҳQ'3"82 38@@(2?olPLm4,9D]ZJ`p싨ց *@DdJܾ}ge9OV3vacQJ\b>e==|5iVQH|F=}MSݼC|6yZXQ_?VzHEEY"`Xt(VdD-Zn/ E8A@Ù)x›s/!eZ[b!cX#u E(fπ8G^ۃa*VJTLJ5!Q67!"p|D:^CY`78xGZ %jJ$I9rz."۶m[ZZ {leQ5 qqΥiz7oٲVԩSVkG)Ap|P'IEQM4Pccc zkQnm۶Yk^>}zϞ=333= AxY@i&A;"Ąxhi "R JXo7d \'pAaY"ٓ޳_jN#vt3 BOLB1zBY<;v^ Aü@xR=/5:$ '<RAg`FƷ"7|sa6H.I\<;l#󇩱y?˳uPDE)5Hda^`#~0)}{FDБ"Ňֻ(5[%+|^Rjrzxf]Q,(kvy,KRZ)B.X\J1xB,ݭƨ)΁ sph0/<hYXkB]bR"?""Vg z߈!4r Uʅ4jt \慵*VB2n|bW{o|Tٷna""U!|'ZQ9Wś&g} h-B } #eap-QU"DDE!B=f/Ri P^DZPXp0Z-Yܙi!PȆoH ;Ńj%. #<=P#^֓}{w޽}[n+_JrJDygY7o{׻qyyhzc<ؾ}{ǽ^/ƘG>CN'ć{H,e`655EԔZ"8^^^;w\XXzIeY{<1x+?BbQvx΢2읊bYghbRQdNldP A ¡J xE*RD SfH4]LX\IzVdcw0; Q}d* rJďEB <Џ*% .0"sTfH/=zF_($_|yū!2d`ihSExhu%( H<(!W{k~Yf9kւ:1N"O={oޱ{g#\MN<:14bW}7^/>ij*JEp+ !$/5BD:ߣ}_:hHB`KW%.<^]Ll[D*J0T@[ RNfЂy[Pkƕ~ߴʝ{KL #QXiԒv)+M _:\&hO-3}SioKE$TA4he ; # z3sՂ?~hI4#p69Uw,yVBrbf ֛Ӊʖ~ L%kđQ6M>+f槲^/IP1BE8/ rjιO88tb2[E"/˶#lͶپ; 8,sz_g sG@;V!*cw6&u/߶~ZyԪ ٤FuSs= Q={wT\5:_I"SU%uokg5ӵ[ +TzHUb"vT^*KK٭NJp$/ĨEq@M!d5BX}Y >J+U k8 *AE-SXpÕ"E-H >el/ zl5z`2R^%i>tЯʯ|8uwDy x^޽{4m۟g~ᙙZ&3(^kCv]TjZJDTv"ZYY(~N_ĩdu=|[^\謯/|9lY\KCK<:ƲAkH={z٩)vWqJRx-7;#_}ԩYg4sa\o}+빳g _X:u|@pn0.pKr77z1Tg.!I/R2)cvbE9!$3,BMW];z@DKO=Pubvz[JMFxaٝۊrɐI@NcFZ#+fWDj8I+Qdx,$hPUQP8UQԨ`yTxSRGM[Ͷ6UUؤڪNWy'3Ȥ%TP" kejZnӳ+))rꚚ rHcZV}s/ߗ{kƔc1uRW']U޷cvyήm!Ti04izFJdC0Ep 9\_Rn784 MӴɐZ__muuu޽i~Z:Ï>} +.g[ ?xͽN/_96wdjb?SsO@_7X_}O;o=x${5]0Q*I7k}>OqRrU3 W;u_KvaaIG,󲢽 |6\ |o% e!ay(QV}CBUD} 9Ո۶g2#:)^}rӊyf+}}Q5e`MZKuPrqdRKDm=YEqu=e`+"@29\DWfEwgOa^pvmm0=0+'A@#9|cU*ՈX!1x!M-餦֕)wݤqu@Ri;ǥ[8 cWO⚁^ˣ+xݴvMIzWsBGm^|HUSq53_ k2$۫-TZQkmQa(j4"3R=ZE:Qژ^G#E,=[2PZ 4%A_k0$D{&"Yv`0 "EKHE,{ ^]eFvPl(#IPxR$ t`ܳ, PoY<6ѭMs(,U-,,[3~C3⹓,+ Q[__]Z;vه>w$ލ޵ccA~5i/̯g_NbT-:ej^zw/|౅vnhss5}^p1%\yŀގ6MWE@]lY 9<^G}Q1t??<T Ҡqe?M}@~M/{>R/>ku!nؾ;_PG~C~뒈c»}OL#4|Oj&) yBNv r"qڭ+1xfl)$5{ܱC#g&`HEp/ R$IbcX0B'u$:5o(^kmW2bV8@PribYsd!=wkwP̟YƔzuږ>E^pU9-D41LI =31Ra/JSLw\9Ga1R*k3a0HlS" Rk( KagDR96lY_[N;WBzmk_RY)ҭ/k?} ":RI3ӰH8:CB g+~!8 ݇_"])O|1n=][\yO>?x[>T+ ')4Nr%{ggΑGtowy|~ǟ9ٿwf~gfT7{Oljpm'&"A:jysڸofp# !H@EdrĦˆ G//xx7xɇHC$;f.HA_J3y`N &jR0QPyMMŕ%&Z{`۲<c̹ouKH`r !P yCq `'ELr+NSq yF$@ꧺ}9{c\ku={oKc{cc11UGNI\5MeMw;Vg~wWsO[W5{/~_G9\46zMlMu_aĻ/—9* 5 [_DS.·]uk2#0`D}dUF\Z,_w=}ֵ;ЫK3կ7euЫlo_z?ݛt?z d7~G~ÿO~7}~IK>u,.v~դqa7XB3uO|Uf]f XqO{ RlX9Hȱ̶)!mf-58D> IDAT4 "iXb|cQ=tfi9+5pmbXd_͋MvNmO˝ A7?zv3VZ:KNwNNfQAF mQթOT'\\DO+u˹vT&ՉF2c0JP 1 [L&,h2#@5ܞƚ Y DjM7کt?z Mc̀'yZN9Lw^8Gyͽw?y9ly_<Ǐ=׿앫•gwִz<1yW6Bt-viQO{ 7!Ꙏ/\¤ e%tN|#O k3nRAdrϙۿԩO|7o_|{z{~9jo|OϾ=g~_Yݯ{?ӟ{}[?wZ]_˿v&N -`"rWv+̨X%/v@5d:~<@kZJ`5p <`LALA-FƗ[1lO2+X2g6Y|$Fe5V ~(ڄ>{nj́\;(XG{EsH:}VJ Z{+x ~O5Xʮ |ѐGX$D3"r'<|ܗ|6ǯmٸz}>3 N#~ⵍfs5=wmŝ | W.\rϩ,Ni~xݗ5|]W>/ӕlL6MeY;#r ~ 7}o/s h`_TJÎ1g2p91i5^s]~ϝ'~o/w}ץ/]'nuo=X;?6 ՛X~_r}rqS~rf28o||U R31u!$0Z]Z{YzS fXZ2`ٍĀ[X jو6S$bDH9pq4fG+aߘL]]E:1T<,Z 4OԂSPO>{?,fM-Aލe;SSЗ u?B911$~r<"eknjKdP^&-TI&1RPSذwq8d8vs^se&؇սcybcrxW󉳟{ru;f~35]^3_ǟ'=F8uG+Wo׿'19J٤O|9:K̊sirIahԡ%U­wA3yѦ9qz#_B `X+hr"K R57úeAq2k}êYW'7[._|sbi{/=d!/ÀqjV_~891'@N&i}a$<ӟ'"[d64p1,D fFVs 킂zev)\عo2w4'ǺZ򊲸{Eւ ,|$d")AFZWb$W #YR:dVǒٍY8hvnUR g FfP_XE6˴); q s!V!pfLj"s38SRp;cBh(ܤżɪ!$<;1 `XW:1g,fܙa-EJPx 2\{U k K</{@ V=CGЊ?}2`7dnv&lzi<ɽ9o׮SZ_jum>ϵޡjg9sɻy'bXݬ$ةBKxcnnu} G ˃nl8sg{ۿk6yqJIChEF؋mߏV[[a;Ray|q(T>I=[~ZTjЎMBݑ p&*GXsuQ[B`$(7E}FbGC ":iB& )j .yH&n2`J^h2C=>e22 %nUd/~۟מ{#6 :44re-ky {-%{IH sDL] /Zzl]L*TU`"bQA.hQ*NstMd0R4Z3k/yV*QH% 1V!T pYgu @Ꭓ%֐r-;a SPI`"V9g"-e@"*(a%fC'CM,`"D!55 ӹX9vh~_!!΍fb'aOI#8ͼ]i'W[3wO''˽L0aA]A_k=qq%}~|k^sllo_C'NChTV\|/m<ģz7lrMZNq_ܠp?o 8\o^r܄t2[ηo{,֬YiaLf9דbIwoSlpɪ@u*LZ9k .SOY&*h©ih:-FA$1YD @fZ 1X8q=Yͬpw!VS39#8*ܽ0U v=nqՀnq;_cq2mP A3"ԭɍԈDD@I+4rઊRGD!23#2v)"62MXzVkG`,LY '&cjo)g4ʔjUX#C̝Tmm-r` `Gh nn]>tgvbr7S6TwR&k݌B\O;7mC$"fw5yP#@vSe"T==:7~e/Ϋ9<{ n/A#^Q[dժmom_ߞϛvY3M<_te'56 {AI͹3iuy՝sO>OlQw=.I88),M! =y}vb)|qZ BX,խYv1mRU!Fh6/_TҦI^MƭΗ׳UUձ&WU=N68rYWSUfdl53KY:3,UyB%YѠيy]̷H丠!efk ;r!HnXmL̪; 0J rJ=H vl(Ϟ,(Փ4e{KؐY/ط ] Ol&j',"rBahݕjH'I=n=!-yҽ/_=<  "*U(ت~KIRܥM(# $ ղwPXDjJʔ,s^,##!~ j~niqIm ͻӭޙBUfI70ӶMu]ԆsN: 0BJb)1Vł'@2ab5 AEb v.MuGmXUͲ!T-vROioHiEޯg_ wk߶6; 7~T\ ~=FK}Ө5>Tm+ @n!Jϖ1\^lK {Şݾ7WK:CwμrЏ f֭d8ԡԒ]`ewԆ e!^+汮'4_ ۻn+"_A]Ax;WlRvR1z٥/~s{#sȹ<׌,~E4uOnJt'u N:Ln)1 X.SjLj2Tw1@d}^ Bhjnj|Z!yp8t?:ςL-i^ThQzN.Yґ˾{q ^'np϶] #6#wWL+nﺽ/ 912KQ6COt@ /n9trhj|w%#&z>0 O}_y荚'М>V|~KFyݏQҼ}(]%Lse-kɀ0+F,/E0@!\Z^2Z!vhe-kYZkYZ^ !D  ZCn1Zֲe-kM~fuQZY:"DP Xm^ t@ۯ "QBuKOcn"6D|7pk Š`fÒ~8Q`d;7nP-_M~D@/T{ jԛ q,#䲞Ϸ7:;?R7s/P#qqU:N.ni_{ ?vavs"ۄ@kk#$PU|q Dpi/s "䩅pq a 5$tså|o~U4s%n4X~ "ύ;4]0Sݰ-3{4Vد G+AaW@apå*017ZI@Ю}Kw zt b[7w95 Ӎ}>~ܝ$"F"9xAE>HaC~.uOkKa4ƽϲs$Uuw` W{n2jFl}*7&>sb.{b(oՀ_K̜sv횪;NNDDDEHB( D =23Uz"BtRJD41V9gU}'y͙٬iҞwjX1l53O" V2WeVN}IAau{_NE n1i ao-X.~HI@ Zπs11w+33U@lpaYbOps7hem7]5s N cR@]`@$ pr.u7.kq`twv0:8PS68wc;*zrrrGSD,xm`ń<Xml\]l%fqncs[0Sw7wTed"sƼ^BJ]ߩ`N&M{,w|Ky*7FTC;f2' 8 ;3@̉b"p0gav13f!oGUYəIȊLlnw 3)9CAHM @$-1C."b0uݝ!` Yj/<_x:v7M̌iVÈʌ1N\ @,׽L~_%2=/0((˗:3,[^9@ΧPܼVn`fs;ػutpb.뮹wG'K1qFi  p݆2q_A=kY ;p[9-|u$uu'Nln2s*fL&uUC U 1ƪMu]ð\B0Քs9Ԧw e͌!Tu^`k hXd IDATq9~'mwv1ri{;ޙX;&lVE&Tog#pa9Je- nb ' њ\p}0r~d3< 0JN&T801on֧Ot:'EsMS q{{,)/sʃK್ &/nVU]f1ՔN`f;r֔ڤPs5's@AJg!B\#=3Znjh Lf C v"(k_8sǰ26ήoަ72263q7wb|- he,IoKM>VFǽ"w@`̬hn$odž= ##{XnȔkVPͺnPW'6lNP5wg>fuSs9LN]&pwN:ញI .ePlnnY˙531ԝԝ$ܙ,%M,ҽN1=1<93 [ιp}4^)#4;# T*Lx'qduw;;&P|V0m 3wwsYw7SSj9pWUDHp*.8G.26>ŕpb/О- `f<fjKs?aU|a .6n99vtps'ELBP cU,b !H*DBU'N@fff^Ui_ ܛ kA+<[f&;OHY9pifN)Yιt,tS9dkYZ^.=2DQ(.rb@qM0 9Hȥ3oOsf|e֦I`ov!dM#$$4(hC4rUS^68!ƩEV5R, %Z 32 0wtv++9`n3 O x`Wmr"l&(4An} d p ' }=ގA`ⱳ}MLn%}EPʶړ@awUC}3R~1àXm |r;t`InHg8\wσXޏN+*;}!0 &YCaq|szb?ͷ`XGzL~L-'b&I9=3vf)WN 3$ G+-S1@"NY9 ;K7Gp '&.60+-_;:nq ,+=O4 @q-?I7+;J9zwN obly'mĖ&ZqVq0)$~i?~:(tUb)?h(m;|ѹq=N:5N ˥|ڶM)}֪Z^nZ2yUU^Uuxi/ !%.Ƙs!e_ײWpW_28v$$Uo&~Y=egr+\ v"ϹeHmZŒ'5ux 'sE.XHAl4!",U31IiRdDa2;u{ۼ+&g`MirϖDnءԹ07CHݝaplf (]͏ TrYc(x 9F=魭+ޭY24&ۧ}l= L1fTГS&arTN r(G,"[hqɽ_0јMÁY@yī2{9 '[躭E]49XC}xrF&2QF!k0IǗw"'ծDD`2Sw~|$RU3hss&!i 9*fnL[@ͭ {s`v[kb^6=ܨzB8X3,1r4Sd*HպVNJSoZV9͍J(fv.swt\M0p,MH}$0iDwE:_%#cFt!;v] 8~8{ ~ N  ff~ӧgYUUE?뺞N'O,*84MӶmUU"\gyc!ܶmiBKJdRH'2e4z" J4&?ΐDJkܹH #27ĬÍRoDd;ղøl^L .XEYDs^$Bc6eXM&!Fd'swOۍ:!p0|soz/5|ǻ/-lzʼnDX'fZ׮F0 Fa+U%jpFK'{.Bm&֎FY8l3O[,x&pudp;:dV;B Q@J90 (8 ;Ip ؉V3"h&!j݉D؍ {0û3HNd:)ÁymXdDg +(D SdBLQHBGa"b,R &"LIs{>1$ {%fL& IZX]999u"UdmLH#,QPq* "*ٲY U'ꬦٌ DmKԝ©M#YCZy?[sQq.šH%0`S6I#&P61p-b)a?fdsoV3)IHU͌j +^^Wrc:fOp3;.JfcU@ اܱyA0VC(R.\(bf.]";yJ iUU!rdRSN:Y;L ݗe۶m.˪6S0g /w/UC7V|"RIƺ| s0U5 8֘h&]eޑ3l6 "IGߊlRPёD I#1QRkrQ+2Ħ:]5:@.bm+jI)h@d=wƅmy,kdwGm+v†v QJp;{$JS;)8'mɲwhr<Iۈbct}%z dD LY%qP@{wIVIa?%:+wo/t.5 xuq{+V`֧b/"̍b]_ RYLG. ܣ.&Kn0JBEԹy8wZ†W> ?燉ohͳpƷƼ 5a!MPU4A&%dr6{Ae "܅Pⴃs4%]&fR >-F")窎"4lXė3:@\S iAɄL֥LYvZ١VU! C8Đݺ-fTk2'm es'uf 24;.A,R̳JM,W Xk"-VLS`)Y;@_Vg*cʠ6gZ!XwduBKĽLaݼ* qmW^R7G*):G3 d(R'<3c{u<ŋ/\PKݛSJ^>䁩덍͓'Ofl?gr,|Wl0cRjVU۶8%f/Ę}9JR?0`>8Sײ-)+;HՊa#]x3 ꦁdY|>_& QX2G9:V 3uYNgr*Fsr(Fa -3j鎻pZ59^{WRa٫n\ٹ&u˞HXPk2ً4i6;Tͭn(JZ]OM殶U4|%ǒrpLgb.\{Cp+ݍheqI2gR52 '쁄@NL. #r20]@dq*vb<3Ve>}2\i*lN0v&%ڭU#)FNLBA!Q&0bdG&qKfVoLu&4rKrnfBk))2ULA%٣8{GSJQkM)V3-åDbff^SU#zseUEy=V'0mXm3haR |^ՓDs|.)%bYʉ $}1+Es~!Fǀ1we1{r\2c0 0S ɑ?gHë̅R&s]{Ӆ$3ft:(]\UKΙ yfhHa,me4;;;u]?&ýs)r|)3{ ۬e- U4wܗrfJn;wŲFyٶ+Q y!ehNF,LdQwz ,1Pi uiO6^uoQىxZk;sשyz9W/",scɴM^v2 z"U.1,Uz ͬB5I T7p\ȇ˞#x g']輠 UiI.7lcX3cܑҋuY2‰@ |665P:.|`?cq;1J>?P |Bo2NXJ14pvs BnIC-VSN1N%x c"`b؀ 3ES36jY!WsX XF3SpU ErJ:;f mӝDM&UB0bNYjd3agILiC̶[wfaIs DbWnA>Vp7jK=Dg.ޠkׯ=So~[_?{g<{շ[׾lZo3׽}O{ӟsgqw=3dRz?ss#?1}<}39ĜΆpϵm1{H-u3  Gu.{-FKܔއbQVpoNV?УSȻ77"1}1%9vgg/0d /lt:NO:uwNB|7!xw @ UU圙ɓɤ@bqr}  vG𡋊`0@XÑe-<,JN]V:_dsUYFAܼYuѸ:T449 [dMDED^FY)Tj"RKzC'8:O}W}\^6gӰB./p`yA(S-ֶ99L(IdܠfU5:^K1wŸ8 ކi!YKC&iMٝe)so-i[J@<n0d@χv*B. ䷹ة c%xdxX8Zd>G[.aHx]ZJRaco]8D\, |iYD*j2(D̕{`&ڹ5qVa8DC@H٘Ia N1*23@ĬY4XT@"Lj)Z( eƺtrafb}ۆ@Xڈf& 5g%getHMWg̓}ˮgͿ;حPKHBp\PI.'v9r9(Ɔ6+SIS8P@AL Ԕխ~w|k;ؒ{3^>_疋k,| <;C z)b[?ѿ^y՟?yƭNӿo .t__ԏدگNwOO$}c+?/c?#[g?7~s}>{ۿ /?]:j%2H@;L L M޶.U jY "` S^@] Y3/lKc^9SwwjR2FkZકEwTVi]eWE^kŢp UcLR{ ޷^vj\a5nWտsn^? W̪}^M?DX7K"ʄ?} rbq~+~YoC I32rU^M?GQ0c4&AYCBOEؠA u{͍͢,Fe)WPUU$"&:A@?}^g5O~'O~Ol"URSI:i}* lD)*\pe+]n헾6 I喘"8'_j`2 eUH5qxaܙb!@te^IEDd YD" l oS_o}W&z4R˦bT8:#o *< 5F`E+ ~M?48<.nSENçΒd(FcR"d+(hcF1d"@ 6D9adPFbޠu `ɃQcHP[frNǠ b6Ēa$hZ1mcQJ7E@q,eh351{✃@ArFxڧsϣCe HF3̥+Vϼ\EbC1 IDATO^qs9^x?+o :[7oob hM|8 /\W~e2ߺu?7o+JaKt6}'lFQ|? f05㵤.#zWXCNTk):,B%)YmpXᨯLo׏2sV~iZm=Z{ݱZvat:$IX.J^ wzϲL=0QMSx-,Kk1I:tfc0{sntkg^xvC^_/w* 0B2ܱQQG䬈0XGI4-2eeټ(+t.%X"ώ #Y G8Wo|7wY,Rj / EӃ-'.i~8N NȀ/WF_~w^.5NĔN wvv3gۭ6ˍ8y>fo@ F4""g@b5 FkϫV̼w= 䕯u%uvͅaz_V[+=^p4h (XYl@} gleV*fˬ9hY{:MI+ ~{.>6#^ZeP#Fϊ`f$J-0B|-,r@ ZxYMdB!ijTDSʨEDNlGɉo  #6R0<!tm3!QPGDbq 8E qvY~+Y \. @<5d!ӧ6o߾SO{ӧN޹s}}&26?s?~/[@2B&J(g>T: WX}_tj:}}ݱ-eB ikG݉(N>5#bQIdYVC!e;素]^1tww=c'QZ;o4:PO}1&l\{k@g7`'r|y܁ 1UG+C5L=%$I"EH5GiDZݶv4gNW,lI@ĉql'K8w;|].YKN_vƵ@N^3k͞&*_fr2;֤k{sR//_s9Mh˶h枽7/Y&"^f&MLd!k@YVyXfxCa*땁"ȥ*}E 8.I1oE4Y*(vNuj-{n=؃Cλ27 +s =_!#{&k`kֵo#*NK0݃/?xႉBIֲȵ7~ׯ]9w8("nƘDQ>^{8EF֠gҬ17ɫͪ5\ 8۪D]Z߅c=jX[?j@ʩbVw~xąpťg7B{r hZQt:jLg 4ԿN2~?MSZY_,<,O:$jU[.y~UmE[__W:uŋMe]Y,qV~YZgplnޚ.%/8i.>FO7 K;1綰eXX)Ѵ!F!&"!DYPAɠ RG5lȳ1bP~\xz6'4Yxz9I?I I/_.?n27O.fp2>E #91 f-U03 2Qk/?{p֓O?zAb&kk «W-Ki=ZLGųOv?ƺ;IvS+;wn߳}p_~ǟ*J߸i-ZqZ t5'ym8LO)WnzW)kaRbxծPV]CXMw<y~8Wݝ4iE\jpFUڪՎR1se*#Coє4MqGrXHEh4#tyܬqwG>rxxXtqYk_z饫W.KurΥi:uBHY9H=|T,<;GYӽsoSB`M ӧ;;mzW ΝqY6n̖K޶yO{}k⳧ ُv5!,x."̆b鲠NB7TZbÓ^="%onP'Dp S{]?\7xP_S 9L S0xSMG{Uyo.PaDbw09Oͮ'yYax֕||~f$v6D^gpVa;\/:.,\ `0S}}ŋg6}ɘ}u}ڹ}׷ ט?ޟ{g6?Hv?~;{Ǐ$_;~>H??{K?xW>2/>?>? 9jso w<~],hhmU,G}hv[mm@75XQ? eO*W4 u4Ykִt:qsn>khQJL$,VlvppjKƆ1FT4mZse1ܳO[&-߿_4vb\I~ ӬhG+̦K*d֧]^3ۭst˗ώgnI;u3GO]"n4u wbҋT7V$}:_:˞YZzFax,Ě|62Zr 9FvOyƈuD6Uwp0k5!vwZah5pZndլ*HLS+1a!$]cgnGfobkI|v {,3H L֐*TO%#c=Eu쫄Gp΄. Ql!+3=2]s`{1!4@ILV7i(j+ uT'=4mQYfwgшgĕeGԏ{4huc> g.+,-cq1N,[>~߷"c??|i|oɟПx$Qdټ̟~O[6;G~{*ڼڏUYCx%4 ʊPmziZm70ͨzBeyҥ~ZVRxssSE|Xy/y~-e!JΝEG"yN]Jd>f^g)R9J(ʲz Dex<~rzr@\,lss32\'d9Y#VnV) S&qԠ XHQdb/JtSWJYt|g $)TWqbPֶ6_ۻY_л?/{j}|˗ӧe{FꗒM{eHwE6?tHoOocR^D/OWwPO|w}ͳ1l:fZ[rW$Q$`+9HgD  أY,L ur~j7RI, +q"J 3c5ܔ|M\FIL)!RI(BC@I$D\=aUzE4{Ȏ/?{AFAYYda`*b&"  +]b$X"B#,D箰"![hIQJe`PbpF!W FsTv T;P#k1F@"fHJad1$49v,3:ZaJTZs|`G!2RDF +ŋֺlGq\8$P#Dd Wm","ސe"HhlXj $1J;3xXҤ8Y##E!X#ITFEvOT+`_,0sPPTND,G1У<T6{}C _맇R\uƘ4Mk b"uΩ]n~{X,Dd{{ӧOc wΩ'x;8߹mR~MtmRv+kA-E bWa/,pf^}gkon:F_}Ͽ ;ӧ^u΋m\;<6O'ˁ&IMRk6s8:q;~d~|n?t8ݸ}spmä7{FPrdudфrјJd PU|001 y0ݝ_H^-$jq'Bx%aoVV#W} (͑{_t UK AFA a/}0"" Ğ |, Pu~ZΛd-z+(kq!"bƈ;2,(D q\zF H1P8b1 HKP@=W#1hK "gDQPg{RC O$W\Q5Hq$W @3Q@ 'k-{ED:af1@ ʕTAbb$ h qSBGăg h5;DB_XkU#Ab#E FBBk-1 "wn  VR QHknfXO C4`MYТ*IVra;Spۏ)zQLݫZTt PSUk]U7~_-.z$ڨ_[xq4eY6ʹ)V;DuMf3%>88Jn yT h4~QSVM8Zw۰ޭO2'%cer7&|^79Ģ(IR3vKWN 2/ف#bD&cMpTȰMvnAΝ={l ϧ8mN$} ^a{kwHZs`w]썲;ImqYKiZN~ާ^~=sT,JD˂Ȉt2n4vEEoA1/X($ KC3IV "]gd%Xtl  mql"FVdMG;Z-d>rZdHej^K5u1|-R@Iw %Fj6Yh"u"x b5#/,Є{A@J|ȩŠSIT7T. E; ^ەؚS(o>]l EXK@A4,:a:22 BLVŸIKV֪B"u$#y\]a">0ͪyD:$ EԫW4T1P@y~xgϞmV.8 $Ie*_}U=P{{{kkk`9/i~,_%}-TbDނr6AY_S0{JmO@$&[QoƩ-F瞅}ή(aQti{`{{fs=v~}p:޸lS D7n/E;v֯y~?ߴ箵zkbIYS^{(ܼ1\r7||\(_8A{AqtfaT1pYkԁ4D $$.AHHԮ>I}$ ԀGUxZVGRS#%iB_h%eEwqH1LE<UVEV)8ۃUOtr|v DrGPD/0 IDAT^7*sӍ D a'_.XX ;/EΒH|{{Xŭ 7$LN^[íᎴ`pkmFʲa=o?̓[ng,wǣ2O|D)p>cnmm|so ˅q[ f2x~jkG6+Q"JQգr /Xuh ! j* c%FyĒK}𥪟Tq!bͷJ &HX6oݺ<+DtWMYiSCX !O WFV}(8$U# a# #xtS<`MƊ44)v 3iF-u!R@F@ <뤁話)Rw$D--杄Θvʄ`A%Rq "Uj ``Bbu%10QՖ18pb,Sm _äHՅ FA4@@!-" #Hg<?"$09ė!_ ސEpCkn !jԦ{c&LjpH8a uXX:ܕPpR#qt]u1gM]qy$ ijs$ITw]ʈHQ9d+?^bEujs,˼۷o>3ie9L8L&HjiYk:G.`Ir} ȉWd9YS라(a=EGb/vnlV/[Թ v JY̻y}YtX!%n6lۧ[ׯϖM[ YrIHx>r^Zҥ3/%wvQ?9*vgN7IvhrsgxN egt[oON8J[E q %W]NʸȗAt(^\N,P2;OLX,A^1S5x>+KQc*ic<0OR}`_-^]8/C(H3 .R%ﺪD Jqպ!F @ \ZY_kAT+=WlE+XA5تJRU$"R_  *}U:f1Y,N~J#)XW_xYZYK#E+w55aCDh0gt>TTK A¹qDD¬'Z(ɡLrf[E5MroBC( GqJg !R]+& L|@:vBvm@& ({%QUD(MJ @u6XC68ʙFWLSkjo4\O8Vz6](ظtfSCM&s}7Uh4RzMUm_[KZl6S{{<1f\N&5(RyOSUS,,+޹xeNGiUOpnmM?dZ7u큕<,x˅_f~,,Gg&IlyZy/n]w[bzko't>w(#SZK ܕeo{nm}}u,Soqo df$}NJ%/BQ3. J"&)8qfN:;giϦ~9ra#""ncyh*gpUyqFVfTE+Qc)S:TM!>H5:]riS3@8QZMjV޺T:k% +Nҁ*߼CHV؃fFID!5"Ȓ0D"֊1BN QMF&[ au¦a'<@En񍃨BB}NFoMbUMW׊Jahe^G:F3 XWTpE)hD֓ { PB켰 }|МP;B8l3vI p0ppH\zI/0YN:|J" noޤP_k{ؑjW!2Z"U!2tzcu4RWape͕FQ],˚eYWnId8;wNcJ5Jiggg0Ԏ U=<<\__٩5Yǚ6w4-4ƍja,K眺sIhO2pqR^ۄcO{|U!졯/vdOQUC"&CH+'+޿#@D,s!XL<}(s7Y!ٲ( &&ASpt΅/nom'Kg?[7o۾qĖ esϾWw_O(qA9sʵe-13-~Kngg?K"Ɲblۇ~ؚ[f;Y}X,?\ ZE$eo^hXnR; YPUƊ.gWZԊum0*t$D kQ-S"4]GGNef$Zml۔ . TwI߮ N"J EpVShaR.y|]:M'[C1Cq0-&fA`!B+1xHi kR+ sҀd[-! k RVB x7ן҈$MfF$M*@Уd">+n!yD"HE4Ž{T#k*rdl!L* 32rrB ulTxfD"h4Sz Az(Dyu%6}͏A~+Q.:eJD: hDycZA9nonsM[q|5%.$IHDwܹx`0ВZ^EQ+JgWyk]CU$T+z.Qs$ DG @Vf݅8G$6"noo/IL 9Ws#BV7⚴J\cog"2NyS&(u(Z.A/{3OGr<_tdOM7Oژ>8 YBu[.^wEd7?SoKWoʢJb$i^RC/>4\V "dR%kR\pns;ȤƮ\x%{GMH5c ġ]Ax-ǧWk;;ƺ`DQN}QU_фX)xzBHakyEFoȗ0oT|\e|ѽ ÅJT%cUj@T]Qa&aԊb_' q쑙qh7i,StBB(uĭCڢ@zd\DbYQasWF'{W>~s\Uk{̘Bi8V$0`gQ .[&F4 j7*܊R;7~<4z>D4cf=)S˲ϪD 0W\ٹ~榎#_̇o1Ĕejn޼9L,áꁄ37 Z&Wc$I4]u2En<:чpݨ`d{?sN|'4+J^w@ӔMQ"g qe]!@Iw:e.Hʲ`Z qQ>i/m-g£ 7t89;]Z;}6/'eݽyvt{ nb63̤1tuܕ?|ޞ4[JQoJ1c"F 1<*JgY+s@.io^;a'9lvPpeqB ̕C׀5 t"a91-SeW,4Mޞ{izA>h`> dڝ}t!Kj.Ed43_+J&@V==^WHɛ+h #,3-&^2G02@uuQ7`P 7fSl*Y^$E9[8["  mZm"|>wzO^,QT W^]"qe"6NEЯgESf0 zR=DVX4E6>BmZY@X-̬{}B#?PXz*Y; jG"fjUӸp$@a!Dz!=¹yCk k 0zj@ӔMQPs8Ozs5޶Ipjqqkszz|^[CeYճt85u,bQEιr%eVUqn˲Tcs=cxq BQ=f 3XRux4^xA?!l_ '{_[ wtPzqNc_] d^N^bt$qXe[n=xg ڛo- N/dsm/dI7zw&drNw晍R\-67glbqx8.eR %/l)ۓ)I~o`ϥ^Av(Ų;2si4m+x|YNZF|.s@&2)ׯdRpKT(b'"JQU|or֫|5U=,a{kBMi"ܨpŹnH5TDP|?zĐ-"*'4wń0xc,W5xB0XXRbݏl=$ j  p(*PB/Rh #rv 0l!%Fjv&V{2 * U9(+` DR5DX0".u4˅'+}`x/Evoor-<37kُ jRvT|B)f!"*_ j~EDhVIn "l?t& ""0B bb#$L^DBp.# !XͰ\R4`4%+1\=* b-+92co"B.USD+N{h=~5FэDŽ{uSϢSX(^{tjsJr,B-(Q}=M|9XyAZhZji~hrm(Ҿczd8 jΟ??Cy~,R5U!x4X垧t}=/pgdA1+10XG <`?Ȝ;ؙ9Lf(˜]HmqLQBb-$fg0u7fe6YL鴦҃͋'Nl$NbJNmm2 b9?jǽV _^U!€uV`46 >BUцрjrrJ!Bɐֺ*ԩԬ72b{*:TWۀGݥ;+Vt?[An07Pbڱ 6}MakÀ/mRi` hm OQ=aPsC%F  ӠoV>XMk +IV((fYd/Gd9+˶(@FY p[$qכV-„9p { F0 *R]9HpO=!^Wej({hȝUKv*gmG\+X EM1EԼ&q[&UP-5&2jJ׾ASش:kDS#QeiU8D Ct^ػN%ꦊzmbiatQ\TK}̭r8.Kux8V׸nj$I咈$|rLJl:zm!MrmooZ;ϡB7֎eMD?[Qpccnף&WST|jbhB?@9`d9YNxuy/s܋/g[mq$I/gr&ii\.Aq lu$_wP|W_u6s.=daϽӛOA/7nC7A)l|fD8-k]K"pdm-?=7k#9{l  *V(nUɖdT=jmm_a'1~iM2YknIK, @ù~3Bi4$N3Z&3"9Kˢr|2A튦*bfO^h-n?u5GhYZ,g.%P8H~@Wږ:Ү7O!\c-]]D܍ *#00n'fA>([Yt\&!b轲1 D HXB)7GZ-,xNF|LZe58ℛN7 @I002|"%`'3FҋDoG[Z4&0+?^OlI=-={2/޹zŃ%7}|zgX:(cȜw0hi!hISik֞^!BȬ4^)ŒA]\b`)еV\f…)AHC-&0JrHD^m/I1k 6rLr|2RzBX`u񷱾>YCTtXǤNB-y!qOw䧱6zdGu#EF:xY{Ͳ,ϝ;0I^/\9kkk~a$8QWًZDjl '}<7?WuG OhYO.ʣ%/+u|pzzpd@/^N-|M+9:Ξؾ᪬ԻC#^h8Zn {#S_`?/\7n{ync_󫽁-ecuͺ.ƭnf/jsޚs ި/ƚmI)5{GB7&Yx$9*4M&{iA&6ϋjFҲM3XAZhG` bߊ@Jd!p} 1ɐ1q\PT"`dp^ M ƳYµO[! ]4|`Ma:.EA'+BAGpcajv]Α2@ Iw'ON=q-xZ6g! |ߖ"YձEYx @f =IkEy@S1C&'"lL HsF<ϕg=C]]"^Ttʕo~ݛO>*I}ܞi;r~Z1{I @X;_'&A@ff6Ma,.- q6{.v~4lJin7mI̹޸6>\|n7矻ʉAܕ;<dx9q^u?{wfqG3sPnYeUۻ/?W&cA&jP!6^;wՋ_{Ӽ1^QUWVέVeys룋/g|YkD/ @@Bѭ@1`$oߥq\]{H-IQ9R!wt~kJ-c8B'n$c[NWT] ck$\Gb Czf VhҶˉ< b &>kāc56g p8RڑHEȲnDaCNŤM C 2x< m_}=@B։8fjkD n D zc=Hh|Vƙŋ0rdPC$xBQ^es|0ɫ0  Qc<"qQ< ,l%-E #̂Z c CW3pDDTi}TZQP@ta[7+!I #Հv!ڀ00 ԌTzNMn~DXģO~g>7 b/JQ|}0ӹfg5G+S=;,Mlmmmlli<7(2ӟ3i#?i?kkk^/sȲHhӱ]+,eF53, Ó N6xOplްw= ;mAf,Qb35҄{IQ߹=>wWןUY2_ n_xcceu]/6J7[Z1Șj?wTN}x%KpoϳKy_|ilP5y^-m)irgҙH$\<9 z짓de<25Skv;q^ءr[y\0.0ׇcC?$D9״U9+ Twa;&;ɿFW 5h a4R@iQA"Xd8-kڗ?!N` Y"rHB0w`H^DHmEI/ hiHp7n O^Zqo0ZAk J4iEj_0ށ!',fɼ3I4[Њ]@x?J;+9k%K"D/(hуpKl5Dc#J}9QGvaEkb @+veV@&a]FmWU-OE#uun@l#tR{:LxFJe=bODpq~FUUuJ gY6utqUU}뭷ŋoܸiE1VTKKKuNEQDI|.D~}Mt:CwZZ̜y_8;t;N[`ʆD|#.(~4G4F)]QTM-yAOzf2ʥ疳K2و/~{28Oџ?/hىgl:wJFtoo+6_|lGBb KÀvXIhxXn./_غY fu2j~tʋK6uY%(#qwQ{L,#"c#t3eLA⸺ :.8N=AYBNlSn+FՎ흨xB?;xg@Xy8K?_]1rp8,b2tc-źZ}=Ucb| }EQWejۇaD}f2/X?AA|nۯfgN5eΗ Njc "\?_ ^[tW6^}aNqg`ͻw K_򧗆0-+`0Z:8:- &gHU8^Y?:ֵ^AHf9&Xg~YX{MK6߿ܹ$a#{{EwFowqQQ+0ҕz~{|_LQn}x}u0IU$Fأp 2q >a %AB1E \xщWF{" 2nz"te:{ k1xR+/!63%*GVo"'DO 3oS2:p֨@h( ! HYQ$[7?0~*Vơ#DkZ!BF-,Y4T;gx¥cHD\j5Xd^;3Hu̡X 8`_^_w6Ϧ8߾F`]+K˳YYn>/-o {iv_suY^8w'~Og`0]ߟz.[KW6{a@AdhEbP;C }d-[mE<;^۾ƙ_?0/6Mؕ$PSU\Fҝh%Jc4vXD0nNHKVqFEB sEl wv\x@?m-hȈ Ee\gXKi /Q8{Sgl kZcO~gQPD{xFBY {4 (])JA b lLqQ֞[k$Ʊ'C>#Zsr54eh|dȤ  !7-JҺlrUjsue FLDl۾B)Iz G2 : $ "@BlgNIO4iXDE D|f޳Ҋ ^clPk] #؋ 0{ jʲ.KJ5 = Rxat&H ](Y3D!NJBLBD#~qq#7:qCŋs3fm)@lmIRUx<^ZZxmexﵼVH[yݣ |W_Ey"^@xUzvRzdt=cm &Ed:hJ%i|ܟ?/+;C$,4b-xW Oh3IP$I$l|ܗy9aK};;?+Ͽ;ol?_nQ?()X fpcmsm\ڛιhfM@ЭEG5 w?nS{|xgM*/@މwk/pn0/pw#Lt-t1%!$h`1_Dw"d 08t:IPPahีtTquNb5Zf t͸C,GG*)#C#p,F6OC<`A  ρ2m8ba$L;bG9ZLF=_ ưqu"F !2lq V!{)q*'֠#  "lXc@8d=g>eAޤIPG,4v '_(GWk* qS"bmUjF2ڬў-9(Aw$Y^M.*) =,nJ@X7;w.c׭jYj;bZ.wngBJQR4_6@h='T&/T\s~̜Ě,3hvwq4eQ ]{ytfpCȤ>EƦigVh` ']5ںfowoy ` i/%HGv٥xpz=`D H1 r鲁 /:XSk`:bvcB1?t"QLߣ_HLOO焪4rCèޢd񼴠{,=s/x˰~'\ [V~U# hAHiAx!BĨ|X Kjk2KMYTy]؁'}7ol֍\sZτi3ڑ :vMeTu욢DTfg58@#^*#' A!P[?} is}">?^+(`,>iHP E {d?f'!91֐ @'Z@pH?8xn5(#Q7h }n/@Hee?~ MSLy4KKKtz kt]릐f׋|ZԤU)IFy9@ p:( _8ݥШ?T9sf>kàVx\z\=?iy}$]Z|eO~wgxOx)0JM.Dzy/l 0#`Уp]hR䕩˺* dd./?wn_3_gvgŻpokj/\pu=;?s[=w,&M/Ys~?xwOv~^{$eR&y6; _yrڸp?bP!h{ƿwdIQ;]X|๜ͤ0XV484^O˞!x--Ah5٦]k洇P>A8zSP!uYi3esj͉9'-z̧CW4*Q?tg!pKPr"ód/DZB7ECIU&ͷ Gxa " cM3[$,4HKubHҴI016]5%"gWHLbx$lP2!4Dm$<6&eY٬Ge`:?ijhXK>,<6A@k, QwO"q>C/mL@Z4I6' |]/oᘫ&tc3 2_QЋ`D,ui/YTHʑόx<J&`Q*nD4*‹VU("r}4U&lJn#$$6Z@ HW4Q ?,pSƟc>kqJw/]!I՜x%IR׵{eYmnnlI]QDfg6eN=*+q.OG9NWy['{AC%8ؾr~wC[eFP.>!%əW]zp8favMzhrG7nrȴr7\xW{ "#?8O"E!"F 1zF~trΏZO|̤\D,K`NhmLfJQj *oe(KFC]I7Z0!{?oɳ{ Sy fasɀVy IDATF%q_;{aX A?(Lu73T=2eU 1HUYYH :Ok :󚄴LCy.ʥAIKc x%;Bbό"Iqjm< VIz^*b\T #Dmdۓ5xi #zV4ϒ "t "!001vcMy@Ԋ;Ab8 iHAx@nZXnv~{'9w̙uDnѰEڵ4F,Eg}Vpccc6)%m"%%&S+ZtEnض+y f?7x@UUi˟g~dt0s666tU5x"UZ[_E~_u/Cq8#+kuy>xF |JA#Z%qR>!;ɛ|k>}L}7ff狯;~pxpi1&G?^Ye8K ۷._t.tes0מ.%go-%.d26 [\|+&ӟ߼UxtdK4l +tbCY?yg,$}K ̏ZkϬzd}ƽ`mii<^ W01 dFbtH L-tAXj{}`䕶&t)1[2a`pNLB!xNr9"2{ .}X*D(Yr=mAܞ(xm[ =r…Dn˧Oc؃d,  1!!'ԤBz.6Q=@9?Y3,k<LJذxg jD^,!.!AOq$15w "n-I q90Y0Hjot2'/6R3#EH]$̸@2A'th-1xK3SbEZGj;@PQ dgJlVcpFUCPdDA[G`5.ZT}"ݓM–zWɇ.;d'|igMa-ʟ^-cyGCKKKJzUt:'-Z4v( mU<"h ף^񭭭wyw޹+~ggZo{w۷'Ql[ՅÏ'f?N=Nx:Uc嚚gy:DyY01΁I{#4%/l_?;TvߺU1;Ev@䉳A/1x29.eCC|fce>=4y{w,//[~<޿f$s1k^~kg.^ ֞O]^.*dk EXyNeUMֈPjs&I&ӟVR=9Jp8Lz1DJhQ$ǂ2m9uc#ZDTBZW)ގE Aԅ}ΚT_Ne:1 Ui_ТKZ>;!zHĜ`j-' Z"` a II2+QޥMMyIRfS3`>44Bdf(G"3ԣsuBbLw%ފxalu.M&{+k޹kgG~`+SQf.gKk@C;;\ ??y0݋tUח䣳AK zȗDR$ Pi)W \/XKγcהQ~R(,gO[GA]Ie aWoCRhޱ03Q"^?_piu}COT<Ջ&s.e"8jQ#ni7~{,--9y4.yW.y5u6M&u*;{.\ |uRe+fsz/ ws#삢'xRSJjmfpw~tTNxHkF/^s{QS޼{&brރ}[n__KϮ޽{ƛk7>+eyXmoSf8UPX]-ݿSzT7W޺;|f8~΍J CA B JΕ6<ړw E(K,3Jj䐋W~kk cL乖H[Ju 6hM>ZRglnb Q~#1S$v-'96qt(Dsx5΋" ۍ΋. .)P<.J"P 2$V 7M"^ BȞ" AݟBݤ@Lk򜜳S4 5"dYS;b0Sc AdE :a(9HiA rރgf^yEKї^YVl% Xڜ'!;60 `@c?PiQZ<"B5,66 ZQrm""! Zvd^iR (KU=<]o_~xO+mp>CH /*Ÿ2DAWM\^5eQxFAjhv+/߽/[ois0??y̬8;7d2 қ7{,/!umݹ{n#Yir4?ȋrܼ_^b f+/8wXpܫGk;0?zg.>v"%K \⹑R_UX&`eg>ޛׯ]=B/j4B , [ i V 5S3tϣ iezؤë#tH8S[r= eT4{-帷T"&t1SQێ]ZIGo %)Xr4?DjHp14Y#.˸*qvm2 9k/%UU0IVB@I]!s\xIա7s)تd\QyU‰5$WiK"+$ċW7i<`ruEW^jpr9{3ikD> (C*AN&-up`DB$ "H 'Qh{Yn2a@^_1Bڢ=$XGy%DLы;X)xKVbڌnG/%,%y.IO 2{eo}K_5/)czr֝s[[[^kjh4:w2"NSGw eYja=ϭY)&DP LcSP:[1yݺukkkk:Ʒ+"V]Sw <ޭta 5库ҵ K$ xrFK ^z\M_o*!ݬV& *l{&jZJH|N§ g^BLP% 4d{J vBB,` ٘ 熓`zk7 &c#!W:D03pfp{ BBJ-u:--+grʰj<2 a1!vh ʉGpވkhk ;v G ~UE! H0AdsŰCIX I' ;;:FB'QMM%b=DFGN|Q~IqrdqQK/2bƩÎp%֠"\eHқ14`0FN˓ AD}-s4(4M4=886ƨش+~…UXFu]3}{.]L&*e?($[_e@Esdkk޽{D~_zMw;FyF8K|(vg/Hj|"UFDVKvnJsNGVi0h>󮲵׀ ݝݫ/k_&?ݟNw9%%Xnɭ7.R5\>]~.^A1,ݻA67Ϭ,= ca^ZR+bg|YܓIAk[+?3+-6%(E@$G`|py7"%K JGP*A)C2zukle&>zPZ"bKcad0G0e DJL6d38*+i,[B$@ n_E4;tli$r哮LD jVςݚtM-PNB"B6UIaDǘ@t:F\[ȧz;g?/| ݥ,K,˔޽ejRue@ *GGGeYzpu`0L&u]˿Wڅ ̫UMqE][`vvv_WwvZ]コ8ʲ]?Z^z΅z\[jZ'NOtG0q}kp{`ҥ_%bݴ:~3WG•߿ΞhRo{W׆<<_?'8<~4mF-Jbrߣҵ89%" m,; 4Rp! XL xa[wI,@cUb}jC QQbQ٦o Ɠ4I 1?*`t`7\V{;SyjiJ d ERL~WMݣ괗kf MQc%;qHMVp49N"yc'u9{ 0tilnS!iKvxɄڳ\ekPyV/iB0kxǚ&ި2<$'wMZ;gZ79;?J5Q;E`0oAhe@eT^|DT]gup/^{Q{bRv% }(c./Bt$uuzi*#]>!Z7MyׂVU) vnDr`Y@I*5 $wurzǖ.>:2E 1PԤ" {Kez )@75eb,[5]wjޑ^"EF2eE! X 0<)@,l `q`0 RX"M^8ݹ箮ꪮ}kaϮS_1 LCTsΞ_?3Z]x}CIdxR44 $t0Edimr jh|UԴ gN AI])'(3)yOx+7q-p9v>2h@d w"F<u buz< PB0Ș& ;$0gseYfrbFŖs"& l$w.l',v`1L蝷hbk[ApЋF0Z+jEc`>NnhVXP^ՄF%R1j Y ˪ԬDEhr$ 4 ^棔4+7I}tSky*eLbj2DW^{P3Ez;Bt̺:ͤW﹔!xndL|׼)d IDAT7~7s;;;eTC^HjQ"q\c4f3セl6;==?%9 {*Wfz 0˲7nf3k\tht^5ϲLgj|3! ru+\{?-xѨTYQH9$qO||x/6N K^t)MV8_۷oO6%,SջND.\  Jגvzyw:Ui4R՘[WV@M `0ܸqt2yh]xc uz]njޡMMn~zf ?\YqWoD@$2X?Q@K0x^={dv޽~o^GRL0;ēixh2NX{ M:_/'>͛:?Ѱ/:wn-EEa7ZA+M&l.^}/O/a\bcN6=ճ*3glv&'Q~&'' }v<=h,u2(눕pWHmUjkϑ2+wT!y~sZLV 2%=^U5m- |ff015DR_c5KI9$m4)Mfw\4x~9@vEٶ[A!㝈+2ų!d5a؊ !1   D nQsRMRfQlK" yZ@% @Ťȴc Ηmk,!c <:d KD$`2RH,1 2ܡ; CɂvHgZr3@h5YE΃ m^=vZvgu!oECCq\zf)e<[dyvͯF].B$`bKz6T@` ^{RoRUyIt:k4Rh0ԟsB`cTdݾ}ݻ08^ʙ[n曧>g?Ekё@⵵VF*._]]NGju:`u \U8[ֽ{_AEUU"MSEE3KEQ^׬W3ƴZ<ϕ\٬W=f BzwfR*s3w.ieyh{mz{*S$BI¿Rz IgEL_X[Glskۻaz'~,w7KgW_⬝d6yϿ/`_p;><$~gq*3\|e6 nnmm.-/ ڱ\\_<.t{Ǔ7{=/?'b;9\DU`|v 1x1<k_nE9#O0O'3K¥qy,ٕBZoZ-[=.X[rԮB, W,0dfĊhԒ>$ #/1Cjm @P|)[ U%\7Wq}u]_Asv뼢Юy PX1 8Ml E@.bEzdz7<q#V41^ $l!ÜREc"Ty(D^qvzW{VnaC*dz)$rtoVUn lpЂ,0Wꉦ力~%YwGz͙H5"#h瘤L14d1P$ [,7t~{?LT2o޺uիƘ7xC/ɄaO~uu0d%I ޵"^741wWMDNGbuUW^nuL#ol6˲Lu5Zxaa!p:+ qìºɻPX#ximiC%@5> 009f4HK/nKSSkol߸~?ۼ~R87gO 'o'P+_ʯ\\ o~08]E?'l0x'49 k;qyӱ..v)s -,㷿ןyHHJH!xOd@LM'A1BX|d mQBs|2Z( BM'i*ޙss?9Z?+oD4e. ã Xx\1H26?#E{k2;r)jFmY ;|"kxak[P:D8YA0ш8ox,,)Y̏QBDy >nZ檭WaJ}4|#xGQt#݇(%Ffy'I蜛fɤKþek>7pAnd~*#.ͣ{idV#I{4@0m,]ܺmLR`ZY?åKl3hŎ}sWڷn͊dVpRܼ}oܽ|l҉[\\hMCACC'`pLXz-\_N=3 eAE^.  N$Dps.G5Ǚl+:h84Kv?eoKK7_X5 tZ\ "TN)ڑJmRu q2="gӚ/gl%$ӛ6Q*:Uך`9ȣ|qjXW9jѫK|--\;?g|83w9g}oo^˫S 0ϊϽ El`gh9'V.o,%)_?6N9 |7~Ͽz+"L@oAqbVăǵ\Yndei5dw=86FumpnBeQ`rHUG#QzL^/JoԦ2ziTؐ/Y~)\0,| F ga q&9}-Z's{u@>u+>b"d Ag9yJ2A$#Jca(^XDaE !B`/PDBsαg}hB$ˆYņLJ Xˀ"E持0 NAr0h :aaӊ XRd)&WhNS81Ƶ"& p60y^X$ƂXx*oԼ4RD{5k0׫ j.=-5ŤS}M:sE5jڡ ̯S!@.3/  2/.6SBbIH9{Sjț#z8VW$I8VK-)qΩ=(Ρݽ{7 |aa;3~Wdqxxx7n__x2L&U;ToxIJ, }QL$iNӅM6t8[}U{_Pb.u4Vv}p&ϖ q+wQ$x!{' ~+;({ƽa >? ^'hعԀ l,X+ 'yF#rČD/ ~g=R;R CZsX yV 0dس%_Vm YpiƄaP8Y@"b 3Y xFv޳DNq|,K` >hWd&# 4&.ǐyaq{Pνq(J3)mXJcIiHD/V5Ԥ ysuҁI&pK^WGuC˨.BF\`jjȴ9^ );sۯ,fuS'*zt:뫫 XD7NNN:N$:-:4aN&Vu~pp{뭷/Z嗣(~U bmۤѼj{,S|I$UwyMoF-nU9 {\?xaY:TM}@uw/Wxp£}?{N*#b+4森r7#A|T֭n;jNvr+߽; w{~͋ do~guur?ړbx-6}pؠӉǶł}61yt{obt:ϓѴQ t靣^;pA|صV=8z0p8#H]Y]^Y'wK/Oݭvguu(x'h֭ qyR޸q+d v2|~['>36|QHYI1T-Uk9T'sF5LozOFɄ zs2}  QUF` cek,Zr^J*\Ϊd!cM$"Ly!bKDj S'#?bZx AǂV=Zk|1ց;b!Aq2g IDATcmgֹxa~qw擓a13K_ Af$<> b;Mg7F7A:`6 !+0z#CA0 |A@E?~ھV<ovfJwu +|Q$t, xc ť ^[o';+˶׺wN&+p:ZY::8aKDP|Ḁp`}{4q$0/Mf\t /޾sLJ/Gq|h'J!oX {i/xΊ1tZZ#T~I%U9]};񜃊'Xiލu HR 3Cl\~V_QuJ$Cyvbgliԁka R)ȺpwtT A(؁dA;cxaa²Acw(L sAĬp< c c s1,F]Eai*FyN``'mX ˆ&wL!cl`m>͍crBTph m LL~w!b:7b,sdR0\mVPuBCo_%@Y&DՈMQsK yU(VRHi[9i9Z͛ "/W*0 9O=luTHA+{ׯ//=KKK;;;+++ktՠw:E¿P*:H$Qn#"FQ/ݮ͛7{ 8~t:wzBR!QSI\F»=Z-5{bB#dWa] wܾ7n l,zK{q~Ε+CC㠕L'ӀL IV/'W'cnZl꬇Wl;w_GyK1GNGwn߻'z+N0YZLG$&YZ ]2qVI`!2m^ omdpRC$?8)VZvx|/Q*}i$ڃ’KWO߽&I'VLJ;o]zq:ecC/PhTJ[#7u%yZ5Ea[U$bhT {`TXJ|d>&03Wt}J67k=#878x5/j:awxcmh!xn['.32SbaGBab0s^=P8gHIƈ,dQ$ ޱ A$gh,p6 ų Њs9XCD"4aqp;Od /\1^E"$##2aΆ!Za K18a+h8uԻV[ްDa&JYVܥFʲҙ3n iZ ʩe(kՈBD تX D>Gjx r9孖KrCUĮR<$ʥUUz{7*`,gn&xDQuPA.EA0 h<먨sZ;;;Y---i^atMTp}7qΝ#8c$.///,,TP)!(\rʕ,˂ pz0 q i*Q6I72 TA3kqěgᑪavynلy ÏZSU cSx6fD-ϭEVpi꜓ IR2+z*rE2)"zՋ2u.SW_}u2ԠU4֖u6T޽{Dx ⋢G>OܹsRb$Qg>[nZ-k54W<%3Oӣ4M8:: 1S4L&j8nn>U4]uT߼ Cǫ|`*e_ᇋ*h{mn{}6$" u,ѿ׋wJ<Jaead ."[[o%-\uvrR F-Ea6/]ߞfGGS"W֯u\_u,;1d↱&yP;{'!HOOFc&&i3<ƽyu8J]rFxFPt+~!y'};AO&Ѭ;Aқ(ay{{sc xf09wsמ~7|f\J1 5(a -;Vc@j9KXEB/gD bXs`-.O`8 NXِ H<fDdne%9:ܨ%=Cl~9WS?'߇wafCmU٧>UL] n.o =RܱD{B9A  D bX +Py2;Gd2Cbb %6 &4aQȈRՌ+ I$΅& :JrYD8{DV@ e`Lqx,N7EX4`8D6 yy\@ di8Ʋ>]ή"R)ѳFP8o+tr9Bi\WQG}ҍSitPIq̦Ub>>/BOD"̍1NG'Qmook_=ͤJttj.^nUྷ3y+^Z#ާi6oW.--mmm=S^O88ZM׷Uы݇Jzɉ::j*vT^zz*f}o= @<\Y!)?Ch{mmV܉M_N0J%[`v@${;ɟ'ňo6/?6ebkQHtv_~/$ df?lӾuFԕ~g_S~]<3Lzl$> QrF#hz;, GAҝvNOOLo?Yi&NYT"n.l.Y+hq^ܽs/DOā;}τWzK Aoާ`M [ګo؏~҃[wڋWOO^X9&s%q 2yV=}acF֔K&`Q4yDHM#X\\ P޾Vz:Ea(@<yMftwn^zIj A|xiM*#L]cґ<0 F]La^{/^hB|f f./`S2 ȡE XLh; ;! HH !d#qyx/Ha1` "^|C56"c53#LF.SR&<ϧɵ0;0`C//N#{8$INNNөUha^cwh=>I0 &> Q)F_+H2Jr8|-GҸm1̒p2tI ~tam+N{<|OrOG)'Փ/&׮|noo- F<,v[m\^ ..#;օ4S{*X//c$IÕNk$V67|Q N7<=<B* T o+JKXXvֽਗ ~𐪇f91;+ K+yi^)8^kۖZ;^U|2GeAAi޾ގ$vNNK-&a h<`pڅd;ːlHcGEd, 1h ˔ĵsCP(axCDd|2Z"VW|!Kv6peuyI/,>9(&,u`1^T/J`CVxXlm^::~ wx8{$Y_xoA]BApg{S1!gӤYZs,œiey> Ac )HK>4{"RØR W9C&!@E,2:=Ig3@h8όha7MDEAKwC2a-LJ&voaqqlcvRYJOO֠wϟSTAɪl<y%24sIN|`08/r5&@$  sg)" Vɏey'}9wzs̑cYI n I hΐ^5 ɀ7^YjHZBCm)d5Ƭ̘7}^n"nI̻Ȋʼ u2 YT#A@謳(YБƌҎZ A @`4֒c CfE0+QIPT""W2 51UE"*QjHA4r%Lja֠&N;TU)Oǎs8O\\Y\I4g7{^T 3O{DXP/77 `#y ux~5)1ќrodqQS{nޫni({G=Q{x??G"x65s}j~??yf7nܸqsk=h.˒$I8e JhG---nmm㢲,xZ-G}{qZ|Y7MMj} :ol'ϭ[`0;ٻn4X *kEIs<;_y{ϯ]{r>~yyc+/9s}6+"q8XZ3 œo' IDATFKKtln;(*4N8gE8I{0bЈ8FgV@ŒGeݽo~ŵ(9:4 R9q"HP4 BŬ9ԨQ@&  T:@@1r; rQ8X Au|UDAؗ'GQT1\Gg9n|"3lZrZ`%X}U: M\~NQ#S 7>/a^zƍNg<{˲Q,H0o~k9 ,˲(n;h8쬬qlF/0 '{K8GQY^m_<}#8nUd2 ð3^FE{x)EJB2 T=?߇oȣ}tLǻ+O:J 00⫃[¼[/wkb*?9 'C'}fޕKOF/oqQUVv~]vחJ7fo!^xf6]^neX]_ h*`YAw0MIGA8X٪*Ri+jA℥o 5y:b'uŸIŕUpecga`XV1U+HvI:M.]U͆J KN&^QxRgZ+K"&#*94T㒅Ǐ|)Lmkԑ;Njtze՜ F7FƔ֖AjWҥcW(Lb1d`+5?N)B^o)neqrr ֺ9r7jg K<35GK[x<V&)gB*.-i cvJiH Yq,H@4 BaL&,5*f@P 0(@B rU:R+"^;B'n:ZG1UsVRZh(@K~酥VC"-z XPV 5YWV+   h$[BJ9Z/Rh1L]vX{-w;s+#g*iLfZ\?% @8/9R#:kwp!Ms͗nuq{I%fК_ \'>AERYumi^ۑjAM˽95MSTZ{ʷ~[D,VU/,?`}zm^܋[|vd{G^T Z[d2iZMj#L{PE?=?DA^x6KsFr+<+<:GxZw6V{#EM2v(S0{/96?ʏߣnW}FǓ z-$|4+uΦ%#P |==>vcA$o'^YZa:7-pD*+`4:i/%a?(WeBfYpsu=;I֡!@(8f$@EELumnR1ۢ y(GKKb2fmd/m c&m_ӻݾibғ$ٴuH(H]c'\_UN B: c7G8oǦM_5{ KSӯ8縨liφ#`' ,<+r6:ց3Haf HueY"5KF3(S(Exq {͐EWW>3r]UqzYb ɫ:k !"Y,-9 b`Ai hB2 GJ"[XN!B D֯%öN1  i%W8q>͆Un~mW2mo? >Tal݈?|S" C@Eȕc)8pA TD4Bb)O<+AB` D/]̛x Gt,,X_2".*R{D`:+"FD 2Bś(пr6v[?"؋ei?5ݮx[W4# r3}׽g}nsv2xn tj FB"gKZ*<#Ajch 4!R(RLwyaD) щX ah(H&`t T"JP|"My.s?כyD85UEVV:4'+2U4cƀB@hǕTsDADi cvAUGb (IdҊAƬBĪfp#[[[y5 5 bG#AVG&Xtǻ~kٗd2z+EQUL b'^*eP+H@9A +m% 0,Non$B3"X)ϣ`=esh-80ב/5Eip 5nA<^8mլ@ 9n?.և](@l/ MtYg9?}^0\ZZjJ^sZ<_(X\]l6keY߸q*Cm3^Ēeh4<ϳ,󑋞~1/իWwz $W{NDfŇ{A1YyU=4o~FMJy~$o_,~_ٯ|9bRʟY PBX)RXsNDB\XxQ~5V_o_C Fဤ+FT>/!c;?8~$+eCa@%IDX'3:G@Mw/&A͝(j,1 )af EG v*s?| @EKKKQyd2o&7|Q Zꫯ~-U}t:z>RC0 =X Zc& 9<<{xΝ |p/Q:99f4˲tX앏ժ^[-izny{V{sbO?ϥyE59_gԍW~?f(WwfEbE*w\ԥk_‹!eƔYIAW+t\*u;ˌ6Ɛ1)Ś4tF:\/ۻE*zYAQIFh)Q A1Xo<{%,m'peRzj>>XZx~oӱ$q`֕deeuq,΢.u%M~P- .T@r?B\p0,ds]h#ZDmN",/BT\8;l8fY:gnꬱFQRǾADB HHv $Rt,MYÓ2<=HT{pN! ,غŵ F^@Y<<pH5VB jdUҬBExݖ8q 2e7 ҔPȁiGEQE w 8Koٹ}sosOh_//Sy{QZlLyLzLUUཪƘ>,_8u]t:ިڤ{zsxyz3\ /i%.?i8gH| Vc:+1@26~AS2"p'nex 6J9#R B=lo'oW_Ʒy՟|=uZR! 9'a:M(''[Q{9VO~kV|eeP6`n$S Nonqk4h(ϦD-E (XDEοkE9A--EZwZJӴb'8ING^$Rt%7J,o]A1?Mמvq2<9][ٺgurSѢ9-'H@Ȏ9cf):A)Fǧy6Z-d%T/e)cLE73k|D9Hn)atrZE/\l3W`e>!|!Xh9O8͙'+Pe+LBs0i 섑t@΁R ^n-eJ$@@PXc $)5 u +$aΘ*4&^bt H MBUhw$p<Ӊ=l)4^{ST`1Tߜv;Qa4.(]DbK9>C@R6?y))"O< 댐Q?Ҹ'@.$ɼT fʁpz8ȽG .z}<_]l`0VOa-vB#z>ϿarQ)ᅰ _-6]jP}X{"zI||'=kxE,MӢ(olmm@iZVKk$t:IƘ|ߴA1^HL&/HtLC7\3;|Pαz ̉;Zü(?m9l`:]j965zVYM a\wG/hI~;~ɻ;J@WQw;INP!* f ۳ɩuV0 qn:isJjZ9#HDij*AG"r LHda,ŁF@;wGی IDAT7 :uFvIo.UWwx )@fq?H`Q,I! >~~y9Yz \7ѩ!YX|W=lQz=`uJ }'z ۋ@.}M-iz튗x4љxjvKn$Io>bn;i&so >}l3徖6z^=* &.͉knS)e!MGˣ _/`P ٟӛzCD!ӗ_}}OwOVp vAӲ.koU@Tw=s3l(Vʛ)(X} m]j'{tg𬣜4iHcjSYmȉ?qϘt}c4ǻtQX㐐*@ PL_|f7/Cqα),M!$/WD0ܼq׏[QA3 {[{# "p8qXAGq1{@< ݄ C=^ihi [#`Z/4R8@['|v:NgW9zcm}<Q\K!='c-2-&j% ؁:D"km*"(+wxwi [ꮛ^:c1}q־ ߘsqiH J c@!Dβ#.&sgOYϮB&<3C}J=mܨpʊϨ7zL,"{< COa{xAx\>fp8,2MSϝ{ߨ,oxzwjl cO]̅\ ,˄|p ?6G?n8P$L,j_MN_Hn>T>qW|WƧGN&'/?o #wm>tF]L[) EI||Ӣ'/H$g1NDdcQjhpv:gf,Tg@g;A.iͭ%۷WW׻Keߌ@Bmu{Iv`2<lYn-ol E)AA[>smq/F*&+0n}mYi/9`!oCv87G@^C(CW::R`rЅ^we>4&؊qyENj ( 6>[QPTeZ턈48I@aPSn*d|J}gϝ4qY̠@,J9q BŽ- i *u\)m tHJX+I*iwYtMiLYiXa "0 A TqFJ;LU4a/PW,D2(F4"NBee52HAeaAF`qhw !* ?u\^s,w5HM",\("((‚`ޤecc[U>>yMLS'x6BMy˲$"va3B:Q{~{`dr_O{3=T!-c Q8}P>*AV> Rw+eˋZG’29,J+DW"J*f֤T4,B} FLBJ{ڟY * h@Blw]mjEpSBI}@43av윅8P7:eT 0;@s,%رg[9ȲgJX"HXki̤1^١("v[n;+JM: P-D0AǾt0Xx:H4M*ĭʰ#$`]EƉ54c@" <=s#69u}/L9tsHBQl/FE.^|i큹."{[H׉)&{+}N_'*} s?9gch? #e(Ji%4azoS~s{Ia~eYm=~PιxbӛX߮ꑺG~o9":o޼$ё~0iE7;?ڻ{01J<Ie-_Eu0O xt2vSTf֊{APj;=2>< :&G?|$ bخ<*f@￞v2Z-(ji8qE$x4-/u{yLh]Nwǽ>AvAHI$p x!{{eˆZ#gNVrVã}c6ӂ:EJ SYe@;6^X/jc_BqD`+I,l>BƿS<+%8y #EMI)\܀ѱE$?W`gWHUYiE ; S:fr1|P*S!FZ9KA ru||hD7_>7^ P/A_"Y!HEƉ6ÀetJt:`tc!1dY2S J uqXH"y5ցX쬕T sيE钨g\)jfbKAdG9T J#! LaFq^$ a6x|P;@Z% lDZcp eSq%"0!:$FDFt3ON>wOט {.8йm ($DgTsԾ=7&JѬ|K).TDE͆dG8 ,~-wI uMGQ*H=z4"ޠz$@i#J)/:>])0fe,Z _c72E{&޽Y$q7})7stxоd9%yg};~x>:_ղls@>P.C(DO|>wތO^~cmm3>ے ~db%*V_?˽V.uV[!XL>cb% tB Xۢ$Y:{sOhvӓP0QN1p"U?$tXnmMl4Hۧ? V{UZ=T' ۝ZtOONJ 8PyYUl*b':@Gwb+*@fS>aN|I7uK|H('щg (  QPY@ȧ)X][ :ҁBDT$"13NRD!8.}%dk *%$X=)P)DLtPU( u lY$?B#F0V:IR˖- `rN8B,lYD@ z"20!iA' V4BQThMNQI15*1б8P;|iD0cc! 5h,W(ZjEXH:Ģ@) ǎDi"(bG0K_Z=0KO'%+IR,XA'8k5CyjV%^&ouoyh; }ȐǵK {gnb . !/3~㿓(u ׎0NI9=yDZKe<26(,kJS,>¥ɇ,'-{y8EQLW8*"zk׮it:a@Q|zz:NTi>œFϐi4<^3־F;88@DhQe1_Wܹ;?=r>>VxtOL>`Q.2ֺ 5/h2y_x%K$Ɠc]C͸;{%cF8C]eJ2V Z c$q$s!sI~t$\P(? BDqbNxtA@,CysB q.tY% RM**+v*Ir 3WBb+LYs"]+ f.Xzǡ}o$:a96o`$BtΖd9Fkv3L.\ c{]}Gy!4sD= {~.ۊ|BY+=x(0E:yyC1 3s\֟JՌz 2@8GE? 1vP\0bslpƘt29oFtuΕeQ O`_ =Y^U֖޻t`0fazi3* K.y9<34ޫ|i|16XxCDk[4˗/(O|:~ r''3\( "V.jя_~kއZ׿;7N[񫯿{_w?ƶtvٛ s>.hqWYPE;O`e%3n@8#, XX˗`;W^{kal~t}*`Y(Upt}i#PA1;7-Hےinź\[xR/`JY8({\0aZ%"QbP8S"!*Za&]]X|dq;wi' 'q\۷֊I^sfS2>gWw|I%Flz& D\~~=У?dx  b&aMxcJVu~) Ms}3|ͷ{h-xJ3/݌^Y1y9͜‡6kkeĚ,$dYZ6  gΝgV(ꏲiR\XYsP6襍ͽ;@$ z5ۇZ%YkUrƕ;;}Q}9?H U-oڸ4 Sf+k{;߻q*6N9|kҊ/g*ysÈWq̗g@H *fO<ssg1{rW)6rjG}@k@ B@$q֘R@l,%T UR$N[ccR,˰{+gL;e<[1fV)DkYiDRQ"v@~$B@ p@Y[ND,DAYZ($a:&@KBhKT̥sJ5 $ybD%I-е(\YIk6BC:)ij,tH A"P6`g;m(SguU5"΍ CǮt\2#?3]FAG dIڏkfq~R ,Ht&сcU(B%fǬyx9QOYP-O {('/t6|ri=s Iɲoeԣ"_$`0eYvNEQqEsڋjK/y4y`i3 ߪW;`0`Wnll(vvv<忰w9svR]n~?WНp<?C9ϲ4Mߢ FSTm?ydtck^<{Znk/껰~i_GZw{4?qs%&_ʷ=ȭ–fTa@Txxg~y1;d(Jd,44[q[°6J3 ٹEHXfં J>}_o IDATuv:BgXhnoxwts'?8^}yk{{:VޮMl:t5]*zvcwk?|5l7/z% cCȀH^%ˬ| ˌeB9L\8Jim4G-bF1 QD#*9g-$ H[dg9b"@htxDAvJ3ijUR@YL-0*G@qޅe$4 A) 9,C"(=B"L ` [a+ɍ rD$FWXHJ[[o'ڬM4|Zs)bZS͡RXQU[bZZg!8OqyNahؒNlVJ̻wv [+zl+\*)2! )-X*$PׄhIS > r_&3qprՅг0ν#[xVZ!K˭Cx]!Dc\WB苁(/R,g ʂs68_(Ks-.gXE( _4M;yĤS"|Sױ$I_L&iZ}K$J8hiV*<~ūS(Zhx|͌DKnL_zLﻜjZֶ^xKs~=w[P@݇x8~Zk{^^`2ywtҙ[S^۸_|7nujcFnܽ}Vsg7OELg~|3Ok7k62A_Dbyi C{AY&t{ejRIEvpw/YR @fQ4c(s>ӢՋה4f) 1+ufFE@˽9"3_,qy׬5]A<8)g_E,엉%ƫbS =^,X_΀$k1ZW*O篬ečԼڗq=NgmmMĎf,,|j2Ahv'W/^xt}.ᱫ_Vsjtԙ3g6^WՕO]s}{#ve'.P)d4NF:vìt|Z•VuVdtĺ@k(!pHfXwYaK*4}~ߠ:(4 %K^: Tn^8< zkyTa2Pi6UM{aVsݼ*1GZ=7R3[6ϵJ]ש7mg9l$0 Ol R͞~i! @$2DZ K~3^Y+n^Ex6A7jxlS.ɬ_¶@Y rQ s*h"f`fR RDլ,XQUuFY Y *К1  hT,,(Rߜ Y,[ z _sMgi9CsEQL&}ދAZzwb'Iܹsg_hZx9/'#l񚜽zʃoܢɋ[{Zye_|q߼y駟7?> FтL&JίwHy(y8?z@~Gv~>v\hE>-?|}[7oU/|koaZ !+xgpAۺ|)^wfHqڣo\}jDV!f#M2gE"190i.a%Z0,]73fS)Ubϴx++"^_] ;Ӊk])|]+d"E%$3LP}Nk#M3jL,*bx<f: te݉qlaRI Sf#/Pe%Cu^qd_ M.-u&43Ǿ:B,vg(*]0ZgyC9 |hIaH̢6k- Zkc^v#hq ,OZZa/( ($%8}LLMARYYY0`tzsƓYNSNR,9, TL^Db4(\dqQ5"DEI!lQV04 @f1y(P{[oL&IM5زm6+wLjW6頳~: k|_MlE^*" Xȱ8j=(Mv72Ywb^TX(p>e'A|5' J@@-{kY˂be]}f吙y2KyH^]GO->!pJ{'03+,=/[[[VJŇB.j|κg|;"yO0 gpBQ>I^{K_|*dY6LY\+s /kۃnz.\8<u޿?z|`x?:Uo:hvNm}PF~jLon0*$4J҉:LQWK9 McřTMLI&GekznoZQtQ$b*X|ѵ0vݍ67+,NjQ(iyH=^}KYqݙj`jJ:J,g0w"1Km@PePk$"@̌FCe413:k,R>-xx7L9rV0qH9v%[U*DAU-dqb @Ze!bdJ "M GA9"N*\F$ hF1H "TX [HJ55@QLA:I[XHJH4#0`gN2椂 XkLb<Ƌݝ"daT9vCHKׄ @dg Wkݝ{;h4aS'31 Z@!̂ Q|"2G~"GeȠ|/'(v8=BG2;. ~Ⱦv^v,~Y^fUJ#ςGrr'׏.?Bk,t:,[ZY4DEHq[k;ŋealG1 >GVwQoO1xfνg>K? p&O%kN}3E)E(^I[Mg(TJ{5ߡe)PccX@ff "')drow{2*E ;"nmu$"!e+Qs"-KPiqer4X?mQ̂IC xpۥdG^H řR"4V$'&3* [ uT*G߸?VdۭjafaVumɂuhNϴ[vOۼym$^ 0RQ%v|}a{{xr[DttH͊3BS FqC3#@!;F`E CD2b@)"[nҘJg@Z2!afsTN +)ˆ@hMɆ BVR4\ C,c8Yj\ܾ>K\|kK! (%">l֌7M'YRq=-p:J+PV}AoTewlb`lI]]Jg2O47fg~=(ʴ{(J@f9|To) PՊ Si4H0F+в b<ڰ)d"pi>ǁqznX*{v)F1HcUj%ˤ3Lt:Zݹ{ҩ5̚"s_ǟESڗyOJ )QhK qm٧OǙ ̫ג:Ǭe0C['vDfǥ-aHXYFb [X04!8ǂE$2EW,3+ϫcz\X8teQ:IX C[ؤi\<\XrnlqYMYiǍh^nwR'Q^@hTĉUj,}k:Pa:I}eeuooE:?xՉU4BN+jht+~lx#*"ef0"gވFW_{NBZ (jsJ%H΢/6(KCahBkը5!ZȭM*0#}ȳӣk9I 'qe"4iV=:ۚ.oq)2$%S Y2|4/P`8]wx@=βbf{w(e)d a'mgA=cKYg1'68Fᱬ߃G^ u/+Hd݇9fYj|W0rJZq4AiZV[֩Sysss4}'^{REB<輀~ѷessskkz4 k׮i;,'2?dN}|Oox]^o'!No+# `njZP`NtZkm-jj5չ~m-kgXճ,a]bJi֩`s+qS\8ns6J)c]Y@%g|{? 2+ڍf:\?c!.7Vs?v{kW.Z~}_l5)=?yVmVo6[fǕ^Qmݺy)]ް?vE@ZSظ"j+Aͪj^u? 8,k$Tꨃ~ݼ2i%M" D |q]9[F$giaKDZ)D8; (Bg!P |"BE˄W0T_J&ABEqYj CAո%*X) e^$ ήC d|jHť@(u$Efnp2Iya)QĔd f7ojuPIP)k]RM$I 9ir{PX_WEa*&q"bB"1") eFfjk< VŽ3Bj5* 7VySY]vof[9p  IDAT@"q% U  рWd$*qLҌ,ƑVXDdC,9JiB"i׃МJ?n,_>OF@3e>)ne.yY /(7r-ͷ"t" IGKDEsEʼnd%9׼Ueq eY0}q/Zrw 0MpXVW|.[{ոݺu+ 'xgRu=Ύ߾r׷S O,V$"/Y=;NXYYx⫯Ze@܇x8|Ie~D\^#0˻LDe֬t4M"M;)KL+/_:x|sZ@훯mj&e.efI냼M8uuɤte}JQJ H6`s׆l(90er_ؽ~䑍:yz/ۻ3San&]ag}ްw=.c"wg<3F888hZ3sGw-9x8~-@N)E*m=yPa:D h0L=On2jj Դt.֪0Td44[O;$C3Pl:{W;ĉMSkJl>qןͷ+?yJVjջݸfBPmo7ךpp0S0PsګiKgUr:Nd8;nVl~8պz4NmCR֛`Ps yo]N23v'-ZI~2ߘ= kŧ£bvXffDϧ sv :'20"lQ8H )hNs\GeiYDkbDkӑ8FcpqYFcnQO/[ (Pk +>*X@fNX1 "tb[ r (!sA@*2)gLEZq Iy] /[sh/6N2Bw¨3Xh}}=IR<#('˽݇SxחqO:u,hTVjՅ݃썍 vwO!\~V=oYXQj?kkk{9vպ~w[o3O>^n^/RP oo^>{;;Akko 7yCϿfs)u1.A^\5֣Ӂj:Z-i։"bȧlH/2E KB O|BJ\(M Ab>!,̬5*-*2!+Ra!n=֒( @GY;*((Yc9} Y=sYdA?y][$2:2/,e<@߁uTXqETAU P+#kV+<4. xVf#WoF^Ɂ D%fG0oq9 = ;w꽀t >Q*u[dPjs(J.@"yV*l%EZ FHX3"Šm6raA-ˉH! 03{B@(X҆QQJt@P.-!="/JQwiq;kn|o꯼|7VVV_~}S7˔W~W//7qƍWO}c?җ__ϝ; @?NVҿSz/ė~dc +ctJz*8)ݴ;XZ"E֝;vgyye|[۽?<~%T;g>‹]}ݝ1&A/ao뷿~?tm…M7>w ɤ;L]6-XzJ=T w ÃSa0$NP#ݎ pvΜ|?DyM9>S"EfA7[*ja}VuDe,V':JlP t)  &fabat:"/*GݓCPG!"$Jik HSJRI T]k5•LSD(5s*FX[Zu?_jv7qވI+wS &w($Zi8DLi14-sVHPZ@ l@!D@a{IUb5)"Lf\a}@"ExQ0>+}A̠,TK}FYU<3p$cRכ"~9<{O=c]sud2̯֭w֯Rwo7O/?g~>~zv?~şO޻?_A\kݺwwO)RGuN|)9|9ʲA6$)i4S?7w<Gxs*#_r"S}F_{שiAZ^ٙZ'K<ǟ~o^U4M-ghR~GOY)R WI^0Sɽ{{6wo{w~XXW8stZTyG?TLj$FFaLw<.C[L]~6A|cJ{\w=1y+_Jgeg{a\nNOtO?O9۾u޴7̩I9jדӗy6$Պc鷛(he=Φp:ǦHۨne֍TyTNj.Ey"<$ݰ,ѷ "!Z:@'V ! 1b%"N #f RHH>RfĞб 1YpV4"R)Ʋ`)[A ȈfJe.3"bֲV:(:)[̊˥Zǣ0B A@(e'ڑC RJ !BRaYr8moJDq>S}1$NӂEiVi%΄: +K p@aQfDր @這!Y@ r5ywA d4a rZ d$,iΎ"/ K@ s7tZ| sU 4<ΙR5te/ܗlk̏}V|Oo3/?_U~66V~7Yƍ?:b$k,Gf7ő,K5ʳ'^!Ng׋Xq4z=eԗ%-s樵^N$Ib(cFYU*Jx<&F3$I2!x,d6V/P؇E.<^S<O^[[zRg=_Cp<??<5R~.&ZxnܚF Ark{V |b6ݫĕ0C'^3O:fJjL(a=sXuyg_4ƒ,;;o̗kUe6k7g83Iɠ(,6em )AȦ`Q-@†?0$ER$NOKuuךUc8/2MN;? ^w{=;w!ǘ 3Hr Ƴx47kq7jFBXtҿC 3R+ݾqg|ss\`Suc@\Q9 HX ΃҆ h{0!"`s.UDL y F@${1{/l;)ftS |9%rVr[Ȏ/sX'Yұ|H0!ܗ> &R!%=/Ͽ$ҙ_G^_n_կ~??;??WGq~1@ a6߼A{[\ I FD47fٙ8'o%UjI 1>($@Pnhh9 _h֟={ʕ+0!.Qmu8j "t4M$Qg]#z0 ksN7]q/^^q<;;ukh42j^'"fz^bUsR! Ofٹ}veKKKfsg{+olFqKu#ߣ?i{i}NҗWڳ+:,|{ <a>~ +o.r׮]=ڙ\prw7 kgJs]}GW޶d̽IkMٴ;E>y$Gu`bɎݏ1TDV5ĩӍFcgkk<cEX|5[  #L^LAy26DF{ ""pLd Ѹިg , 1:b%(O-~0G݆ yL-{q/nkdϏG6zy'h8eل6ځovuk41YήDB'O sZA{p {ٹ4AA@B csa"c$aΚ6~yKAhE  (9+0hklx4?cff,>gm{A6@b!/YH¡E"%Dg4(`F>"!{!@k)-_met}x9W1{E+!T]f""&@VBtb|A^18 N.ͷۭ g\yA,䡋7qkWߺ"zz7ac/J'~/zǯ[7,yꩧBG~~|ꕗEa{X _0 PSU2ْZmiiICq#[̕:FOᕪ&w*\+P@} MϲZ[y3Q)0|rպ}vnZi:fgg~m5U1xuuoLStvv6M4Mgggw쥷ە4@noH`aŏHzb%wy|lg9ӸagΦesDvwG`" ,p8JwBuͣ\Q梠 bׅ&P(EYvFxT\ 0,,湳$qFAc/,xر󦠽)HJ0"vz6M?LnlxrHjÍ_$xd: "@~>4!X4oThR>~# BH=gXq 8D΅Kyd+!IspMI! c2 y'(@Œ!FK%i  (&aD't%" F43"74{ ?~Ưxg w,4Pud(*$sT;XiRƛP|tMx3V>25?ɓ&=rb| Mooj:?|//|<יCvů~+ORgW?pVsWjs~/'[^}z3s|o *|ЖURv;$)\Q kнb_227JբwXUЩ*R൘G`033yן+rFS]5ޯی4M{BeYih>8(JDwu5uggguuGᄿNxc?]vLdKo~[:w֗W^ެ#g0n1Y1@~9KG!FAׯޚ]=<ߨ%s&h8/ m}j4Z3>/~,X ݴiK VHYAh4_lX]Y^\#;(xb7&6`o+'Wd~%N'O;7PzQ`: N 6 %w6nWf4323HpH{f8nO<\S2Іyx; "FP˕k4P~PGY#*^n$F,dUHTI'wy{F۠bP G bC$4"QhEDG<{cJ^)>܋lc?ypq2Iw_F@ꀸo+Rb2B^3 %aKdy@2bcؤ`mTyˑdl2lQ&]Әm6g/:|buvrd9k<>@ bR~|{B'ptt+BMj H]h$3u$س`(Xx@!AA#RqF@ cDcdE0^ & 0PN C3v\8|%u-_REdm=Dcz(  p !!0A8@Vrbf,d!Q-W0a`! U6b~3Y#E!$! ^ʔN0  (Q=r<`Da<,ҢZ=<V-wcPlq>rLID 0 #<s%9"$1TUaX<"fFPŋjYeZ'"1 睱* ZrgfBւ PAXP6 e)v#\fȳ'fȄ,]1gaN 2O"˵;~dF?gxs6_yP.[ݧyJI7GA`X|@fq6z>[6R 󭤟\~(V JzF,^uûh3ϞNzQ[;fgvgHZX W6B^C]x@9{AR@EP!(@H@Ȑ %#"y҄=:̂(ɅBK@y ;vfkfy,˭5Bq:KawDd |’hZ CɚU03z0<MyG^$(Ȣ>xPe{gQ;K # b E*P!h& !zC Eb #(@E0 InH#Q@ߪC\as - KL\. *>T6(F~EYA`j"..BLEՖ(Aغg&=s"D,qA&^υф@o^>wnĭotQ^~m qZ{7S!aw޹t{QέѸNV0amn̞ NtNuIH 7QɖqSYYDQwGghf;Yla O|` HXNJdwHa,@0 (bEX}xSxųW" u^"JSEs' !"+HI I :KKaLs&(*ګNN=3@ȹ wle13ANd}q,؃ȬI !H8Α+a]H@^ iPI AP;DbU.Icd @B&$ AD< :ABѭY"`Qf.?p(QR"B]FS2|33`dIP@;ELs$Ί\EʊEV5 bc4]aSx7 %5zyqZ](d,ˆá,jιhn;, ɣ)q+bG%`sA52.{NO2SGqGxݯM׮^}w] C؅e:}642vaQ*s7:Dvң^-3\UmLм~ӟx®wƻ{Iclݩ:;&ر h' ȅ7^2瞙;v[w6F󭝽\?j=6l& hiᢻ=rrTc7޹^'՛"D7Eݬ|S;h[wzQn/>Efj4]?Ǘf/|;ܬѝ۳'WEcgE'z ~?p~hmhZc"0>/4cJw9~ʢN|%5I2y]~d21aep AT"Zc (̓hbgY9\,!lnTb]p~w*3uc(βZ_$>Ch(a5v2&@%)3 CnE&l4kah9'XQ<#y,ƈ yt Vf,E""k}DYZ:IYEih f1Xf!O< Zf_kQeBBBpn@"cP=(I72"%dd?dzs*KKRb""ϽIy8 "`{@ixڗn +FBb,,"";=D 1xxVKc]yى3"KA(䅉Y8Z(Eɰr-_N4:Sm1"*ޖh4LV_OuաWpi H#///+{LbwyɓeYt:nݪ(#{މ'_6ntX޷W'F?T|m͝^=w0r394M݈7짯_Y]] xg_OLQhe@@R e;fcH2w>/{1d1"XB̉(7NEH@DP"kQ$HQ=;3~玾*<{gϜH.؂ ^rkjQ9XK Q +9k!x!j;;۵Zktgg͛@X^ZZ-"p٥v;˲Z-EQIDaV Vk<7u>-=^E;Z#LC(SW9HuQwZS}DO9G;6^x$ͳ`뱪5QL^J]W5'"%T, "PzVQ?RJ2,'H1L,M"@UL= U%'FΑ@%y߭̋+#ԕb5+Tu>'+B& CRy@D`cꇁ-TgJmw';^f#~ "8e(,2%biZQ0!)O 2bv}Ѷ2v*̄\Ćt%RuHB Qb,{pme݋o~ H2Ns 6Hj1" +Mo0 .zsQftqA|b) y)~DJA0gcM(͎/=Bt"8YqA^ JAORd⾖"Pȇ=̃(V u1#L$x8"G *JʷoƒN56 afZe: MPN.+ 'b0 "5up<8B?DCsbyuQqR[뜹";)a;c ז"Pֲ=ceAfO '"2 wuiZ)A;~hFy"YAȢ py1zS}Yu mw^=ET˫ O;F ǣQ_oǘ[#HJNhL z  L^^ {u%a]9F7+|;o$ATUЃ {HJif1X*Wk J }i6,[@ @D~Axb c-+|}J!D/H@d׾YIV#j=o u"%eǑWxxwUT_@+xzA;g~#! kߏsEժ|< ^oUOu*<6~kt{n)đ=v'(A(r\G{(rLǎo3M}POr}.+Uy-&yWoxW(4E|&ˌ GM"\ ߛNj/xcJ OAX^Hr:n>=:E{ ~K;3d졗^Cr|,_bGtJ7'M폯@~{di˿?-TweiS;|wt0LNcOOqO L;֞x9+6}{`J ;MmjSڟԽ]iLmj uw^ƫߒ{Mx^(NN;﴾N=*i;O;G3Mqg;|{iwZi}wZv H*;PDJ iUt}Ԧ6M4wtwԦ6^G =z)EԦ6Mmj~A=}4uD"Ɖ MmjS>`wPM#㸏W&"U]1`!G&/M=h}[>?m 7Ƌܷ{=.?r>b;E{ ۧ};{m*pO$ t91?~ s^4` ήa ~X5hlL{083vo w^ EtYLˈc93gf~ḦHHZ@,cL{D"cD7}r!DaG aiz¬c8e1d 8E@ ,ܸM"";E UHW "rȡ.gEDg;Zvڵ˯[Mh i<u}$iT91gϜ/g{/͛Wo4Ai4Yz,~o'4'rݮE ΢o>/W,;dޭ#@D e2e3[/zp^&r iTpb bvLյW[3?|zt(d[_~7swAq,̭V[??t^~mP&ܝ2z{}'H!"`e^xD1Ӡgs k']7:7J3(fF𞁈 BUևH s82~8椘ny)sXX(UyV?#+ (]]Y< 623 {a#hV {(^h%x(p 3͎ D?aE(`>M (cI&aT%LDDnĤ PgϝkD#qRVK@ј@)j&D%HjtYJ@<7GR"i "Mt1+(c'L *PpD<"BfӦoYH47qGJ="0iג( 8|Ѯ!xpXd)l(eⴼ:|^UU@]L/m3eN|R @LF@X9ubxfb52ȍI0c ACs@±/oa[(lf`S7YBE|tȝ+N꜎cw`7pj)SAL7+nXMvVdA2O Ct7IZ[v)YUl.Wjδķv;lHkm5TUUWui:Ic)[MI>=+RȤHAREZkY[7!V3(@Drژjvj0n3_RD ЁR!\Ӎ͍Nb'-t\.,YEy6Ίä [)2~Ɨ^~ҵ H ;@VDH$3+%4tj[ B&E 0@ "E R]h}N7Q労Y6}g(3 /Zï~O<͛7\D6ZssV$9stYy^8皐9J8ƘjBvux1J)"8 ò(9IjWUp!/xo||% :8Xf8pekvTkgz}pg)jtzJ(Z쮓㗲'm?׸Bi'lT_Wu{Uϥi'dX6w=X{o:#"xDZk] fS7 ,d !s Zf|eR$Qk[gm4܉,EYh"ADa&\M9wjݸq+_O}K<( Ht^r}}{/_^__󬮫ck˓Ѱ(V9sәN[y1ͦysJ0yvL` MG "aheY&qXs>ϋׯZ'x_vm+aV*( ظ}G@&W^}ꩧN:`0ϲ^+(LӴj!qu]3s7~D'E὏]vy@ /"sܳs" Ib] ,Pq+5Cڝ'A\7` DD<*=)ǎ j<һ3K߬y.ԁQ9^68=ر gq([w.#]vUG⹠0@7^¹.j|,hPh]~´{a8@a&lR=n`K2%?z-a}Zq` qo =xPB' คtN[}mŸ˓ǀҵAtxx?d:5cVZ70ψ,em@D숔Yݹ @7J+k+Chh s^)t5ʽ3_)5LsKKo˗/t ?G qz^dk{Ҫ*$zJ~A0$ڼt N;x,yO8sss{^o~3MM۷mHWw?\nh꽽~+;{"xB:eux8̋lmmՏ<> +ߎtƭ[ι04Mv L0"Db`MA)5{A/wv˲tαM;M5wxuv>Zkgqcc׽;;"֭V+2fFtӴvGC!a8z[vYa\׶]oxT/;s|[M(f.G_b @F#FfMyf! =+S;Y[z!d?79vdz·_u ,B $ֳ'$RusCmDei;[hW(8 U$y0N#!A1101[ATFq})3P @H (t4 zP oC{ 6{2K @"DQP&' !5*RMIfv,&͍F&LHXŒ=o G7H_"@P(DDg>q^54F)[6 C4u@bDf^]DCVBEeuo Au9ƨ9tUɽ290drK}Ol8I>ZWckF$=Sު-,^)6^煋,] Ӥ)u@,k0@@/pkzbL°"4aC1FAqE]UikҨ&+ܮj(jf&IRUey/ 5{P)HYbL( Z)ED(D縑gU/$TBeY-..FNW\/2f{񘈪Y>G?aUUA`ʲJ[ ! HUU꫇}?_v< ]sg/zYT{/\P{% off+o^Y]]֝>}^|]bEf2_;qmC@>W_ý4m1w^@w޹3g~8NQpYA4TDs^x8Day{Y@ɤ͋4Mϝ;7GQ)2MSf.|<;bpΥiE<3y Ou{TW"5 >--.M&#I" @QFD4O _|f]C!ڣf7 ͬ}#,h n,L<`Z9OK_x@)p=V/>2&VA ( (<96vֻr2UIDU@ @EZ{iY'/@ Pу.DE c &eaJA!2$|o盈ցR,- +#폞{IBӐ;ydAQ*0I;EJ@f P51";-*Hb4*`F/GVOm' $<{5[5Pgr,QIb0DdV@'a@Lh+;θ: yj:@!@3t*7!Y(!FS-M[D$:L L-~gද9WG_ټp\_td\?|0G O򡰿j9ݢ 0le]u5ċsNbmiI^p?b9/ѻy'?c߮ 7_"PU Ͽ؂p8ʲ֕Eht`\ =܅ݳp &{ֆ47:}G>o{EpxAFaBeJ({07׷5yZopmhitUJmnnYc3kzA^{xED`0|QoQY\\^Pk$|E;d:͆__/˜ 4M|+Y~ε+wOc - `CߣE<;TI&J#1V$gZ!BzNHu] LXPA]S5k7­7|+T7 3H ԕt(q-Ҫkp5Y )! {fQPL Q1\TpE"(P(DWXhP@ l G(r3{Be_qӬ4JOm_ԑRaUe Lc=;/& H"QEk(`QܕЄ*!b) MlF8 (KRF&MBEشڃB1 j[EQzq4YO2wwwaY 8}{Xaayi2ȝ]Ds͏~#O=͗_~N{ޓxyu( ]޳s6ӜQVgg{/gi,^rw=߾}޺$I0̳LD0<~t:^vwo.7Dĥ8sA(ZN3U]yH0L$t:NL& 4?_nhҢA sԩ3_W<4+~?>wQ _Eg*vb]{wBQ@uʕU)>cǟz` jhnj1|w4ͧe ۺN?5 "xxI]Vt`j{ Z1)t;v{}PZZH?x寬\:@+@bPR~iy.Q nWUӯ.F󡎌2Icn ܶ">Et7+g}m8&MYAw"B`aafFȳh:nY<Yp8<$ 07xRJ{FmLY޳1aEp0s˗vww5ͦN;8Nwۭ ^gV:W}L Ah辝N /e^ֵslW(s4{WRnvmgYs#r1=WUeYnoGf@(WWV CZ 3"L'ciO)4;w>vڮle?ۣH?l 6~E輟Naik2ANwoogx{{4R$3s#t3&0 Tu]WUasc&d2yg~'g/>#O @ͅktyW_}Y)<~SO= cJ/oӜ:; ~0~l:w{w ܫwR0Ls%Hɬ?(s/Q-20'[?'ݷ~W_>Vvܷt߽~景<ɿOved :M4:CatFQ Bh# <٪l "Õ>6q?8oܸe>I86$4va@:(H"EyERӝOgά +QDlk2dĉPPXE(gokwFWdVe#3D0"^i >?yocemZe R~jb_["Ё̇#D?u[p޸tJ19`fBFVb!#DcP@Xkerw (fyR) p{ ׊gGHw@%Q%g#PARJeMVGG>(kV5p ]jέG_t=ϝܛf:Ã^]ߟ;ݺtMh:_|X\,*|Rݩ{vx-IL&2ަgxk:ZޝݾtW&3q8J#'^ʷm:}1LVu,^=-4qN VMRvty"tZW|eݰ Pў/ꚠGv! hei[[`qV#1uUniR: '?IԪ;כf*2vgN쏚v۟x<Ɇ0 ߸|UieL`[qdnnnmx]eU,f14%[{z:^E5E|!γ1A${gϞ~K_җ?Ϯ,p8uB4it1 Nכk:vZqc4 iá1ƹZ;fY6iƘ9aΛJlHn:{leZsZ,aH?#7xǯnN$;.ٻx~sww}Ddr ,q㓋Jg-rOOէ>һ'~A}[[7"\YCvWQq‹ +P IHiD 0 0Z *;!8juR+6yqyaڙ3^`zo 8I !DZ''' CU#;E>$D]!gYQ(rP-(!A;jY[%"R&ʁMh=xgs !4g+(~v~7W_ϧ2i IDAT[c$# F j8 "<%I&xb:U9?qwb2֎Y@R0;@ X+/BkH{eZ)k 8/ csZiWEQqC]WAP޻,f"xZ-X)gU]< "i&D0g!?{['?r w9eͶ@pU%M4u<.O]LIJx;nol:KGWN?>tvu:1lW9z7z Jsz467Df4s^x̙ӧO_^.af$7eY6.Z0 0;~XV\eVD:NEEQy 1Sj}#In{G}tuuiw]le̍Ad;ѐTa~Oܻqw2gLwO/Cew??OG:YM7pi׾_?-V6!9e؋8H~o#(6P3Vxg& X+^E3pZW%s # "i"i a(F[3J7TI s+=*4'KUVh Eo," "FWy/@8"PmvX+a1aubQ-wZYWJyNJ._CO?cǏsf^0]qoj?w;<<g<ڭ-"Y卛{[7o޺ukyy%k?sׯ_w?^Yʲ|OjʳHumV$T*"hNhY1f,˦iWRHYUnvֿssst~axի~յc//G?gy9w{tZD4LQEQK/a鴛TcLYY5ǵ*ʲК?8N.^#9s,//WUU:/ `W_?'Nߺu~yy^YYkk+^?裭V+0 Bq{ ];~wJҲ+{'o~;-Ӎj`zkoWJI>~>zqC=.enkfտKƤe>dȔ("zeEQ$c3f =-MPnkc:tZ?g%uzr{fz& H@4I ,ˏ-K4-e=?{4eɖEɒHJ 09Opo|+ D[_UsNծ(# ՍJ (DNa|  LPCƌSRpJF!c) (#\ ˗a\p 2tC3eT JʦݶGD !FI@CĖ S"  aSƚt A fr:BЇJR)D"Tíg'gdFʔ&On.*u?B)LaQKtP\{y (@aĕRB4c TQ!RiŒ31 iR׀eԂ/qd)  # `L!S&2Q4-^5o׿+2 10Mk:(դ#B>jgu]AM2"Bpkc,3(QaZ?TT[0, :;z ,E8gf2SBup%lK2GH׈bNdRswH|SH>PGNkA! '1[5,ĕ1gonjǝѭkDmy>.7O ݌Ls] T4~m!Ţfr;3z* 圷.@P:Hsp}cB>џJ/|~׷^y<%$Ib]s @0'I!Ba5x$ gLuG CK6t$@@ RH({!8Z'2FTt2cGˆ`q[}}}vDH)0Z]] V r{afaz**JA0z]7 뚮תZj]t$W0Tjaa5>2ROJ#Gh߽`1qAԋv~[f8ܿ+ViIc~o~w[!2$#=v{)e*6d$WOW1{=7)k6v{_:V ]M0V"." )JL@*%Dj?OR1viY)Ƙ`rA(os 8* BJ1B!X3<\.yP APR$IJ%ϋz-@(t;ZJ uz{ VqF,.,MlV+!)vҩ4!HU^[r8|ȴ-aSa=ʋ{ICTC;{qq  D2K)LK\8$*cR uiڽ}W|?ܨVr2 (*kkJAJW\q뮻.vKJ4(16??|e_~Y_)cM0F^z驧bcMLl- /,,(| ˶ u+Io߾0 f^YY,kzz󼵵/~mÿbs.X>r'n}sU_NX8Y߱!*+\I BATH%\TGTSPPX$*Ҏ "Wdt47Z+MHF-lt\5. SJIhIWm?1 y)miV (+;E4c[i)bet@آz(B9P &|3DA .0,C<6]AC h1%%B90JXIOnX&Rn&ARibpIfp Ʀa';aCB##&J1* &l1K¦5By =>C(R$oxn ̈́0)ABZNJ;sҘN䫢DmP}od*Y(p]M:}1l4U:7Ɔ5dE\RE n 2@EՌ] ޛQBsβSOBW__kcFI %a0!JJ.NFoqeO?!P3@nFQN}OgݐH(5M#+\p $J IfA(%tj0 BqsΓQdOTޜJczFl48fTkT` ==8}SRn߶M)%H9Nx+!?w%' _xtt|j {qCӥ]5^g2 \ l)]N_M-[܆JI/0:~8"(N޽=ܹsϟ?s̱cJҦMJӨ}Ι~q({^[ZZ*{rQNPA#+ZV.+]Uq7yRjΝݦUM|MJꊺ A42t:)zZfiiyTBj]7wAPb.J01^rg|a?ǁFqYN,P'P8\1 <汢k :QXILI.E,# 4ZR*JفCDLxtO:ڍ8HQl4 N/xE}pɑ!lÜm/i#S^:{JƆƚH c|@bWbIBG! bH #m"[+*) C`$6MF,.H(1ABi\Yo#Hty0 ]7 $@"D/8P-7 <F]ԫDEH$PtTa$"jiS+$d-W(* Hz$L"LC ߕЭԑB~;  5+uBHؕABa9]߈l2e8 !,%팑-T.5KO0r'CL1*ЕRp!A¥R=T@*2.Cn9&8pscLh'+[ێE2: P ӃV,X̩Vܹ}jҞBm.6}$E9=ttZ44i 8nguv+%M}x/RBGZ>y5cC)kHE2 J)F.O-046􈖦q[)Յ>ݰb&2c—u#ϦNc xJl53OΞ9:nSOjꪫ~||WBJ.UDBJs)eI@!DBpf(8&W*\!j4|N)8>2_O_{m||+ ~b 4BE$|衤ߦiA$56<'q͝9ung2m^zi\.Ν{v}վ0u[n{gǏ;Nq/}K{waa4M3<8rcc)'ʤ͖i(Jl, at0j5N~VBt-:tS,˚VQ.`K~Mӻ(BMtM{7?*Սthg{k ;6yO_9?ӓ}/r PH`7޳n#O:yѠ!h00MױkQ"7P RAQBhP1F-fSin05L%wdRRu}olihjrb|`,H^nT(kef\Yyp[ `5ٔz[jMbEC_a8 "Pn(ÐN; W|+Ϯ|jÅPA%(LD[.R\N' )p=ji Hrq,,8 +#]R("Q)!*pR^3暬J5 Re"76(@@!)dJ) M8٣Lm=\]ƝHH)$(H@N!!J,RT TTGvF edvl;1iv+BH@Q%iY IDAT"жY0r(RN)-KuP 2ێɈDP*(cPq0LI *4T&nBy^\!ʹ/X&\#ϯhEioBq] ΰ9$8-Ű5@S1~|b޽ V}o//놮'~7, kan*ӟ<_|}wu׾uί|~u<5e/oi~~~-@bko<Ο?mZiz0@bz=e)t]s]4LE͎ 0-,,NOods۶ù }7rvna0ӔT@L*Ig!}ϞcmC ?cc$FRoc++>\'IQPɓii##SUJTKVJvVJH1RPYEQ$? S ]z5\CN{sgz~ڲ+b޽{)>zn-,,  p={&''~N3<<8Σzk_o \fteY|>;00nw8궷fY!!1ed2L&A7TÙ`6rxk6?k͗>_~>iJ4A=t4|Qj1Уe9GI[$Fb6PZmחr)J3Htر}rb7Xr|N[͆盌9;C&N8QJHgR7onl;D( D&ea! 0m}XɥՕ*o0权Fm?qa(Dp?~;ӟ>{D 1B(q)bi+`Cy@WQ+H \\7Nb4lˬRc})L8HRڴ-(I(g;j )n{Bj9jR0@D) %`#CDL .zyp4 #$Dhx4et+`>CEj,yGB0P P-۰3n#;[_]5*% a0bca p"c/]A ~PnyJ Dq*d2Y9$lf/wv}[x~¯")QT0,p+['Uo/ odئQ`  AeKHt-R&fTHE2:RXQ\z^់Zacڵ^Y}ƦӼ=;&9?*xn(y8"xHk_9 Ծ"ɇp\vU}P H?v)/M5,Vwl Zݵ"OA{^~8ٳgs}ɮ]fyn+KٹMM~nnLNg=w{~n&2sn=pW޽{K Qg2_fjZcO0Bſ_~嗾u˖< Ո* C&8Jj/]?HWJ+n,.TvlQ[Vjjٳg}?R2JIy oOm ıl_qDuMc\ZAl+r@uUЁ40B@*@(db6(\M@*&@E# b 2Rj}DiI>vız瞳762Q)篽!(xn6tpҹMW&@0Ϝ>ͤu$Rf{iu-I;5 7=p`zɇ‰9B1BBv3#eނRHLD1m/Pb8Ia qJ)zKtzuuB~ȑ~0R)3^{WVVfff^yWsK@SZGݱw/ bi۶i;MEeFP (R"-RG}P8B"D qaBo˕E%% h3"N(cf\81DɰޕzyP"%:Nا:Lg8n[NPI嵉RTq4PHQ>SvU]@o%u ؎&,[!J)ˆYziHWIc CӤz H0&vZPLHHIB .yb!|GĮ~Di0y_*|S(J&6 ^) !·s5Pm>(nߥ{E!@2 FF{G!kbug~)WjoD1I!o̗<ۯ/䈅fRo/ޗ_ֽ> D8y|5h^#k3+y'1^:bZAoIe6Sx2{kv;u52)A2s^yWxlg^jUN'MuMlΪm;Fn7޻O>sڭBvf0{ jLo X_Y|cbC=8$I٬aEu̱w]0?ϖ-,LmnwtHT*G>*u!Sw';;;cw\^ku:D f'RƾSnh5l2W3A@(q]7 !1^oGW8^84D0") N֛M14ڝ慐'Esu]/{ @1@I.()@J#@!PiC^e8)(u!z{ BBHW4lݺV c$c̘ɤt`czС_|1IT*魌AQx؉ե|_'g/+===/b^/S) °+4338H\. vct벶s[bKvΞG汣ުW]W?8N{)IWQmݫ?_ Cny:%wN$U1U '3F œȗ}D:l wSiv؎ 7^~^y-T(MEa<$%Lzw`\\iv&#qp.PѬΗ玼z֝tϝ; A +Jn<՛I3n8nX^' "v'J)J'{zZNp APՙcK,bsY!M=cv]_is'[0lKEa $hR P^0~h( J@J,lsOu&a HB@)eY'L))݂`tHb_@:{{ VB ۹'Q:o:iCq*e0=j ni U=nmE <ˈHA(0B 3AEAt24J[ D %$@HST!KBX$ ޓ3PD!'{ŝN;%:*X&d1yAvMpUL>UmkLD[ m Еt ʡxλnq(xrhP\P78C*їϭv[nu|ѪmPݔv?C^V8[ _QĎ[)贞m|8;5Cw6(<^_qj`W| wM*0C}QGWv} fCNޥ`sLh;ݚ<~'`tvZ>$ԑ*| V9V{6BAFm%6MbLsgN{ML}kk^~EgFi_믿VY_߹c/e<<~༿T*=ܳ^[[{gf̌iozQyC >q2ד糛6MT*{;WkՍM/|g>E{_j}zNj/8yiS[&GGG;wN?^+;u򘦱88$8iǶL!iىRKvlw;lq=wֈvQfҔ(z$&vs{,xSMBȮ h!xRI VB((\Mc]nA(gI)"$$.[0;u.}ӦMi0 z{GQc̜?/gAp7a:BzlPڨgSBO8y#+Ry8FK) J%% ! \?iTcRI)mlom0YQD3)JkE!z+DH,vܩ #$Wk@}Өy!;>m?S`#SHIJVRfވ`T{ @!cB+.2a&58 cwזa_h-OZ;R@LRa(Mˌ9ODbQv+dz Aur P)02`/9@ARf29lE%ͽR/8J x?KM!oPX)!ymeRk/VA!ع=ѰX EQs0!]H{gò+qneU;4W6.Eِj,ʖj=ɊET&jmVN<o?W\3$J`z"S\ds0BV]70B|7ݴ}۶L&8}rǥRGCGZ;ue~;4M晧k8NEzϽ`i>r߽zoH(hO>?>us=5r.Zkg뵚Ψ) 0'NM?Mc33;cZb_ߙg=ϣT57nX\_ ^T,Sv\CIa8"4"Ql^ QK`L !B!RP)5M1ޢT a0 K)]u=NiMӺL널\n|.]r ;lmx'^?L}ǟxE};#C7xS&<\٬mTN{j_~y&i6[D:$NF.Z%ih&yQ /YwqaWn/nyhB1+N-Ҝ$ B'ھ?~{,Ŕ|ƒa3w{nHD YwG;bM?76:M nXY)z,*w lR/wl_s5:#%qhzlpu}8Eo)X>k"dKίtwtMn6 Dj͝0T,P&mCK[=φNP)3,4|,mXfhHǢ7_G"XZsaf' K>vŵ=awA/8ʀPYhQֳ 1$B)XGjY!]CP*ՀұQ[{L30 ;NRa+h;f#M"!*_w"=!T!$X*$Ao^:D@P<8`4LU(C W)(Ũ_K[w2m$@ 4MH՚['V`0r6Ҵz%!Pxcr*#f9B2N-YCĢ$ jNT HԤdSQ"R [P$&&\YuiXCٲmfM̞H"=$8184I(U@Iv 5+{_y L' ѳ۠~Uxq{Dۑ# R)Fˊ}PjUl):z9bjD}!YVv~:zFdգ9A ExZWlW'(qK[Vj@뱪` d4eͩ7N)Y=֢n=!bرlOnoKPh x*lpq89Co #Na/-M,X:mVQJ2ǺK+Pw(X^hn@ +c_J!wڵ^ykֲ압7 %a)FEM&I۝RRݬ_Ϝ?qttVo ڹs.?ijܱ36 u._]Y :S3>K/яmo{c?u9Ro428N/&ɟ韼޵sgGԖI/Ac M+J|6&,v00v: 8a@Yf3~n2,PJ|+eMa%xen!L$ C)e[]]u]ױ] q!$'O@ ;KQ=wPJӵL&#B+גo?woieEQ P(Billl( Cd2U|1F!d2Ϟ=s;wT7 R 1pnnAPքﭯGQpE% Ԋۆ?O=͇ſ.!u~1z8!_w\}6<_e`W] ߈wI?[bߕF۷_λ&CQ$4 i-/gYK'os]wi~إ^H' or?u5E sAIBsI ѹZS ~O | "iT(KXse$[F;.jA Ab3_<̖ٗ/52Mk"&A!dL4JR #QX&RMSFTdRd6R%iv"ln )@߭-fȧJJ,Tc$ FAk@NLڎq&2DFQ 00w\D(x7AȈ E+vzT̃$[tR<S8N2YV5pPRi*I>L:QFтp8bXI*pddӽcDq@irȖ7nEg^W"$9jKfĿ;?AL՗>c qAПF1551+)#`b!vRJ C{.)'a!Bز,0Z1i )i>oRj݋b1O0RQQ)(###J))e[Gn``h\xT*˕gyznnz 8⶿jkq5hZX*W,[Ģ`Y玐`HPhBR7vצ_xU_ܞѩpm7n=xك` x2ͣN'6hV#uRA1CHlFL6cgWB!at PB@@4<ݏX-y㽹:o'?#*R@;bpnw!#hFVFmwa;ݳtX5B2o׃AYNPYfC,u_bR4Y\i1KM"3b2.ufD2H(I!$`LP45LfM7'\ )T׋œa&KeDnjV*Q=gDRG UK3",a.Nu:aCCd@3GvïsI BpSI9Ɠ!B/QR 9y\Z]b7؀JBk.)"ꄚzirBi[JQH*:"U)I($NNJ!%!77y"z/Nwd-v!@gdQѯ7߳m+@6 yd:6ʄpq 0+y756w]?|fKiv]"B+zwb !J_V#JT“_>)#n*) %Pi{Aħ:]+ܐ Tn.Chcu:M{Rx=1:)9V]Cf='hيq%5jAM$ʼn^e p~:+BȣAu^ˆڨ7nw?,IYrĉLєE[SW+v(f#;v-Aϳ #IߏYvn @-'I)9Ng-0BR>R @W _MBB@bW})Dn-&BXr!0!!B)bf2TXJ)BH'a)')aaYTJImc9quMĄ0h>q;v~ 80>3&"Tn|Sυ*JYkLO%յfF>x7DQoz [ѾQ!Q^xO A^kzjtɓD@a6ZkU\ٷCT~ CӒ~_=?_,=qλnvیVWw~HD\J}^L>#!.+gfeA/7nY `ˠf;e^송}+LNefJ~4@~[_ߴym2iZQfĥuT!;>bOAH-s҉ԑw̕4/nzR7„ώT ~ J%<^\]:$B۰"Nx*l~zwgwlΒM4_-ܖ=hnʄkP>[j2aLFC&gKFH[մk@Ksx)Wh !+3|#G[8K(6/-̿wO8~ӞwNqK'n&Nm߾'B__}remmdmm}Ϟ^z`yxZ0y<^'5졔J^`ꦔT˗MR~i!8s9_XX%l{B?##e$fÇ^dakz^T |!䥅AFpfvZ.RjQiűiCc)znPQ~Qe OMN+) % " T@*`0DI)!} B1iLc)5K!0>>Pڇ( u]$qBN?&^ !Ip!B1YB8 !ZÎahf&l۱M nё  J)"KzXJ9;}lth6.9#޹{wu}V"R&&&ZKN\u.XKKy_*xKTV'ܾ&g/ O=\pY%\>c0_ti{Tx Uq2m#f8(?K>tOũyitޙ(j{<6/qk{>%;k4 S ZҬE3۴ @a)@ hl[ N/-nB$a p@BFÂUkH~͟:k8`bxA#֫XoPS{ҶSrRXrWo+k+WdNGCs.쨥 v1],s9t /|݉xEX%P@ !ҙmVC@Cm#Ln3hlm _0JT2 Q(kn]SQc jBJ mjnu֗s5tzJ'D)踺T )&@B!d,2ھ+utc@3v iӅ8Lr\TP(xjhI Hp4lqB (-ӊ# %aPEJS@D*A z($zׄ1S!vE}+uӣ[,;Xg|5m2cal**#@2/^$Zv[bn}rJ %Jp@~;$"}DK_eM;M: (m 0o?Ir/h|PAʜ=\][&7$Awu 6)юz!W@t؜ߑ*Ŀa  .6ݻvN J%eg??½#on76-l csg#dv++AOOO'Ii&Dptx* -!f;ї^Ɛx /6 ՛ɱ%y>f2ÃچچafgO8/%zi&3<>1N1ըj^5Q&S ШF[l5LGа u,tz,MSrW+e,ec @Ź|饗>ώOLOOW*|>$ K>bvv;϶g,}?IJ(cA RjN5MXAP}8RRJ5Mr`aJ4eiJ5M?5‰O~?Os=/~_k۴ޑ^+ }8(}O7 # cuO;jW׃ Ovy+޷/ߺtR_^yֿ7CWJЁɺ7tov+"LvF#hѥڍ0cwK]ޥ8Mu7K)\$-K=G=2,a!CdXqrbBIyzs*fj{+cRYDB)8*Tȴ48 wm0‹='/[q5.9Gϰm8i aܵrHg66hp+%#35 [-' > G8{Sor"\DcTUq1aQP fjc$Vf8@aŤ\U\9׉xhx׈ !ôhc9| Mss0QXimkiB!q{~U0<ɧBJFf[,7/n/cY7/R0LSzSӲP#,]{M $!w,"aR 1 1BVU9H`|#Yл&:t^Nδ:hLK*tz̓;f˻6qϛZ@ MB 1HT?1R }[TN;7;x #t9rܾm豣=wg޳مŅFy^$ϝjb)R\,KT ҒmB(IiJuu767s,KӧN[}_q˗3330|@˲B?$c1@|ٹ-v>z؅ vލ2K՚eaI菦ѿ/N>_9s,c4͡B1g;03nP(^^>62228001>zz#M194MJaAp)BBOyqUh/~g?5t&yǏ޵~S= O؏ UF $Ӭ&{ǻlzua Tȋ C˺J'i&N+@ a0u+a Νn ^; vWT7 RB$ XʑvK5,3s)@do?Y t5Gl.[Pf3NDG .lU\]vߙWt 0(pEUTAH 9|p2`lL;ae@*af4{5pڒǙyh8Ca.crw RkNvÖ!<еP9yơQp.N@AЁZ{&u7Uf5"`ض [DY&R ]'R&SbB 0OLt00@iSc<$4MPCTAէ#)KF (f>47c#y xP( RD gveqޛƹO$K$>wY'|CͿf19r"^ oM`d@S*j HD$fL^ A@H ά??uo}b?qV"\bP1/sΔہA ,5\ o%K~*gOHHoKd3%{,ǐc`N.ͺ,'GaCxгZN.3i]۹qӝ%&p ,c"z}7Htrm?#g k+++v$4W,żQMo?W_ssssl6IuWV. mf_xաs---U*Vdg,xg-`q42unӨ /f: *hN1nݮ"j|15.d<=95]:sL6;!v:/˔/L0 7R:בzK_][~vʹwݗ`}m\.Ꚗd(.Jlcﺄ @ gոy٘-JV+޲9}:՛AʸhtUKI:дa$׻ذL1 8C][°GF¯ B+̬cg4,-?`;'X^^'0=I*@4;Q҅&:z"0 HXJwܡF൸$OcE$ QȀ&4'@M+t_||DEĤkjHJ\*Y]%RقPM,-[c,Ib 24MGi%%&X))Ʊ~;pG`_6g7tw67Qhi[7] 4@L.]3*cn/K[Lqg.F>wzK W~=zOR [v 3 0ra\wWI{|Eb)SN_+o~sXUR)$!Ra'JHwkO^È05}~Oa=s'<~1 ~<=cO̷ľ%Pl6sߑۚ(du4үˏ+_><8@^)OBNoxO<x]|X2^Fq.\x0M5q…B׾&DA-$^wug\^}U|͍Knsgϝ>{@zyyM4ggfZm;Z-W>w|$CϟosUG\F|>r-|]w VN?G8Z*ADQdifFus~m߱ˋT4MD@8 ᫻;7 ssfVWք[&F+S* %!\,I)];37m}uO 4lkjrvlR@ @z> IǏm:^Q/ɓ';F)t݌ic/^hV$O۶mvqŵmqSƲaqQB{?!J)1B!AH)>[(dEk[nݾ} Xr…;l4<,R 8*_Qlk"&_}+e}k_^\Ϡ kh꽶77;<bsۛWm܅=åuxirܸO~_w|VO'>8?h  ᑩ۸pfKTV x#$B "ud0Q,h%ElLZCduMFЋS[%+3Ȳ\&B,T` f͌V{/=nVWHiI8T(QEcPyI셽Ne>1=, ~$~WH``՚')v!o?J `HJ)1тmfKqj464ѱ|?Xvf'L7(@&BC)S,e)a3=x`M#p%Q/YRbs]UJF(u2Ԟ:1fhTuJXȖRfc !$1BTp.8"X4) Re$i 5(FJ$L9Aw{2O6H8 &Bn=2`Sn =kP#Vڍng_wN}=kk+׽[{U e!AP͔|g`~Rɸ7rsR'@Z}/.\ H8ўӸtu|XX ǻ$iK]~wGg>t?3F)i@N/_n'jݎkCl6G5ɸj;x=Gv; B۱}QCoSNL0Ry @{G08֔ΝØ()O8kP |Y.;!(UJ[m@( eAv iEa0|3dqaqM[tR9}TSSSk뎌 lRJf7!`Z|ħOy񅃶e\lݱcw.5LAiEnwuuBڬVT!%a04$\02tS;|tvn4jぁHZK4L6~I:mRԔa) P˫|>hbB)Kcg}+T/{oeU cdDƐ9gJJejD)0 m`\cWۮvUW]U쵠kU-6eA&49#3#cxg!apGer|gsgo` A0!A.X$ 6!laa{<{o˙)]'gzco l4ڭmWSB^ae RKbAƉW˽_·}3d 2K0Z47v컫g޻\z/>}Ŀ{8ޟ?͏_y˞wD_?ˋG&'g/C`+@- "BQゼ\N&' ;xӰـ',ۑ6䎛BA KRmjXpiXSXQK hm) ȰV{V[)IZDAqgIf@SVXB4^nJ+m͹X>wfwKBXkuz5lHéA.I75tPhZs+$ZiI4ɬq(*XǕBJ%){16r?:!LR&RFK۵0ġԍ0i5W4[1143Ҷ\)cք`0ea`Bc\qnn0ӰJeSk9g2 c@ I 4B+@&1BZlw}gF;>{7B ޕM@./߸V6^Oa ~xb ʸe i 2dRlM- 4d޽oV2M+8|I}5s-4rw{}%V"q3ɀٹ/&U,`a?ߘ6#v%v0xU/ ߓaH؈^wZqRΕvyɊr;@Qx_u pn3Pk@~r>J~̙R9B{'N.eAťcGT_/;vݙ;wK~_i9V,|ᄇsV9w4hnV sJ)yaAFm=zyiV M5Rj۶@J)Gmx !DZ))mRn2}uCq<7w㻫+˷rK.\BΜ93:::=߯շ=8d8t&۲ٳcL0䬝nc|mRT(-rmBNk}e4M=NpffIZSj۵C0s6R*Ν=czǶzF'^zs #8֪s-z\rx <{l=Էm mx8iN!l[61_\xg UH $ 4RKzw"+=sτw|K'碌 ^_ Wypςay Z1Od3d~jA)VD&i IDATl92Hi-$QJ„T'X@9W,)0,V`T `4hf3{v&P| TVU iKK/ij-۝Hk_>RA@Q%@J1@*!Sֈ!$A, $ <Ba,@/W{[QܵM))GAR,0ppxf4XCD_E2C ydSD 8ZJ)PI1<&jrΤR 8@!Li8bH ()RJ-!H*%2|Eg;Z)bou#4](JDζ'MRRySZp4ٞdA)AxwGf~"3Ti!0 C0VL|CN&ɗWVQ[Wƿ Y {IWrC"*Q5l,v BOl^ȶoqPC`em_H]SG޸!؄ c۝>Ʀ5d ڛ 4-n|$2?҉B֛={v}yIpӧHxɧZuK'N#MBFs}jr빜]3Nxܵ+.]Zucj6a8ܵkwYRq]ollERryyy\X\SN=cnc||ow_HgiKI(6m+ohAvLF0$ԩJ/_]]t:}Oh~ qj\'rn'*C3Samo-`fZQq [PkumC3">>N:tCIΟ6Q"Ûnup췞5w#w.[~ׁgsKKiD$e*cXhmyq.Y@JݹoŃHN PJay20JZz'?5H0r4LDm^C4O/ a[HY,:GP*V8ϴjQfϷ{bR &L!KjW1R_>2s‰\S ňa2%geYD 5Ԡ6"2@Iƚ 96 (e4 W):Mtav7@T2%RZp XAAu'.?ER21~tzަ)!R$T+7S=9m6! B慽j;TjXqM.ZVI9xS5iRq_>nztbBc7n^_o$NVVݻgJfFG덆Ivnw82 #`P#; $p Lqbh۽ra+5-X_Yl4ܵk7>lug r/Ϟ=i~/^x)SOI%.΅x:ag2^ld\bXW_<~Jv^c!d[U- P8Jqs?<Ea+!{fH[q?Q\biCwO?{|rC>/ tǎ!rF"fF+ ^] ?H[^R_|fڕ[n5ۃ[l!a#)"c K?LDplICl@kbYNQJ)10` @H1(K ʵ_Ii'ݼ*6=v(Q)R5@*1N0jR);t9HN\Ar(^F(=Mԓ\P"v:2v~R`m{!З2 p cY*~Mc9z j0%5$8_*^{ΤSo>[֟9c 1>#xh1dW>H'UOOWs+W?y7.5`a廠@ 5uRC'|#hq68_VaL: Y/O.#0N" ֢FmƑ:cR @5c)*L GQęJ!DiMxT )+Pfdی]ޖm"ђi 3N'2]A}zז 9зqbQ O8{a(F#6yDb/xK}(F8I"G.iX0g cw8B ﹙^]*CIǣ@RH*ƤWL3B}(2 HB &<<+o7ˣn@oBp&ܔ˗ez嫆iێw\G;zXXX[_mn47sss_&ͦ}׻Btw0w}wE~BX(d, 0r4R(~u\֞c5,MxcǏdƥ+~ FR[@V5LiוR.//#I h8B5;}foajlg9ӱ$xE Qa1&(dΠRkKJFA 5I@1F+Ϻ!J[*Bao~#b`$ @0cÄ)[necaLl^+ i,/aZV=z춅[o="x饗(O>tĉ?R\.~8p8B;}viyi~~bmw !Rbzsan ^V auZkG=~ioŌB9춌}^Q/J)>[?wA3X}; sYٺrjk~xRsaȒV8I:.c 4eI# J9Ms=kjF1\GO9|ѣ/\{i4n'Jb !Z3gΜ>}<2u۝l&{dPH uܑfo4 sttf_^[30F絻8A04 \+Ҕ),^ֶ`%|ߟٻwcK+&$lwkDZiR JJ1WRJ)%[*As1J:eoضQ)* v/_tVU{qys>2Zo7[aC IDAT? !BH!4M5˗g{]]g ;N$4MB/+s%SCR6Mo.Q[c :I^6R2 E^kB^/lm`LL=)bӳaF$aOhvr T&'m>,y/="B`%LI&C(c (7nFxg,Ê$`0h( 8H(R~OK;`]( c͗m5]1Q(V3PT &=sγon(֦xx1"T<>\V+z ù!p_][ Gvʊ];F!4́+2)YCtj84|Ncx1o>Q]ٺ܁0SC3)pL㬩n~Qa*q i|0fiI%^gFH 1`W(K.#[ϙt1(]Wd-msw6okI"N;u?Rc\t͇t7qH ۟q|G}+K=HZ!ѱ֔Rju}}ziollPJ Ӝ_\*+*" TFZKK1.t;<re}c#_޽g=A8 T*u;ɩ{lrK)۶˷;/LO akkB>#|]+ַR.Fqض^W,Q&5 3BӔ1?`!#< KVmo啥~ZKi R!cDk`3RZJbi1Ɣkv0!oQ:#MLLԸpϾ=.^ZXO>'@4BI7 K{4cdv}ᯕJ%8z/"~9O8 Iض}7ds5-yk R*I5huqU)13bu1+ .X^JYc$0A hl8y[eJD⅗!Dr[HgLs"nawO.6_uk\S1$zF;}ɶ:=+9l(mښm.9meo|7=OOoIj}D W3, !^Za}w?3G ϭ *v% ےsiHSAXQ(vqRco01B 9+*j 1Ġ0ARXkYAnaX,9 WY^K,dDPIiL)*+0 j!8ҦCF 0,(UjXiJ!ijpҥN$FBӃQT$< BLQa5bN^k]vcNEiNx4Ϟk(& ^ TJhI3;Cqlm8^\}\PUayNV@=gajd4!|(5RBk!'_E"Wnd!kθ~dz$n̨7\hiv6޳7OT' BB5$ A9 *~iv,ߙwge*eĖ4yj0Sjt g͇W[51g'wԭi lqY *Fofuu7,ow vT*5$[oqkGiRƂAbI{53b}<;G:)`X˚pw7e(1zbx6kkf)趵Ø];r-I=̷.u`CqΧ/]Էmns׮Fsfǎ`0666>>h[ 8I8޽T.50L*cccO=$Fw0t;ۇ0|>o߾͖j$TAv8 v ^ZZZYYw` /`[?n|X,Z-Iǟx1f٬o|>o;aZ;v?{L/i!(h \%*-PJ( BH+ >u-GOLR 21wV+')RJ%4)Z)MJز-RJ@)Pi`iqh-"^繹lvK  B=J5зz{ǎ922reIv7|Un7B/|ԩSbuGjW2׫$ھW^jh @- !A/tQ IbZ<'o.`NsJgZ*RFHwzD2ZI_^u:L)d4~aa|Jm/^/[S 6 YSnI אPԐEӗ@$M8= ZPlZQ.Jt54?ck⥵( VXB8ŐCE58b UH !8MP3 BI8ZvA&)JZJMe ad Jz~?TMLi" RI D)dLj4;ecdpl[(%Йg0 iL!YKpx-kPpnsK7Ii$E0w>zt\ EηgB-O##tn&FCa)Y0!f& ͕~MG!i-D03Ϙ[^{>-SV;HiщS2Z"ZJh6hڊѧ+VwXݶB TA$,Y)@;Xq-y|h6116!xc e !8fb,Yj9?W;uXD'DY]FmjaTѱWc3T &갻4 0n4z,dІ}>a3?3MmՏ %y_y{qd+Ϫ9xRwxJ !ks|w3~sG?hSZ[[ժ{={RP_ۻ{zeskk+3;w+-`p\.<19EΥ>7ߗSzTJdI6$AզmE7Ct" ^i 6 XlK¶m$9H*U/w߻/[ K5oݻY߻'oߞn9R(Wsε:\'ӧ3zn͆yg+|ߟ\]]-JY%Qz~|gnof )lW7u yCZ|>n?ir8,kr8;;>:Ξٓυ{WKRZkqկGAs9cP[CH(p9991=3ZBLNLI%I|ǎNLLAe@}(mq\7嚍&X`VVR^\^^ڸGy$nF?&Ǟ=zk_;{w0HJH~X8TqB^F)Tael|wH4` iPx qx/f2E႒2"^wnVಬݗ.9{} sUTͩ~aV'NHrW^g_.{OuŽmA0ֻӧz}޽{c}wǹ187ol}qWGh%kNb(v玹1gؾc}pNq'sb+8FC$DH- JNĩ!`XГ.߲m^u[;>/H0LO+pާ6eHvP IwΌ N )yp|v|pf0Í Mph\$S4N[uڞcI5-1,+Ɇu{҂a2b$a!iT%O`,Vo >!e5T61*O l:8bAH&4_4ST+vt BSktjIba"3ŠMQ dCf38za_)nCtdyXmSR э+7 򍨛(8Nx4gh2qr'Wj4[??JQ-^@.Gil\mmh]kY"G]εgνvblt_32q}j9Gr266u֕U??ǿ8QuJqԧ>iʺNṖښW'I8;\s`̪vnv,\8p׾~KdDI-i|AHh48{:|… NGk]VΟ?Z.FG3jfkumӵZMJD+8c811Eo=|-Q/HDk=H=Wg +ա}wGclMOMcǍ֌K8^!nT]r,HZg 9@qZATyGZ83D1l A9h0t]7|"ic5BZCn7IAz@]{1| FFGQY/ )B)EZup)1Z80:6z}NMo9}#GWVaǎgΜڌO;vlxODJi@2 cZHd&_@޽LlI{A~v8l9>bd3iSI7hoPk7mgdj%UoTt2uXWҊlzzO͎ܩr-/Zm\׿C_ו_ ;f8m[t8.|OUW'~xwv50t%4rY2c"NH@ѓnUv]{pad4on&J"A^Me0D(ZsU&WNpOJ"d9$i9w<7˴Q!ET#M<۲ꎒΜVmʋ@Q)J(%!t";V"m\q/"Q1T̢ r4W ,5rYq# AM햯Ygψ?3Hr gj³(u 9dPu, J:'!2"5箶Y c "Br jUk~:oԽO_Lp}q'x"o8BFn;c3kpۊ9B[]\m3BPWJ$(rM-3eJv ,#@g߹8X):~Hlѵw±HW6<`"Y K,s?5Uj+HcHPz\ʭOYxR5aAN,ƽeg^ZoC[^l hU^uR!k}{V\v;v'Cw\O)c7?pPܱsgјZZ\8t;_wGdYq^/G6}驩;vtڝZTR66kG?s#f3bmtBtgΜk4DnF!hju$MszyM+z۷7677kSJik鉧~JҸY߽{gYٌ3Fn2j4&'Qq'jjjj˖-rYePngqAm_sƹ`dd\52q$`bbb~~^3ƤRRJ)k333vDdAdP{$8Z3PVk2R*K}@4Z Da@}ng?8tc13&''Jq! 0m.s SZ ߳_Dυ{ L KcrC\=zfˑo>"0SGlJ.pYo]i*袟MiG{'5WI+Qtv»tδ6&}c-TyAͰƦFNUItW.}[JZ\+9=s^O~877~ ~<)6u7֒=?+^E0d@ $AҒkiFR :-6bZYf9Q`A@P0et9HVFv+O-KV@~.!c\[(<׏2 ^DHЭTMEK)1 C)H aƱeS\|Aד,N7g\bXf"9ȴ+,c- m4yMuⅆ9V\c9'Qw\ cH 4(1Ny,WXԘLcs=OyPyGBGD^w6 =\6"! iIfNHEsH;_n7[`.]y9851=/(W.o*ExB$ei-&jZG3SU 4%d EUN(Icu}hںgqf&F̈X5_Bc:g%Scf;[>i5]ouL]K'02}´57܉~R+{?{-9˒L3gN8q LOOo5\Q߸uGm\gFk|gF!rjZ j9N_^_LNN6]:uR4Zݩ;v>{J)rA IDAT VP(1n LZ X $+˓VkY̕i}}|sj_\X, Zmz?A Y+$O>O{r916D͍ZsNuۍ(2km& P(!ree* " YNF 2Xq0&qtLr'=Kz#r`rɔΈX,EDC| 350!%r]%7̹3NJBlp_\\@D1ZFJA8JB3d Q d)bsfEG{{(#+8IRjj?|}*̮g #7ytرgU ڱxdV+Ƹe\]1gㄳ,U=/OVfO3>=Ctw 9g8c@TR`A[Sy?" HW@t',(\yaPuf} k6kٹR;:WW~ފZFsn@iCYNv軳3w*̜%Lt|Ɗ nYGRsNE\1;eo<<2ZT;o9OlR-oML4bRu-zwuƳmfsÈpˤ/|xIgg=n`i~siLNחƯFRڐ9"d/뽼Jz奥];w>|$COm|jrrV=|ʒ9!I3g I!1믿Z+vF0Dfvf*JRuIc?f1llljI$i{+j'1~o-ikZ:it?vx〧O1ZU*=ۻS?r#ã5P(Sz绾Ϸͽ{fff&&{X,In1;wj4O> _Gf/=8!$r/1$ShDLӬk3N- nnn=zww#]ӻ32_)1@"!"0&c t1 `l= KTzo|Z˧("ǐ!lj2P+kKgp-et #9r$V36Hb\v8$)G~|Fw]$faYh.!c(]vδr辳YeyZ&d]1 3c$,v9|42*2C.92B " iABs` .y8J~xXp%7ڽtJf5jdFWݵsW-qβs[_9t/j}$emnDBӵuwMnQvT?mz}_nX,NpaC׿udw^:8.`r D|l!ˀִ'cLGէl[}G{?ѽ/7=폔X951cMFӭ3~{qkFt"iY]YשکcӢhaawBv:dy-رKo|X.:nyHc4|׮] ΗJӧOY7p×wj8Q.}ൌ(4M)M6hs'Ջk뎎 )Nkf[ΌQZ2RrLsLcaHD͡~)5໮+ZkιVMNNYDf ϲLJxOdRN;|ct朧I"HDe`ε0}@q#Ҿ!ń!h^';wUxa'B\9 g8\,jQahWo>SL*`G.y &\x&k=I7~jnjG#k;S]pﹱR\^__c.i_B1a(:~/sͅ3fFlul`˄Q b ]XDZ ^QoNnO<\J{! "Yk `,s^}nh8#V{ibHd-@pǂG"e[wzb0R ÀFc1ƜL)Dɀ!7;ܑܞ'5%)588LeL+`VX@♅thkݭl JP0 6ʦ"DkHɐ!"q"=IAY )#H3dZk bDfDqc(#rUaɼ// Q|;yZ?t_$Aknj}7z:}͐ 4{|ceԓ`R`I`&%9"Ylч' ;nzk?he(S@`_]^[5BY[voc7QQo}oy?>/: ސA5Ii?ox%ǖO?Z"|atߟ yNj+ 2_ym?rH-3[8?hַzwFsa>w_ ިLYmFF]mnnn;]AQfTZ[[ `VUZ^T,IZ#RieVաgOvmI_sUN{iilki2pǏ_YY 7msKڪf= sW_}={>GޑÇ}7 !t^oyi)eO5W?|{;W>9z/Z8M* ƘqKFk522O/Ͽo߾}ݻ8rHyq_WJ}ϟ;ujq,KTDۭJF]Jf\if1V ɴVhp0PJ9cIT:[,S{Tū ZP. '>'ͽe'-.$iFCP$}Ћ~{U9KtE1 -Ϟob)ɤ,A.8F wHF"ș g nZuMn!i&ׂ6sЙB9c2M22,S:3vf;=Um?]"kԽZ_jm4ת[LC0<<䡇2JRۨ-,̟gm5D& Y$cF@ AD4 H)Hh^'i!2q"QiqQA0;mY8t+3ZZgNG)N-\sː BicaX-iJAs2TAˑel,vɋ /^t}3HrY0j+Dvh'zZ'Z-~f;^3N턹5g?W890%Yr ^!ƃݍBM{۶w'kAiF.k\-voǝc N?u^x᝔{*sVNHd -(b>,]OzjQ~(W_?fH-0-ų(+E+IB&ŦJݍ߶uc_YZ^K634ﭝ55;{"CD nf iܵkZ=sglrrh:/_L$McNxYkAHY,3i~GGZZm޺ꪫnLR+K#BJ2*:ԠX%),V28[^]߬A%r7ޥiB|jjX,\w5~nw:(,#2pM7!~h6saEB4ɴ2a}}a C`Oi"}xx8  ~TeFyس[vEDˑ'qڏ"LeXf` YJ[k 㨴1!q5C,?7 &Tqϫ/"G+1M D%1wRE f>sHʫ-_‡T2 /tR\ mp:h)a*XJ_q'c6Ezx+z6Zڑ;t"+jXn;a^4nr R3^j8!cuGeRUlҜ# owqrjnGO,3&cCvc'{޷K~߿;3G~?|(֤@ R`~oO>`o'~' < ,r42fv|M,! CȀ!0f- eX)W.c L[C`Ȁ&M05(4I ve}^?< [GS}`Rx9/42 ,Ye4hR*^21; KBLﮜ)`jÁh,YC֘ DZgD ـ|PҨx]ayf+s2ԖܛnÛdo';57Dn gNN6'C$a~(%2:ZiD,1₤,tVw箪K6|BTg[r?r ݕ;mdn:1G|_B^7:Xxמh\X_MOy̅مPyh-E`vof7"q9+?m;ugEy[T̙׽yv1, g05Zn׋qجWC_T(KGGGF?V-ux IDAT[–G-7߼Y q:1Rc667R.J766 kϟ )@P 8\ t~Pj^gZ[c s,Z"Z+qIr~sssYs@e*ǻw7 \]]{۷ `>|peã{sgncԀY2w\de+Yp]i8p`﮽Zm~is.D({}IJƉ180rZ>|zp=ϛ眗 ;vFQ,1Zz}syeiyiu||~iV%"t]H5 8V{O8c۶?rEq|T,yW*%@JgV,1ANMN5 Yxwz{͎V\A׼9]_}׹#O~oc1$H W@LY'"eJ%B:YY qajtEOMtAU5*.:h#Iqgڠ Jb&y`TQta*@WNȣަluR5HsI1b \Gq_2G* Y=1U2^D]N(^èXθE pɐtd&&qܤ:±Z``2@@/xiQ"m۾q uia̝ssӁ3k#ձ-SֱkgLWNWUzNp!>qҰzg߯-_yI׎oBl@Q10`Ha&5-dq 1GgO}~+ۇk-x RYa%Jc>bRniiu26vمko{[Vzܘu'7OUFdNfM,2};9)323rRJJͲ%Y !ac n0ݽEUյ.M*Sv7`6G3-R5 9gt?"e`IWA~?"uWw=w}՘7=3lx.Ճ/atǛjOA0/|0^y4q\6(cyƘ th9ućC?ַ,_$ɱG_x]wQͅAlnn2)cv[0$)tcsQoDxR(Uvm[;FB4c0mmfSiSF(Z\<(ph2@2‰$:Bּ˳s&qRym}OF}8L g}VKK^ȋ{ع#GG/(Z1ͯ> 3;vvmJ@^`P(]]]1Q8s9 FZ\C$I(,vu̙0 X^Zxaas~СOOO7z… Jz^.H5;7,.uUZx08i~;yo &nɯ|+{]aSZ[:Xq C$ϳܵa]Lj8._`0ð\_\ϳa'XyyBqA/=~)2-_U\?p oug[}RwO~]Ptɱ|+|'!lb2G+gN%<$ !-W/4x$s90M"8G2-jK% Hv)a(:>*4dlXfYV-Bc[hDVu:/e/&wkA<9*9.|Rm( :N Q4|9:R\Yڦg^X|!=PB`23Zm<."!!!dw}LPz,6wKg.;֌ml\WvJm6͍z}_xΝ22?feB{@&|Hz4`ae 2Y o?-gtG+fcё1ZT/U<J'`f%4~p;&'5GP\r`Ȍ$e g*"S+%-'ntD㣃÷eq>{Q=: ֋AM.N%.F=TPxSy/i 0`[X]q rWB0^wno[c읯yQ76#z[pX(%r y.-]`jrPӫ++##NbLy搠u(h`0Gy4.Ń@k\_0JeQ~;uιݢ{XθRZ}Z{M~}5y 䩧YәjQhmn馉p <#cr~GJJo| g{5?ַLLNcW[vϮois^cQ™,8[U8}/!0b.E^>Tneb|T./-] p||9,bi1x_~8뮯V*ԧfǿ?[RgKq߻VV䃄g611˾z1td9%ZFѮ]K)*i 9t2B<B!\a=sU(K % ֿWWqҊtf,l2YcFuk|y)(Ҙ ǦGkoL!uePp駠:OB޹wt+-rF'kzuKsюL6to?.=)Y^F@1%> *ch/堸EispAïSRogia@9CG@Su Pl*F8XE|!RGN)$8(+ "4 QqߺHpq 9ͅPː+ ax WSf0ܒ&p@faqNGpVYg=OcG.(p "B_al6Ю62fj*<6~鹣g K;쪥~I,R s~`zoD2lDH '+5B4uؖ%1 JzqDÃ󷌔n. YqW#o"Os duAI{qjw4)?,I;5XѸZ9.$4/'F'Ϝ9S1gٮ.8|QJ.?(<'iƙqTeAqk5*ٳyB(rj~W[^u,ɒ%jRtZ3/HROл0E#6_0yN' E/.g=Zі Gx6/{nVnIKwN`kLq%.DIl^4ki=ˑ!RjeTRtH,9@Ն۱};LvM~fUC[ =}#g-ioxVJMW[*)[3:W4o>GhYz_8\ፏ޹5Ngz⡏o/s/$/" 8_Wl pƢє:B\_ JdϞ=KKK7tAd"q ©ǘ."PJmey@8vXo|Ϝ9^o~c{_\<>gSy7׼v#@oy[#Gz[cR %"rcAm&م-"!AFklј:ydX<㣝^R!xV* "C"%W:ۗuA*W_p~=>I5Fw&`㖼.q7|aC隙>q4_e#G㜓J dy9>xbr rZ.@ 2&˅7_jvta2}} ̰/S!Usv"#1O𱑰.gryLR e:}P9JHH*DeF< dr@?Ş@&rYpHe3R٪OfFDQ?ysKO'ei'ln/ز&*p= 9%m6~n6VPtpR6K`E=B%ڌSJ*8 #21bz9.cb$1֡P%UFV/tX"wvޔԴ VzfpzҶq<`lcEU1[ 1FӋ??}?__zC{vQ!X^]9~|_ryĉ#㓓Z\ZZTccR7p凿w!ٳRjRb+0rJ!=/Jii,H\)9zIO:,!pηhZ2uYk)b~xjvX[;w `nn&Óg5ONNN6uB9zƘK..]Dw;F[[Y~\{ݵSSr|~p}ŋo.--m|?x~+ s9tdQ)oFQJmnwg}vii]zٳgR^#J|~sA?'Z<{a7jG9OOݱU˕'z3~Y FC}("^{nwzqV޽w}_k]h ǟBfYW 8rsnmm( !pe JaY^- 2R? -gno{1.Aztmm]ww:(zfá֦\k+ղ6N5iV׬/]޷tOSORJ[$Ͳl۶mk+%mΝ;ECpqJ%hyJ>9669ky{;v-Z555k8 !N=𞧟|&8VG5V Te?~ycL)Eэ7xJ0 $n8J:+=OHyΜBssJ6P BEa#>p`ߞ={.^裏*sq)S{Nt £4 J"=/YeiWbUܷ5D}]?218vM]8/]Z+!%"i浵_OY|·V&쮅G}tuujڭ,Ν;l6`]^@728z'^x 8{A /\Y];;Zs8d^} ڌ!bE֘4M i[ɩI^AEE1&/ g s-ʄ1s:۪1Z{X^Yٻ{駟Ѩ;p0KҍR^*sKKFFFw-8 NoA[dpxJB)m4K/k5WI_=ʛRcjѸuwת4?ġ8 %B)GYY;DOKqKcs4-B;Z^^ޢ$I?q9aFFGƣ( | RBc;6jҥKs=c'Ox@)pja/I_f w wйէrӏq_?C aukQ3Ca8) -1IhRV0Cʹǥjg)#2@$}|.d}Yc9L@32rNSIQOI8t-:^6 3o{ʓֿrCڗАe _@Dcq,a$V0&3SV+zY3VK1za^d=\0J'NEY Vi ] )D.e' YгqTz 3ip ˓:$W,e ̨G1$ 4< / bv 4c C$q L%(@CgH)2R23p<[t:{ERž2 E dEJwZl7)~uˇ|o7v8=]R%4ͤȘew*ZYYD*ݙ|}Sg>eX@%h8@TNNcRR9:EA)2!Pv,#KSB] ;WVژfsS+waw( (҃okZO},k)eKKKEQk _ʄy*"x~[=^.N0*xa Yb 5/8kJEk{̙R"/,+UJi ¨T*QJ,#k_}XG;PT*q.Ϝ9z!9{^xo O~ryy9z.BN/R ˝Voc}}erO:uy)("nu OJ)+p*< $ᕎri`DH{>&(IĢ~Xh 8H 2o˜!/7߽EM Yꗹ[DR 1??|u;w~}>Z]Y{v EJ'Vuo|/,mTԺ7Gv'8|PK GE uR;*Ni ZwLOFew> =.,>{X k$dzä.˜95LǢ"i4xKn>K_xpf\k'g𾙝s%[3p|&Dҏ@\LOȠw!܋KUCK3 72$8~eڿ|d0I/^ \vnilotUgK++ހR|u>3`tvژrt͍8o^sYmv].Wƅ =ARbrrrs\V$qU*S.ʟoZ \i ~&B)r#B0Xonll>rxqTEhOH){ k q9gqιbo M`0T*ssksǏϲ\)挥i?ˆ)ep9Z-B)r)Ο?Y_^on{ RڿG?QB[og>??Rj~~;i_~\.>y|@0.|A^98DtF'ZVZD'|YbKm+MC^**U\UCIܷT 0Gs@>>+7mPcR IKo-+h\pM~_5KPUQO~ާj3`nG(Rƫ;pi 9%Q@kXcL1y8VQQ(< A "%I7,]=PAYL %$RX%孴Vq)jL"ŜTT?͆c) -׉fu>Gg,רx7NJӎ(|pꊑcsf5AڼTF-(B%¢J/ V=H$BSM8RPDR !'ֵB4$uo ݗfceg_{g:_}xd{ܻg_7^Z>T͝l̗/n?/\چدn؃˛,!u7}׍+3Wo{7c [/NUt wl,@id'HL RvK@AwMy4n>|~yiφD#uCBt41#[7W;F?޹ml}ο?/ sI ӆR|2zk8޹kWZ͋bvv .ι"Kr3UkUt^miNO60333fYS2JiFDJ(" :Vr΅RDQ [;n{#G2)(T{l6eN<$iňGlaamZ!8w!,h=puuYBO*GDƩ `.] ғ0%i{5G|)'ǀ?ϔ-\Hc@*laU`ժnwLЕI'c}!}u &KeY*^4ٚ ys>ỷD\3 < wES6}.u`EKqeOP[}>/E<;g"K ;hkZm-Nc8g8Aa8E]4y (Z;p;G(?8ǽ}VOL9п}ȋ*бWavH10p1r6ݥ(aQZ}Hg]42 @F6X봥ȇZV\DG21aժGTY $8DG (0"A)#q4Ye.z9%ܕXf, fyw_XBHo'S vUk'~'"PModD}r}pɢB^>ApHH mD"+&sqm9U.4 `4I.t >QEϜ}1}ǣyj~nTWW/\P.VAӊ8on~NYVx<KFD7y4zzOPU,p\*Q{^f0"2T*`̦0R ԓO뻿Givv7=#jZ<^TuN:?Sq1Z:}lHld[[ټ(^gȲW;OT $KCD~O>uvsf˗l !c:I9g˿TZkZQy _#[͙J<ŃAOJBdTG N3Y ĩlx4"QTǟQ DflrKͥщ#8IP(dJjU4MPؐ|, QxYZ[4B2̗FɬjI4!E Y u D,ČKI IDAT0m0錰 sZ*9ȴQB#f*:9 1:b['̄ _%.|T94Bk$ 8!BB(Ś`"L/J}ǑPCg"+Az\;b5#CT'x_~z}Z\mx_ˣlKnuCKw>sOn{C,mw>G?~͕n緞?CG~Uy2J'.okF/]?]d6j,)qjiIT QFV̝Z9OlDD<+ j5ɮ䇫Yᢉ}{;"ޙi@iڥskpH|s%jT0嵫"|#̶ۭǾh?o<¥|;K.=?r+Wxnnv8>;ju$x4rvy<{}{{k{|p8PJ5vgD!*_.,NtM4'qzsk{S:s%ekUW=_W&  \\X@+Wj~('Ѿ充ٯ8˕\{Q+W,Ͷ\/'Dpak"ވg 2?*A3[OU*wx׿ɧ*g}!8y<%M t "JT)U}߻ۗ{챓'k`44&ʲz#"!8eLZk"R8GlSJIJd<oolmm7M)7,~~Qw)4]_n' J{ m`m.<5@H)< _fi}m ,7tWwVZ7=7W奋WE^ ۉ ]I/JaYd2d>w4幹C~a4dۓv`vqȍ잷rOUİk^pG)mR 9;Wd:InrsrW=wڣ?W)+3 dA9b@L؉<)p}wܽsɯ}e ?=ǿOzdYxR?%Zk3nY*_mb\n K/>4V(@11;b)\V0J[kɤ*TBu0 B#Xg ahr’IEU0g[^=%HaEȂeAOuF ! Qx#R,DKJR֕L;Q%$0ܤE"I 'E-O&94=HCex]@~1y*!ko- >$ciPNA¾\{y6z/ _O>0˻Ȥj$v|u%)h=!wO]si;1T50P\&phRR)Tro|i ms o`όt sSv&2~TR2@zNdNǒY\Nw̺{z3 ϭTkaٗ){q(-36z㍟A``Bd!Q2Ӫ fQiWE(#bR $ P SUX1V~9;'"2y6*<%HU\JN@ tX 񺶝3cGdRPZufORBo H:UB#2|@Q8i X(g[##%@@ʕA ʑ!QID!Q|S^ GWe't-;XܞQHol~zk-=2~Н;Z#û2w/)n0u~uF2VB a@ r1q\ʂ4O\^vVyϯ͋Aq*tc׺L>ЪD8sF@ո։V-1;[]/*aƽ,sr&jcXuI*%XkiMcXVKK-?6jS/B$ėCӋi+D !Rsn}=sV EQ]J$ )f|=ODDB)pW`&uɤh6"f  @4(P'=_*<鎱9SѮڢV(p—,wyJƦVAdрl1E#ow#P8rȣHOXYJ[!RU6c6R(gdSPmHﯜ饓K0s&Ɩ3'WDq{iDPgIK|OU՛qEݱ0PM8,8[ty8xlZXnoT0jz|aR9յ?ݔ_ඦލtN灹'ݵvxV!Ӳ_/k=lơӠx(Ї>|J [$gEܺrJ[;~B)Uunll8=07e\ff[G~衇#dv%Irʕ]$(fT7vѣ_^TVzV=lj+jQd>̳Z(vc4>S!Ĕe>Jo矿zjј$SϜT*RIJ D$%LK_҆VRޅ@Ea)e8 гJm,BRĆ 7Cg4<:?V-( c|a[8yjEcBBG\ek=%$+ @lс(A 5D7PseJ lndŊj<<ʡ< gf+q[L *=IqNzCF`@`- @lYe|aIh ${R PH@FRFZ 6@F̂8g3tRlǁoZqfQ0.d$rr@*$B;m,i =)m34 (CtVh?t99鄇c&I%.`J8(0z H"i1H٫B?DDHSŚK W~̃<ø. GjsTj:|ܫ4, ,1A b] 洞WpN=oU?m@Dj0 IX,cdɐ t 49Y:vdF7!;(r_=0q#ᭃT)͹ aTgerLwd4_W.gKJx9JRD$X@,N)<B(R30YYɂĵ&oԼRKxk 5["0x]D_jI _hiPA!|¥̻E`.#_U?iCo)J6*%`-`n fwO΍Tt6M:Z!!=wol$qWv{ff…kkQbGaE~}o<]8xࠔյ8~RK{キ[kQ9%RXck̔6 ժy477/h(L$IW֣0r)*œ,Yܘ_\7gړd#=hڟܭ8/ٳsssvJ$K|znn{(ck3HmllT,Mo9~KV=rplU*͍0&FZ kkJE *%,r8XJRϝs$IsS)<<4][[Va::2zcǎ={6ph֧rzv֔ 3 iy:Iٯ=÷r((|vvDk.`Sz ",%P(֘$I"ފKN"dZl8'gg  , B .R,HQʜKVQi2 %FPRN^ȳؠ0DQjN !t7@T8 <s͎w/tBi7:n NVxB? iAp%@R0"a``&L90l) VR ,v&.Ѩ؝y*Pfޛqwh&4=tL $s!XQ %`@V# DFdT(l%O d!A RcG<P;az"/OX`dj>:YH0:@iZ$Xdʜ@SydTHjf I"EP KCoU.Ȑ"Jnj `"T).7G0cxIjo< ^Hܙv@(L!MĜkE.Iu2纫tP v'yOzvYk/ IDAT\ݳtyٻsw9X˝#y?pl[W¡yC +lw.LKɯ HYkO݊ɝ^-F߱Lt W(١Ɋ0RrU9GQĪ{˳kB{R+;gvτS/{ns.ᄋO*ͼ3+* {!z2mw;A;vn47Z4MvFzGQ1\򑏬oM8 5()Ii!0||lucr `Zc17R @B덷Qz8N:Uo {߰墳/N7$qoq<- @&o,$h" R8rI%SXZ'}r2 P)5R oHTtHK?t{5(tup>xK/MiQ/n z]o\}eG "bDb%8j]ָb0|fOH;$Pda@F̵XNڱ\2jWu=~}p#CE2a jFƚ"iFrȨLӪSX6*@(%Fj B3,0!Β,\Tl%i,ءRId +cB8`O)kYxAfr=X\ed=&*c!AR⨬LӏuF5. ,R܋W"#:R%FcvӱD!U  J#W"㛽%pS+u.{KCEaN~k) Օ`xlj;;vʥR$y\+Jju2I5tLk-97D#D)MDBx YDzeStS@o) |=zt<wΘB}]3@,̔yF)Z+"`cLk}Qy]v(x,XXXxmqDa/9g5Ϟ:l4}#CDe {EfiG bJ97@AaxΣ {߰o >z^ւWk 0@v+ׅ!-ɰda 8p2EE,!?5 |ː,4-L)p$AģѤpC1̒*Ck,3GbC8DEa|KK7oNx{Ý]V1"a*2A8$Bq=@LXB) @VdH%iA/Bde*WIJJ)tS*wz/f>ɇ /? ~ Ӕ(QsƲYD <I7*4!RB]JZ$? +I < 9h֧bP`Pc;o.MN֓pvΡK !HOd`'(v )Ie^AgҒԊdQBb|uR: Jr`&%2@pI2gIPR암Դf@ FMWWacޤ=w @\s|1>vz#<04W=KS;׾{i}:'*۷OXs (}e{k\FtjLnXU0sѾꓸ*! xbMS1n/,27޶9ΙV@ ,DZp:VZ(J}0"Ii(O_W.1]K@xnkk ]=vN+?tB|]shL˗/cʕ8*ݻ矽r 9YO{/^v۽~gyӣU*N_*1Y/lT*f&676+jg[޻ V["z'/`m{KX?NeddrehG3I ,1YkXk )777)E)EDS4@R%>1(^JSOz0 ox@~x"wĎTBhByYVR\Kx82sO? p{{xC=iӹvms0z+|)?bd٬74-;vy1I,Ĕ7B1CD!#c{un;h78|BF{mߊmg/A;*•>&h(3m@"`&GHBX"HHT2Y89_5J͑fS{ cP9*ȳEQ"₠pi*JNa:`44h:ѹZ火߹8u~;;`L! ӂE/JD X!I*rAGIڐ^1z Z/LbA?Nϧ?%_yzRh@% 2^3,J5I2!ҞʒBrRQI,(DN)Ip(D؁:X 0 qXbFFVmL!h@FFF BhR `쁌) (YD*LjIkdfl#q!DPH EcY*[K]Хql,gȎa(H nlH 8@ `ޠ0҄I@\{7_|;WCSa+V,GSK.ŝc2tW&#%]\g'T|<g8](md$Z觡Q%q7MG> vЈۡ@+,)]VmfA3<*Nm5ET4AOׇ~71j,NE+wr_Eo C˜2}^wD+:ǩ{laf(nL=9G^)`~2hx~hd{%хa u!ڷq&’'&'kkD&VBc[z'\[]Qj53윷V+ j{^/MijuyvnJ$I`ccq!ul8fgzPvceE@N~"MzO} {LMMoovK(=bZUJ_[Z W/_-JٴiuߝkF3t R\,ʕs.-OMNTjv3{67'79x}q^Hct]Z.JمWxK/ާivGFG~!P0 !Q)|P@}MuRJRB$0T*I>sν 񡡡MM\֚i̜B ) ;#˅B9 ctΝxttv#" /_>wb\L,\T(ϮH_ݝ] ٹN{ф$$!yYP;2TR5/ n{kjT64Y(+%_} @Pʬ/m$BY{q$'[D6Hy v( Z-o, fV<2RRbtzނkIn\ҵ^]Zzg 4Xd;DUHR T>b_P Ty'XFQh Yǂ<%B 倐gbg:OsG9LmUj$jHָޙ?wʵH;?JwKQ4Yb{m+3)C!AK q%C!&G:+Ph!Pdb[e0L=" sQ:Z}Ǘ7{/->,8m,N'ΧeH+գG ss\5k$>ҹ/{'oè @tt2GY^m6v[?mf~?j晗`~ de>yp;wdžGٱq|'ӥG'O79S+g-3퍗Lxc:Yͳ<[]]T*ZhАfks+0f靛ҊsccN{mmmdddڒlon(SӝNGszNT!, C36:ZW%Ijz1B|}λŸA<{"iK Ţn6J,vwFRjc{:TYML>|RHVغ'/=Ο~abbzѺ\,//]pncc^ruffP(0~R,ҕJjKA@¯!%Ixss{jS.hOΝ;nw ѽA=/rwgY6(dayzɵkq8;-ADs>Bk-:뙬R1CRoJ>Ifϲ̘pkk&&+1 QO͝u)s+xQlm5wҎs. ; tI$䦸)Gen܊[Rgi^oQu- 02 R" ` 30{`ف!\U% E䀸K%"Ld8!P4{b$9 1'YCs~% @(hي*~frgH#p@)Ȣ}Rk*^rfPP KF!iM @+T( D)KڂnjӠ+2TQ;ugZ8q(߸8z '-jR΅įl6LGyg$wHnw#K<$ gY"CzO"؂#{)zb^#(% =B"V̎IBAό(H{#@)6a bZܠ"9B( ^`@Q<#T 0zFCPu4 TJ M9at@˼i< QJ9tҝwjl:ժͽR @g{'q!<ϝ*r"DD2ThiZ-àJO w:$v[bEؐF!b$c<9=m($ϐH^6RJk_־DDxQx7WCJ`v+a{>^]Cԟ3sQ~N핳F PZL,(≔9"1a`@aqh(lP)/(3abB 1 awB0#0t{90)J%J$`X-LL%ojo]&K`Q&PEH<@us.tlJB~'y,fCwXgY LQT|hC&y`=oL]Q/>Pyn (Mh1{IsqPH7q\4\$5PvrCa"fADB@B!(Jڻnf5RZnCEDZ ș<>DPI`U$h y%,00,uqb<2hm1e7vV}wcn>ի_xuq0Ⅲ{w>3wnMU)][Qi;K&şzV͡/AW&2nm| wR:|NE3#>Տf}uB0y~=}U 6}n3/=[RznZzڵ0/\T׻tRߴWJI$Isnueea^"jvP(fsIӬR/..6aPvrux$(ƔKJ&&&VWV:NR.ӓ{oYivG)ʳ$bpСRFF;a|twww766FFFٗ^Z__߻oOyjwg6JKٳgܮol&ml&i;@VZry]Bt!g!jT( sc/]™004<1=Aj93ZEm^Z~9؁y}}}hh1{e C39X*%IQz"RٻwA)eg!TV՚^"D00{fQ4~nRv@ dB%M(Vٛ}KUw"/˭{ K! =ն jP, Y9!'ȆD@,he4D>gE@b !Ĺ-%'.HF6 *^ z):SRC}iH44WN̏P6hj8/_c 4*)+h=Ssz+P-TG+YLxO=(, iTȂ`z ",N$ȸ\Q@Rd[RlchABDo"BJa+XMƖډjt\+T!%UH +V0}aPv)6tV΃%U@ȱFV:Z)KPE+^qg 3"- r3%b *7 A#A}`" neG9AU( GNWh2;EUSP8mn!lkJ6‡9g]DxS{p|x⍷a&KFѿq}{c 7g2H\$;V*/ʅ z{s/0R6Vg)?zv{nXwS ԸUv:ۭ{iGڜ[x>}{aHWMC/jٻO~ҥK r{_Z 9u+Ƞn$~nιÇ9rqR4|jAx(ʲX,2Vk*~he6l=q{㎻Fe9t:xB_ QM`pB@*EVkX 04R(js}P(@q8JXkY@8R z^^&?ӹV|o2Z&^^p|s @Ј# ^# 3 x`䁈$ aHPDbRBB2D~RT_vʎPp9^rsu:'BbʡO-$IzҎ/WNP_ w2Hv\D );B)ݾ[6SuXQbxhM">8E xfvl-3g1 Qkeef7wfbh&Zx"< }' v^V, Q*h94*LX yBP1ƙ CR7Ҡ Oeў)o h@O ^ XA0&Cq9zqd S( $($4{D!bYr%Veq:;w5>jβ %]'Kx>ap2։d43%~=:m:XNf{'wߡOf/^:7k<3N7ÂB8AMuTMyj+>sቧ'rUk-;߳*= n3\ڑB$OuwۿzR?T{e_='nQo?nT+$I,R).㸰g%~e[; V KŢĞ={/Yس'6fVz;pVk;|nj[jVaP{Y]rʥё͕HVml./kr☛مCnWҤQTsHj}m:7>>gy`Fcچⅳs|={Wn Dtݝƾ}{7 ;sφQʨёK=j|4沔cC巽-So~`{kRzWgV9_i?(y>`c8yˊi _|yzz^ ?_|qddZN$(uDRJ=le4'O\[[ իۭV_S*]30 fdY. rlAp^)\);qHF@esAH<}}ẍ́ b8{k}v7v`:o\"c4{ @e=5A!3,3!,$EТĔOl9jYE*rtO9qo^p4^7Vt6 dO\]YM-:FYU)nAg .=[aT u{YW )T,BƲN6#̍EEЫ2(A& ˀgQ(Fi$gǿYj1{J0s4 [wց+ 5JyYr-b0"B"@ DBJ 99ZJP3U.ʬ#6"iD90&`&BqsW 3!伀 4+U3֦~"hm@8 Ey̠^Q ,h1PlRzoIsǐ9r ݉^U:XoTfg?OG+G\8ᡃj"l3ccpw7p;aŤzCzdt^\o}Ë3o 4#:9<ؤ%oh.™4žn]^o]+οtNyf粻E@`n~.g~llX,nmmMOO#k8;Jmrb{9\^T_ʣ3<<gݙgFF?iZ;;Y Ӥi}חFa趂 /RyGR[ht:MҼZLMMiP@77J"u^Tas& g>}hyyyyFiwgpftttl||bjƍFl0¹ٱq"j4z׻ToBj#,w?BD[[c{}'fg66v;ǎ63;o|gμrcp*M/AY Y Hĉw_iɟGy?O[HA ? $ɲ4m6Q ˠ/~_Qxw2Ji"fVi%w<"~kڃ )D&B<nZL$#BB$\ײ˨1׆^wB+xo{`Be@nB@)fGBڃCOH#!"Tтl0 f,Gbи~˿,WFkj<>|cj lx8g{O.ۯNsBn &U/j$vjMmɿٱגKpYaYDhr4*E(vf5I6O35V %ѕcf,ϸMH D=# !UaJ1Ep4仝z$(8Nq1g(N)R$ g1fbZwơPI^H*9X 8_ =FGUFy-|$x'@yQ ZqeQ9*VKӪL]pPFG`Gͤr&,bFb$✠I"QHvXayt*7|/Qw7w}8:vz@uP}+=|~ǫc#E`Ջ*Ce+' @ ilJaH&8&_8ecCo[ s ˍ Fs:6ZqQœɁxU7Vǟ{cW@^ѱӜIqWgoȾh?ǿwRmc/dO]_knÅ7;46r᝻o|ɧl/-->|6A۝u:[#cfgsc-OٍmȾ €_\ju 8RlfxFpy|bwcǏ+~Uޔu855tgfg^\.K ҵ|9u /<է$ݣǎ;n鬬?q#k+C# 7V4X[_vmqkk9::Z)Vcnvf>58tpva>qKHF_[\\_h4;Ή'JQ\/WCvZwN:g9oܷ0j;kxeX6=vǞk|w i2 P4ƗBƩOyZ<>k׮=7:{=tKT.9D!-_O;h|=w:x@)"DHB ) `ދ00{k@D"2J+YXBF'K!` @"J8 Bnŭ+q"_eͪ>x%mA Nnm  11~a{Ϙz22d T4Gϗo\4VlM6>ˉ  (ﲅeӶr+ ԩ蘮ޭ*r,P! c)tV^0PZs鵭n z1e0j " FadTbbRf꽯|,H$,rP[Ș֠@P WT*A?ǪC_mnrxoaŽUh;ιᑫW;7;W־8qsl+޻R44T_[[Vˁ2FIz˕r;;nZYow{yzhZ>6>$Dۛ9~rxll++i:up=駟s ɵdee宻\Z^nS?jllljznommYk''v^|lXvJR8﹯n^o}<ܧ?iTţcRNNN/GqX)O=;A!f G^ Oh;O4]H IDAT{h畢Ra{8s}Fcn^!:W.O>=ރ x_xA}{@*L`k}/Jy#bYa|ѱ8(z_n[dEИAz5cTD :t֑RaIBDiR(Ҥs;AB<}L}04FPDba5B^^Cu ;V܊!q˿JQp:6UbX9P3 8dB +ȉ48g5])C]st8T.b_~x^) " I)6tQӖ ZրP`Ubxx'BCLW5XXNQF!`^&(y) BRā8MExQVIʼn@"n&;Dx]"7TD2JYf&`T !V&!q%)<*PēbB@dq)""@BF%3dۢbsF-jl Q=a*yRA@h4 % :qʓa{92]+z#&bQHAz`q8  ܝE!ȉOyL$ ly}r"40(XJzgOxW?txP;;|q:G̏N!S67ZY.dŻ\~#Q+qܵ::4} * 譋çV/;oφ7 (b$\ -Ls.1Qje}‚#he]aR ڇﳄ*+,үOOӿ~m~'omfݿJO;7ODfӓSPY^m.^[*{.$Ir܅B`+ ٳDl[F\Zسf;.\2  w߭~'O=Ǐ{wkJ9xBFJ i%ux|zum]KA U)J-ɦ,>cВ}K=$DI6%))J"EID @hU]qD$rcK}OȪԋȻ|^Oqhmj=6^M$HV 9 VjiQq1'/#"!4{.E>Unu5*VVt%gAbI+$T3סD/BD^1hE  l.$9*1`T !iTF} vgkvͮW&d x~z&+(Ԗ:}K}-  xD\Tu)X^xՍ &JA^@ P  ib^<2x,E)A4>vphxG? w~z*~N͓SKJw#3BN'^s>Kՙt0 Fj 鑑VQTFBġfU*4tiqޙ+KcJ9Pl6==Zoa~zf3SkkkkTj4<ܩ?w#ёկ}?m pbr)_iͯw`!$t ۆFj{F;qYЁ-f26:;v4ͼwn7Owݜ3u&r~RQZqE1ƹT}~kkZ&r收u~/g ylf\*Ţwn&Zk[˥jA KYdExiB]Y__wf̐"⼟[H{p$@B j_c[o\/.-o~ox19x6LC{.=o𕘓O3Ryj޼ISB|us筌 J4WoۧZTkSL!XNZօ*2x $_4- +F@y/," tj` RRNvn ŦlmB's)G ($BbdVD߷ x| /;HH ͬrAQeB y唼G>?ڐT-zGB,ce.l_77O7:vZ`zOiﺃ37\78r51mnopZ/g5[|jJ` oNɓWAO6woϬ;s C#pum5G!9sT* Wg9+xuecFFFz!CE|^{&&/\X /yq{ww}aHÍ&u:jo}˳>Ӛ6GFNfV!q< 8k7ߜexQ]__'RQU͍ӧOWkUUAn/ 5z_|qyeСj\]^ M}{#_cX[]]طp왫pS6]vM;..%I=3˽ݛe`Zg"("l3&Ms]U8~HZkukw*eN=\NND4HM2,jnL@@m%^j^f/@P[f0,KZ+C=ܯ =bvPh3#Rzyq9(^Qe=cZٹ['6kw:{m9;W_Ǘqv'gҋv5F0ł)WkBC#*'Ȱe!ܟF/^y䳣G)E@ea}`;}~2`b/z0z{g\y:#mf*bx@@ĈĈdŠ*@ +A4Mv%BS`)$N%4ACʳ`сS !`3ˮɫH^;VPD951*8xP@䝄Fݐ <(B) !zMYLcIX 63?T}3TIH<[@c9p [; 0$^1hH)@IkDVh`iD[ ZbR`e8p KۈVcG(LjBNhT^QHƮD^).0=*™5$LPszb9ۜZAI7NMo}]| <uvoV {aj~olm{hOr74JopezPQ$6-³}#{@9Ѩl:Ctʁ} 9~D{qMWq~ۃMF|߶^YyXͺS/mlaȂ1IL;=p?ξ?#y/_p{m|/<\zOvX/X :^eA鍊nZ]]5o>~wϗϞ;3:6zכRt6#H4Umnn#:l\՞QjskTvJ˗/:z ; BvބA]X/Da7 ՍR###gϞ*޳g뮻VV4+ƨBxiitt^cw̒ɓ'NHXG"V ) zm0T+7~/^0<8 ++IjC=033 ϿoͻtOO6w>@lmL'w"i#pʚO>6u~zJau~33xWsB)Y) Diq);7[ ;`!{f4M(RJN!3an^ .a^t% ñNk^xZjU*K,ΕkF%\ZPȠOY+r*KIצiO+d(*n8W?/',{s<"\\k+%d[|X=,κ3y?e:`X4uꡓ1sOdp_8}~t,n݁M2'0*rˆt~'l P3Ն1"(PZHI_e\'`/= x!!Tl@{P Zmݦfq%z4s∽A(.AȼPDLNA!H!OS611 +(@hl=jH޲A@` RbV4"ZPVqwNBO'd43xRVq(:vDv(M&>2c-D)Z{Š&tPKNcV5Wҁ5&c'`VP^;WdX(oɃFɬ<{W9WY|ɳD8e-${CΩ\608ܷu9Mps<~xnbXXR>c{7n>z37_o/~9C}[\hwĕ ix^-,2D'Noh IRɟ;GycEQSOufveY177x1Av667g]݃>C޾gZ|n è:Pd`sU)?ܳÍreiiy6֒$Nshh(Ccqۭz`L$QRYtrbCi z*Rm!,eR1``YK*eFq<ɣaQA 'h]5L@Zlz>H@fŢ%N d 1!pL BdjkP5tH$&YP/CRRqܾd+Yb3W?}M7%k_~@{>0:kmtp-1( F>5mן8œ8,RPnȽ6yu?p#'54 +O)~]1^PjAb"ÞfMaYoO7Jq4J7~|NzDz}/2<ژؼ;v+Ji6wGibd#[n>=YV+fsll$DQ`Bㅏ>\,]875353;e|*JRcRܙRVePT*+=SkB\*ժkkkN[)j 7X\*)RJJ~7(vwv=4<<<6ѩSuםkӭV;lPgnl('&'4.]QzZm,3vT*Uυ{X{(.\lVY ދ`4QQDQ'Ws Yc J4͊ł:LyV<{df.R,!'+4A`3glQ"Z)]NJ0vѣG~Ѩ<Ʈ\\&Hi"B\^<;3otҹWU+ʣ`A𳳓ZXe )A2{-fw IDAT욽F/Uw6JXKiЋ.?A!X8cG 9PXH (m9y@X"%3!)vJ 6$H,Z$9-ZhD!T s\!dƃ`RF #.$͢eG3&ECifAAu6q1zB@J؃(AD/>XoW߈Y/6(BL4)*E( {l( ؇/JسsD*R.7'0+]9#dbExVCjD(BVQ0QF Niό =XH5$U"(%cC($:!e- ST"?zˑݶGhR,c[\AW!M)H5#ZYAtB#>p>PǦzcg̯Y~pdjvx"R3\̶\PvwWtFQY&oFǒ̅7yo__;0İ 7OItۗ>~SCV kIPuR@No/؁7tp˱O~3`ma vvޅ?u챩7>տg[Z[oa67mno-:4Ol:7Ɯoyp ;s5Ghz>O9~B)m}Ƈ~C}O\__[:As҅Bh <0];7N$ Bs熪^[vW1\fkn/+SONK`3(R)'${qlEc&&&({Ƙz~Ԕz{{zHtiqY#u]~{뜫 U2<2~V?|hnw0:q\*A >ëW<Dhp: Jb<4TݷoA7ֶ77vvvۭg"s ш$R*^bDP wιFC[jmDsVkmFAF̎sXJQq~~?׽g"֮~S:qĥK666 _J9 Hfs63N~oxݽݷb5TmmmA3i}7lREC??V)^yݨWAE>|@e0'!\9'w*L㫁$['mM-nj)kRP  x)uHQ9 `H} F4A'⁐"@)=[FD" @J3{Ȉ= |ߨ@@LuA<((LTv 1z򐐢&(E gaUa,m <(HD4y ,L"/AeE ;m>aPP@(FB]O1AwO 5 reB/bO J!$"' aFQ@ =V66$鄊RmדxKG>ڂb=!cf$3ʑ *CVx'@B(`=:`^)D <D*&bqh ԩ[߬!8̡쒗Xsk?rd>TCT:PpYմ Ͽ0]I8_yb|fk7>顠21dgS*Ou}-T`&r{-bMQnF7ߏ|Ke'{jk'/~{:_?oޛeGNzjgOƌPawźCw.VJa~~AgmOHeNUǘ5q0;%^֞=|Qt^?ڟҸrCk;߿;.qͯ4/~dfg{6Lɟ_o^# (Nxtf|q-WjBGHmy/>u^;WUƘV ##Ymo;yc=&"Ǐm4ٹggʲɨH(o ⑿3tȰO+< 7ݶX bY珺b\[>݄Az+͍Ccw;klnm 333s{S8.FQSI<ɱ}uB! c+k|?I.]Y*lmOySʘٽFC"!,H'm,_Yu;KVB?=nlGFYY;7? {jcks|lP VWGᅳg˥Ҿn4/uRll?9j?휫j_íG'F$I4.\Vk)Z.8 z呇]Zr[v899133^owwZs=s ڬX,0#Zϲ9wDF)bffGQtD̤H<=\V,j5JiaZUd:6"mnn=|r~$I "Z[[h|Ϟ( 9'&$\EZ])HnƳJ zGv {ͮ_xAҎ\m{Qb)#8t>*,+ ~Bd7lB!H L" oQ4Md!;""hj鑅)$&`" P$'ta@ /*8Ȝ]lPӇ`KQ$iP)LMWXX2;֣eVeE+`$f(H2AP J9IZtΡCbfJ9.V©u@Jk@P+AаQH%!sH&@$㭬b@" Z3LV")*DF9p b.@cURD{aRދ\m|PJ{BҁB"14B9}K Y8KG0*h! ڹֺmzC44{yKWК~{"IW&y=+K%`Ep^NkXkxM7.ۋCaX믬V: *巌R_Js+/^PAk2C:auiЮmLv=]x*EΈd#3_ad湿i^k+?4kKk+UGu[KAic\^,}?%Ҡ/GL_ۓ Zw4&3}wJW/^"t:a!~&&Fƫ׿zn LtΝ󜍎mc||wr~o~~odz}bbb{{yiN>:VFGGwwv.^U, 5wO<~oV~yyn{8z/_? ڝFѣGWWWgff8ujc??s]ojuڭfovU \o{"v_(…~ ׷۝G,.^ɟkCC#GqcN  B#3;ﭵrj~sȥlT r+zđWJ}35f훠2UK\~@/RʡU{ǂ!)DvCE"bHG4HJX3dȅ<LK ^`7{Yr oD\yUe.Ӿ^Bj!V{ fϜsv`CI ZlWUJ?]73U-FhPNfnfĽ>q !3,hi4pG@1c8BH]m 8N{<"a f=Ђ%h1biX*40iH#F(MH#5csFhsfu%޿ęI+J4  ҄D(YS%mq033IBvtb3A(HL;f,!ZI#CdI1"2H@DL 9 IDAT1 2BDd#LA6Ae:qd eFUk:Cc[|zΥzO6RJN.RQX_Y0X^[ d` Y`t|M+qh"Ƙ$ `J2 8L Z,fڴ*nE4\DR\L<5)173 @A 6}9HA'n"b00f ` !L Yr8c X=0A$`,9$%Y 1L)xČ18GH넜2"2f(0.h Đ1!yҝ)MXpސ"D\p$H1#2wNl+(WY˿߬{Aޠ8(1w܅N6\? g\m`_*8:+kr_};˂[F1SJN5) QystH:sZptEǃ<={vC '__~oa.`HֵBpAe옟+\tl\w3$ϵ˵ցazj#vbڝSswO-m|56Ёm-[cBKĩl=;M- N\xw׿ÏT%ԋ'OZm˲ժT矛>32-?3]=_hWw%Csã~_][]ZZOU)@ۻOsU V,X γtJ'OK~p޹kcbimMJP֪bq{ob-w=7;ģO<6:2j[<JJ.cOߙ}l&?vl XمyeL @оBYLNRF1K p)MX"Κ$(QZ)v,mV&80@U>"f2.h'h8C0Șd#l Ii%7Ü 7cI-U"@4 HK>d4#> 6u𔘗!#48g`DHJH|2. YI~ۍ,a'ڴfjP0va.~Y|\gcIeq\=f;YlΑ1KjF`m1cH#d`lUmUTXW44+{*ށ!۹F}O߽]c-i֬v>1 @"6@4 =v=AWA>UR 4:KoΏ3;m*e]ۊ;"y%\x2ɋDzN*|b(5n4ZQxfb,?Kyw=b޹wa |~}wW^aS/qk^)ա* uםW^y=C?R*v ,?2\UoBpJ)Һ\,eөfy̙#wܱjR) u:-dEB!gKX[m۶x??ddxdq~L6 )C}{Sf}}r*8z]| 8̧xBЬ6mNc_r]^wܑdN~qvf#?_|/vt8ps===####gOZYYZXg/7> ?\m:AEDB؈m 4\.ҷNѶm˲yPhmFD 8+sFCb1_)WnBPRyo (8%s3ayd۶L&))eFs׮]{g?OcB~\׍j !yzјrDT(֌l6{(qgY|+W~=CLNLf2iDRBvJ䄔qvpǫb'''8r\@_p7ϱpTYn)=sd /Xƒ_i6Y *>YdzRu-s8X1bVESDA J EV]mUzr`DGm3 ]=}W\,><48+&'p\-\)y駅֪R)1_ip|.?08"$p9x}Î{?*j'vZ !W{YɂFCoBGV\Υ]kEqBF+albR P *N1 l/oz5@6%m6a.m#W@ج.NrX9"lA@8aBQot()Ѩ$wЈd!x^FD8lUlA䤳fSy2$UX@oJBUf%` 'OT .c-6 [m Ò8KF.)F# i. WKf6h6,6P3$lAZ!FqTFk*mpsjGr]-agwipoNQ]Ow-ƴfO.W~uVոB@2j:cv1sG #^vϧNߖ{hL|s}v[9+j ѵg0PCht{lmѮj\>I&j{&:l-UJoO_2{ONٖ@x䖱oξ\v{ΠueQUZ5?㺻wvlg~n'v?tcۖ'8ZgR^'#/=[.maa\>^xᅑvőm_>]xӛ͆R*A+;l6z>][Z-,;r7w_~>wo}{mb!ʤV\Օo}ۋ ;DQ=Nb}}}]=tfzu]#oysWO]vۿj:~ꙧ7d7Z%ǎ8"DT*Їo'J=wk6RRzR)c\|eOO11V.mffv=d[(4KѴsowPU]XXhE1v8G{fEՔCR[b܌Q bb lJ3#9Cd6"jSI!dԀ9''$IiقɄ5յvzi\N%#[YGt%F,:ǭgR^GrNL1@ ir$2`i 븥 !Cͬ^ܔ6oM[Y 366L>{BlVڄ 7FmքH+Ie .]xElnEa[cH$%Fl, hhdoP~[v>o l 6 %7# 0/:逫`14 P8\̌iq[ nJ ԺQ3C&*"l @qTBAsqmpڭ[f/ mfcNUc&FΖ>V{SZvΞ]NtFbsOfmB6*%[˵˷}w3"\mn'{O]7辽9OΞc!U90>jWsc;AW;'?>~"h:fXJj~a8?|a8ڪNlՃ_~w{ۣ{PfAɦٴ⹹NGFCZ˔r\=m4VףLȃ8\^.\sL&c ǶlARj+׮oo4-۟xɷ=N]8O?;h6{QX{czFuuyyvvveeBXD@dް<ַ|el> |]};|n헾?cii/ǎk׮%և;zhOOV.ط:ol,I` ٶ A$.ISqv*C'OTJq855_?r3R1q ۶cFQcDZa pLAN&6F1V( )d9tО={ڑ#G>_v}…g_IRǏvZBCD,q~$2J3gCa.89 XR8uoqq;;;TJ[MēTz~.K Z0 tb0oRSNٖ]w/S')<">|oaFCA|F}9pnnE "MMs1ʌ+7q-1SSJ?[e ʎ23q 8:uݎld}qPZ={JvP؟|㸖ew:0 WWW1cuׯCR={Z.---..ywa2&q(=rd0FKrׯλ"@٬Nٜ&Rl2aEQjJDw t:庮"'GA!sXO]h''37?٦E> 0` *n(FDmq9WJ!M?Cu"'"cͶQ#pƴVM]}&yū(vt2.45IZ%J/H|YHە)B37&rL0r Ւq ktxWt1+;~ϰxsmL^t+~LZ ,fuY=S>j7=s񗳗~:wGXl9moDNrMߖ*wsj+wkaШ nq{2FF/ݵbV~ܳꜪX#sY}fJW.wݺju=g;K]]wp'\p;Yo`VCTFnOmFucc J]cZ)[\ ?~8}lFqvzz,z[^_??.X,v6҅J7*&|!Q,/3?{'Ovq}ѣG}FglՕeLZma&dlfccs@7ycǎt-s{Ņ3׍1kk'OLR|ajj*ŗ.]zGo֧~3t An@ښ3Tc<חRcUqm0 tuUzBpOrdMPJׯΞ;wTՍVeYD455yiKؾc)LNZu'pw ZSX2z}eeX,q4;{駟lbqnna]=cңwm;y)9$?RzBsyDdeyyNIeW7&2.֝ihGJyk21ԼZ~d|c!2*J4IE@0 H1ōAF:ݓACLRHc2!v4rC1"0Ό1` $ [жDD5p !l'1Ji87h9 fF D5t$y)5,hj1`[-ci!{/oœ6+Dx" MCz"1rn,!{}7 fnkq6C p6_H8&62ffPMR!F ċM~C$' }Ŗ#8Oȱa{R,H" Dld!)c ;`$ccn!]ӭ *塺,oIy&XpjhDaO@Q̭汍%OvKօ-qds7/\ qJ~:swޑǟyYܑ5|O!PiA84QEO}wc}CKo g^:}5oF^mEs׊;ۭ7gΜ.W*^*3x:<<ܳlRj [2}/Wȶv?ПNgw泟 ۽{wNd7;pLsc2r g|hhhvfq\)x@].\X_4ZgN;w]GfDRx垞yܹsիW1CCCJ3F+p?~ߪǹITRʵMOIcǎrd|?eYI^Z__YZZt:rl6 ( b+BT})eɩ9-#sȐ1y}{oַQmB)cdLaF8p&z{{7R0 FD)eU8J)!>O>/!,a",˰uCd|Cdflm-7+7Wȿ ufo'l%Brܬstk*"`^`O^`1FN6l8 "#d)"  Ґd' 㜄 hW],%- øo&/D,w($m%f qLMmɥkMĄ@0 D[ħq ' iJLJ0'3JqLuƑPNCM#rDhjx',Ddo)X`ۄa&ᆚG:A퍄4ܠ}E" ddھm2ܨ 0 1S-$Ӕn{&Iы1?Jo؍՚nx" $Ơ?rپɖ`fkoӽwcm ~٩@Aj-KjH @F+^8w~Rtzjz\:?Z^Zx^\]]}r޴ﯯNM^dGRf_B\i6YT OXiMr!j: s >۲X;~[Z[o|kyauVJ1a1X/<}7; 0("P BGvdЉYHGr)''qĎbF9%8)q37  3o޽.uwUf)ogνWuWUCK/1hh-~WC[J\otH8>>裏yX[Yx+2??_MqW_}5'^L f6ZTҢ1ѣӷr9qŕKיRAz|aaeliNHa8C҆9:u?B##J1Rp;919?/9!a|Fvڶj5D Dt3&ޅBQ!V,X K6Ip~y9"+R Ȑ*3eE@؋\ypY b./8szBNWz`,(Ò(@Iasqv : y ٳWxw'_mxY!c&AAEH2zǁ@ ,S5)r!){FrTM49^%ʥ,s(@A!:T0AnPSULWLi쬧1*:Ŭj??ү;, ^z(-D%%r4c+rUVsl:eYQ! $@@2S nVJ!R()9(IDV]%},HJ)'|=_/ !vw$\yPUZ>W|D M$KU Kˁ|Dm@G|r٨Lth˛!E}:@31 E\wH}|;=XKuCjp+kTɦrm.4R'4~|q`lϱ[p=]j)U5M%Q\!uG4\]ӆMϟ?Wn?xƝ^~ױ}>O>~⥟;$X3?;z<qq gu։x\#ϳ^Dϝ;o[yme9ZxK_OݏVkԩS fn¥/l^ RmWiiu0zV66*J _]]zͭ?z_dstǝNJxFF<7wカ(~~СO=dZ+?,G>}=Fgpiݏs\ܞɟU5Gvė]o{SfX!~P7T7+ kU<n"P4`!aDP(}wTEe,:QxazL^F ߪ۪[H5,\4o}i-)MH2 0|5 X<a 聼+xeL%v·ウbQ"@Ll @8HvƈR\AלGW6}QLhwj ӆo^Rb85Sm-[VFkda/ep^ѤV3$%$56 z[2Z_TŚ*X[]5 hW[k-߻%;ИDl{3m/-)F+ U=4:2.NG\<\4]MQtsqUƓF2'OucaE%ךglL33}͕?x쉕;y|?g-SzaΕwy_|&o #p6_:$!ψ^+r]aQ!)V+qu:4Lά|-}}?Z ϿB}{׏=f Z\䙏GJވx|饗@V*3zk{|;_Gk'''&{ͭɟE .ػ~¹ jS.^; 3®R|kk陙AU㛯6EQ7nuummm瞷NLLNOOeYVT$M5;;$IE,DD$uFQ(z^p`T~Ңg~g/C2;[8kmgnɔgϞyĩ=vc}zaJi"3>>666j- &ݎJP2 IDAT)E{g"ilH)1&CӠ A^3nX!~P7pUC]ٯx+@Y D 4x/ P #fG@\xKSt܀QN'R c/6z/{>rX9F,#7DAA$4 ~lD5(Ll)[D{|Yp- 9+ED＀!῔+[/uiZ9X'kTr[-.-:{疗Z5٩9+45iO:S3?W{v۝zW?GMv뭿?<i'Xkmt!5z5VN7_괏uwߵc//\Tvtt|~[F.-v;zm߁FsgxFF=m׍2u~jEFyccctttll,aeY"׍15G; 1BbP sccc/ظ60sEaWV:ݷ?J ix{""Z+"΁ι^[EDy(ȃ$MAopyq1_#qIp@`kqX|Ć2|se]oY/#9zqE)m*)9D|^{Izph}c/3IP'v$[o;p]y*jq.* UjPJ<rVYC0VJwiIv!z@kmf/;O76"N3iFxPNbQQREIb^=2 iMLks`ekmO(֎R+Lg0cjթ϶Do;-jo-5ьaĞڶ ;`*m6%;&g*#fEpkʴn$qrGjR˲$J[KSQt(3/X?yfǟLU#RH\\^X7F''\Kտt[3( nK)c*{%N<{aNw(.r?#Vnz}iM:w~өV+^ĩlqGTygQ8+fff<'>$NENz׏OM#?Wk:pŅǎwu׉WOx/YEq}Yayɋ gf&E izfz~Y^ `?<99y۝#NG)YE155jyM4KC{]=f~j8>jިE`:,8V`,#P@)uKΝ;#ovȋ  )*\l 5JnWhi|ˑHvq"3>59>11;3}ŵZ%eV76S'^yU976ց̤sz;ޱW_SHkfq8ko;GmΜ:"u 8`{!t= I-!a= H j(`<||U.;"ߛ 7dP/} MEQC-PeJXLJ\*ER _;Vܧ]|mp58(^+DE\F'lS^ɠ B2OΕTr˽u[Uly  $@"(T$BP"W@s~k344)䁀 *PQiQQʪT{00hfaTIA}lDq w`"G)[cER8YaaB %@!""5p7 AT,?#Y҈a )zNd/VpJR5Q~8>f 2&죀B^DwPV gAD(4efa80BҲvz@DBt:9VMZ9p#ޑt9 HqǤ.u6O۵T}c3w+5onm71mݑk ;Z ě=xL36N3huUQ}ZAUŠ` 3f=/܁F"/صd!sake;UMf/NT:VUK[*suߪE#WH[Yq ܨTޙ/n._ZlF$< {|_|:=",B ,Cx"k ,>cE [J+F{TlY#9vۭ <ٍu$e)mmj*_S#c<"%^y?v k˫ iNOO7oG>/UZ5Ǐ?|ҡ[h4>ܡsx~ҥ8N~g?{=#Gnժcc>ޯ}}"饗[[[̓wnIVI},7sh_|_W~K $ε$I(SXEkkk޽t$Z1YaP)ln^pqscZi4)R hii?g~ѣn7Mәi1EGq\I5g$jb ԝN|8aTZG*$IX$cZbR7SȳUD&I/lQ`YujvՊDљg"4F:__tӅh(CʛZJ'#DD<_~u AΛm/ N /_S܁` XP( "]YY U^6p^>UkEvȎRRH \FG^΁b{y6|:e(-`n &\ևɞp+bw5# oQ>4Z73-4KU ~!U"v7y &(~27Ny,B`k .灒@) 8nDֺ|ʋ#Ap9 _YA@"/S8||HM!TU݊Q C ,A-˞б'@M \芛51D`Y0:㻓u}R&v!0DF`aԕdzd@hlBcL̆/6ʰka* ‚OF y]82DQ癈("ļ(C2fZk\f-j )Sʳ&#Քfϡ%kfD{ч1#D* *xiuL9',= `{PPzwD_4Ȕ w鮘lW6Y5%J E6~+7Ν8 e(obQDoɳ)in]zz`@ܵ\$񻼷?z_Ep*{pA oSRNB:`o!µ5 ~wUTvLײjE_^=t/:0h_~.#{x;Y>U1 >Z?_55uDYwpѤU=E/-;*vCay5`r"n3EG[(w^5 yJMͶ߭UoGG2(( !π}y{gaPFE@Y?( ,7 M%͜A|;z}w=x7m ao}pCzwae(C]EDD;Do0\qdG3F;/wCj(C~XTR#Cd2 e(Cʿxee(=$D"lkn]ԛ4V|oU;wPߡC}_ot{}3up^ eABP E#v&뿸7Vl ;w$gs3.~;M0^ R?ޠfP2 e(CΉU;7&cHtPkC1#'AGB󝍯oo \/|W ;wPߡWk|KnyP7]]à8v=^ofw|W ;wPߡ7Ows~;ͯjiKn 0`($2d e(CPkgѿ,0d e(o!T;wP,E_V+kUߺiy n[-wPߡC}{\^J%_?~;Moi#"" !)%$@(*}buz[@C};ihspa?w5ȶ]#!%$DE7|ϸGN< yNaF],D2mRφ< eIc˷od@R }k]?DVK h!i ?Fp{y"^[l f\b&#"*R@W7[^!}@vm\wnu<4w_XM\5oԠ4Vn[R=v7Y4i#}Pxfj9|~F 4Z]W ` 7DP)"$dxٸ7,[0n> opPE!71o94DΊ yd,+LU1g l#Zk+ *P,@ Ɖ4/z@~"@†}7D#T 5F6snDn,n )-4$f7pH x kAk&ϲЙxh/y8ؽ*`y)DrV#4@E|*g[?9SOe.Q٦SUaJD"UYQoA ex K]J(F͂w}gT,։ 3"AX@@< 3fa`f0"" !8,|x‰ʁ&D$"DUbY0 &eFV*$"T}UJ 8g옝xޱ8^"10".WpiU5t E z+N%4 ֘PxaP2 aAp#xf޷   ňWĞ" *Xw:X)<26,yA{QG[t8p$BD@P{@&; #r@atod62;031802 ys_'+={"ۛ%3sy?Hf5 Q]m[M^ʟV'Źbe @&P (A,P9Y`OeC8>YWaDpk !zq:RqU+,=r|sH=?NV|GgҸo 崨M=q ^!+DR CL#@6z"?^ )A!" @sH@2 "0 p ݔR^#fUS=9MSkXmb$vWϟ9z^3=hHgVN//z&]j$f:܁d IDAT^+vG񜡱8*57161[-9'ZͺDGܯQ+AhA"78P<Q† o,.R̈$B@XB_ A O "@DpC" aF!+XQ8Me. #baDFLJ,^vv #0"/ Dl~J2:[\aWvE[α3XXTh u9+p콳e'E<cR"=:Q"j")ebϘ[τ@D&Ҥ#2B-,+$Ue,F5jQ5VD 5v$ b0,^sEm,kuV\mZ.qֳY.g(9ATDG6ޯǡ#Yc`[;:Xup ,f : Fbm?n͋Ď"va<* őVE[p={h; {i2 XD&)O%ŁsĨ'&n_?u6_iLMA2VJCݶnۢpIsOGF&h#?%gӕS_lmͥg)\ﭏ؟|I$9EZE(BI#>bY " %6(5m,D!VUӍV?u\2;.tffF$#gW/^X~ /uU<#  BB~`|@ ;=bI8D$F<#HD"HHPE#:^ *%}w2^ 65bWZʢ+VUt6+FGFϭs91#5]UhMk?13Sz]tZB-me]\-$n|^۶=5־c?GNY_Ń'F& d&$T Ѓ+'ׇ}LDH0x cȃ H$pF"] JÍ@gJx$,}7;!Z>D (]7I1R*Ddf 8?>چ }3$TZ"mK!p?H4DnD% t{"$y;- kley LYF` Td`c(M?P B@`dEbb"EZc{mV5 ƫR$FGQ]f Df,h(e 6 xa" aRa\ء8p\[<[f ozݴPg.[бŠ3JgY,Mvk:  ,T DUbUٮёͭΡcI RJ"fϖWQQArBI[_X!Ѝݜk^\ʚHMR}/&s>e[m#z!RR @!HYT<'o4]uBrk퍀VBH)w "˕'qW:fn: CP}܃<.߀_b|G߹W/-|}C?td=3 ÀVEV;Z/XY_#zņ98vݹ;[#bf@4"X< hcH 882Zmq}]<*)B,'K!D`F1Bvkt^(:j Gϝh=s>Uzssbz_/k7;[&9&M @Q3 B(B%y#";(",(#Bww" `߉<Ȅ,HDQv)[{QPIwkuUԪaJw|d43H:T:=Ue|&ii1BRJekΞW+nKIt cz>{ ! `.)L; pKJi`tJDӈ̂B$aޗ:"7>b߮+A!|H;^*GvυJfgM6cDXB4hb|B̶%b q,ųx/\$ŇeQ< ! :LLulIU#UT(m"F()2#bp! @$R*YR$-`/,f'Jj~s"LN)MX)["BjQ' 0 ^j2ȒՑbb=REm" b#ԤR@+0hQx ^#9|aOgbevi'9JZ[{ǡjr)8؍5-63Ab͕lu s llxX&]o#4QD{NPɋ"҄ /QQH/vZuvϿ~n4UBD z=0A> C4Xث*Pw O"p,!$3SR}&@ʄ  E༨LoLqiy\ը^/n64[=y|vj|nJ'ZjT8DI4UjmR*Œ[]O-|‹/uX9{5[n{V5KU@EUL@$6pŒN|thT!)+PH)44JHf4")P s MB]^ГD0=B" G#CgxvN`+ @ܞx I& DAaWx m- Uc!+f=HH#+DE"ݶ9 -@{āXv(MO (NBA{_۲o9^{|nNY$R j m^do[mljY Ud[sI;0"UEY H]O}\a1gM7$z)PDKpSk)ҝ\yb6:5g'W*;Qy狔#0Z 3$(U. " Z-C/ā}8ޟʏt}q8^Oh&~S~OE]kf7Y>y|5h[/iEe{oWrXXtx˰H3L6737v(cbl`mLmYdj;wqniK+]ߪ޹wV]tnj66Fu67 14:YKyU ^mm(@1 /#2Ȥ@ye -. v@t${\7<"1/c}t|mmg67bkwhL\rr U>~.]rvTo" "19k>O[ϿfR^_}/y>,4x Y3`YȂǔH,WE,d̠3lY1\$#LNʌb:W bPY" 6O>i];Am03 {D"tB)3;V_{CM \jD>(R xbNJ@P>h9)o1dSu(ƑR`/wb 27sE0x v!!Gc c F8@H$D5QD&vgf%! (lLݙPjx",y‰<?iv\|GB^!pgqFosNh^0$5F'Vn8и`wy wYG\HШHJٽD lqZ^J0FvkAfV0-'΢14߹Ǟ֭ ){Isr~zxoǟn6˦lN߽}|3;M]YE;D]% Nf Qp&a)K,̃Y`Nd Ha >V6]P:DcYR!Hɒo[Wg30E9 bo#Ѕ3g$Nni~}y{8nәr{ldm}^՚pju!&U5Iܾtsu %x;\zϜQZCTXX\\"Гa 8akA;|R aF g͘E@RGǨ" Q$Ru IBŅ wdp"*b9߫BJop'˪|/7^򉴦|R2'hqՄV? ByIC[2HRv7-"'"R}@T^j^f֠ $%ԨbAGQ 0ٙDK\ա-.&{ 30/ʩ$3H+W }`1,@-eX 1̰g{#=3u-5`3R%Q Q+pNdG#oȃĊ-#PO%"k=-L(2(KaP bD9`%gV"s4,+~k?c|6/n:z m׾[ͭ{mw0bN!\Ei5$' UY^ [{;;;g/~˗_57 h crb-ǻ3J(=V0E?M.2 YEr4Ԯ2h1"t`/;+kwZkpW= = q)@Q|>bR3.9M{EmBIŃKykt5J9 l}ndHS1-Z p 6"h"QT!Ac5VJAHȋMDET40E $T`raqd3 i4 hBVK2<UʈQE ~P(H4 >Vdйak(Yw3J&\DI[3sse=fq7D]ND*;jN-҂ "bBFҠrr'IIƜ{H@Ԩ[UOnfJ"Vy^ͯ3[po93g]AMd8Ȥ2tp\VD}1Cys< ZZkP!:ּ֤㾄m |4)*>Zl|4TƧc|鮛]ؼK}s/}rnwKLF^~ӗ綯V?{ u~:v΋zܚ3SssV \|;8a#$ iEܻg]3>dvyw}g3q\謩7dmrf8,Z>s;; L P֗@3zZ+lɇ^ScCy$DBHA,I10H@NB"yir) +iɘ:7-7j<~zb&ފniCw@MMs4etvܵ?|Ƌߖ.7.\ʯ'iSݍ \ƸҰ8R٫f0JZtЙCd,*']2]0ȠΐR/a++<ʯҌO~ʪd2.,r;Pb\DbB`~R[pp0}O  y+Gq^?QcƔ݉4cF8f^nzܡK=qr0]d0kYM:*$T$0ԣ*d~a]Ir3'A`KFS`DLL,N BVe%$,L[LE-@NTB!fTo=̅g˙ atj&u]A;xnlG܌hrkTY$Y Ҽ벰:;rרb,B\D*X-̼cl V sI\z}yEzcĜ8Dr <ウ{zz5?h?~3Qɉf޳$qIջJ%ĭ6H!xV(h4Ν/㺮GGG]/_~xx9ª$ٞ'Қ-&kk[c'\.@\g !߻O3tm}3?F_Ywռ.=/͝8}ݩ *1nJ>wφ'wSϑ]@Y]9082grk NTŘsn;WE! ˈhs诿oG;|j;whM{#K'}&vne:;>.^cw *đV0t*nFsRK~Hxr/ AaeqA;D we%)8pJA. IDATt.A`زCM7ĤLkG=*eYs$/||8oSqB@?MN/g޽޺6YU~vam?ܻq?+=ܗ?/ŷn$ 䙐Y*{0K @وvb!3 X'#[x(!dY0RNľ*w HV]COaڠ!xH%b TAe —yAC\,-U_RcҸ7G:amٲZGqcdJ" ']pc2DmD3ϔ٠ DC">)3] j'g wrAz&DC`Wao32i9EZ  DEXHa0+X!8)Q1`%R;> =Sô t`,ƵdZXRuxpKS Ffũӗ/;axY;iX+ [BȖMK !OHI}bg`HRn1 j]!A|D"L[gO'Eۼy%q␒Bz{RT-U٘b4Sdx>M5{{q15~jH?2KԅRۧW|Z!>eoɞK`Du9[|o6wn>}ʗwVΩ_?j׉BbyM>Co;cn=޹1_w/5 `#A*( uEh6~T/~Wouq8F3;o^۬$#]tXն Aݜ ,,JdKĥfIL 9 6WFJQw9x H@hKRʙfo,NW1*lt 2[X?uqO^>jkr٠ip8vmڜj;{Ux㽻L>s\o}ܥM:|ǾO}oz!v +8q}GfwB ;Ds !{>x&`&g|. 'Fn"23WbvIV6Px(̫G1E Ʌ CU&ƈ؝pbfsԷ2PO<ÿLuUNI=c<!o3aH3l%uvZȡ)DL8eZɓe7 n61H8T, g#KY$0ȠC2\0gVfbpAX<1\)'fp^5eJ#QeI#'V4 LE5 EUvq0H #ΐ90*+9/Iy6b0 9~R5&X'-GD T"HDIL=͑E"*, gwfBVU&|C_O6?ˉx8އ82C|Wu?ha1.W/|٪VÃo>SGjl#:W|>܍[6+gW~o^>ڍKk[[;7/"Z|_? FFՠ@oI*}Jtp"д=NJ©SO^.Oz?kߘ,ޚcpa18zp黇;t2mui۪,j4xr"y_~gwafVH*B]3/숔{RBYM;31g ɮFBq"qn-nf W4ʆx6;ę[ùIl߾wLkG369f}~ih4kmxSۛ #w0L D!KS""DnRG\A IݬfdQ052<)ЊYA'h(q}VL dL!Ui&,G?iG.'}xxD*^v6PŐw=*ݜm=wE߹{[o-Ƿmwn_.gBA|yx?6gwO+Kyc;y]왳U_HŢ!X9…]e1C56{.O_ӻsݛk]vsݝ+mx8]FhP Pe\BqVV ȖhDo 89GZJVR2y R%e#Yym@F{$;!쌂?j-g[lp2Ill.Ab 3 <]JA^4A`r] ߊdŔܜ,)D `ǡ ƪ#N U,oyO vZMH龙#u?i8S$LÏ>|{>H)HcUq%[xK*Vi&*D,1&Hqm6#̲q6FȢ vxrrgsbIkTfƆLR rF;(UcQ$\uA>|[߳AKsEY`n,Tbҫ0 KPAd&s{يmShBD,U?bQ2zF0ā ޅ,('q>>lh571Br`arE S64 %ýL\HE3g&)'9[?7֪K.qƎI;()4mM!UQ޽xxS͡sgurfFRu2"~͛/>uJ!ގQ]>ܓۧ@+&˖_w([GdzO.>R<;&SfD'4݉H͉<1z[}Й_ )R«X"`G*83{ɢU*8ഢ{%f2CXJT.B( bb#/ rݥh01z'8H6ӏ$bE$I+};I5HaZ#m.hRqU5hr2J 5qȪ!&.N*3$g%##'J=X V$l=bHuE dZ.l9=.  AGUX G֎[,$rBRx42Eʪj!R"!0̭7k69%r#OIU(W'}UR1:xNT801,ħJ@@ ^C;FbٴVmC-@eD$ ]X @03K&nUOEQ9ELB QmS]2w_]`3'(=k>xӻ;@U..(:nِƛ&ArVuZ.,n]o/sz]?w6n;ffKz}4gwn.8˨ė{~/ogsxgrmnƲw )EDr"#NC#3g,LaOe&L@*D,F,PlvŠzd˴Y9$LSNmk3?Oד8mT'|ysq|ysKj!8} ^^t{ ۷/ߺP%JZzmzǟ+nz8Arq0`13T< yh]9; q}۰gJuUfvRM:`s*ШUod-h&qx_̏:;m3{w?U$_2^o>>r~ԈHk΋iQ';mdC͵iWOF8wOVȞwGc?շ?~]ͣGb:,g ShjLU"wp6f#8Yr@'f66 Zǿ.ӓ;jΞ?sxhdg5g6M^w7&5&7d ,!ŚOӿpë/;>ȩ'ϝ^エwxIƂзiWg}q[oOYC`9%T!Z7S&¬X*B%$_ fA{M>hG \4:FvJ0gC<Rj#$b(j߫(k65YD'"ٌ!*Zρ<{6괜!>f8lN6EIv\Grftl&} =%1HEY'Q%"Bɥ!4/m4j4A2KnV¸QUɖۛwF&x_o*}aF>x.qGl-?\xzxo~?'u7Ns>ko+Ϟ>1=;hdrzg>JUo /\v~ 7cxs|ǣwzέyʩoڝ#hIXd,-,pPM~&ym8߹}k=yrvy$ΚYl3딚{S uNw;noN"+_VVg-K&ؙDAN"5G+;VQZ쫀z^I{JP DzB4!hzpvb co|[kǝ]_y{wSO}Myjqqwk̙n9! xzw1?h߽wKx魛~qgWC8[8djJ,SX)+(3!VI$ġ< Μ 8(q`&'Iș͋y#rC9AC>h|%34|T'XǢr^zRoo$KUR9Jΰd0GyA^# L(%QǓ# c#T.8 U6s-d0Qt4h|aMl&$2ZXSC~\҆YU(L$JSL,=.n%dz@$mE=yi| _xIADA$:ďJK90gi Yēٖ#TU8,,!11Au뵝uWX5)R,h4Ot\R[ZO57s5AGR9y3_LF#^jTٻ I ʼoLp?uCHr+Q5]-CŨqRA[nO6*ܐZۮ[*zj=̈8އIe0 `ms6/Mh?>\oTz8o_86??r+ Lg{h~xo3,v.Tvɻg=xڝ>v6nLSY6x=j:Jɕ);([PIDf+H)OE-j}Ū8$έyڣ?ׯ5TyL IDAT֨WE>qn'_Nq|ͷɮI]|oK]K!".q#{&*YAIIΪ%j *dph 2ge"x`T0Q$p^'v!vb91 lhqÞY|gIqd"?wq8}o{}rz͑PG rh:AoQ0ոDa˦]r}?_?ݳٙZCgkڍ89! +|3EL $Lsg+I3aaxd́HɄY B Q,QAgxOY2[Eș3SFH%y&p-^+p_qBs@QzuK]}gN bPrk CZp>t'f!AY%+ '*6;gsJBR`Qj sa%]A{ߛgc/'wZnp- &LU&kh8,CyF gaqLWlH|`ai'6aNI {R&ΒC`% dXى0? WIxiGPVIև=Vbd;i҃ L3r9SbF= WLBȁ9YUɕ< ʠr $[9Ʋ5:s|Y-(=eb`f[uL'gN;9oOR U H" )"U%( MAD^@zl޽ǛM6uS<;;72dGYҞ %{Nfq>i.*fLGVF&1r!W3S8sD$ҪyVR^6dRY?XRj!lbe$&pmF__>81J;$X`6MTXۆB]qbPG,U| "͌ZҊ*+,@wGcq˾pBܠ:M6q$cAk]TW[Jk M#)by=k 9J*E)b@\mUOx/{w[5tyiJ=Q dI )ra$20XkZ(Ň - Il"%X!:TJ`r+q0J.t\km%,u,Zu-I}nWLk]_fk8]y qWc-~ r-r㗮b"AQÅ\kV">x.gRaf["(.?J/î+QWKo}.G1^6Czm2`Z@_ 濪K5SRa\*-`"<KM,  Ӄi!"8R:h)RK }K(x1wM KYT\t[niղǡNV)$3E];Ρve\QZp,[]k"/=+ ̤ЉGפ]ڕގC"Z]3_H凼d hy Ԇ-dXB'k=MxnXqغvSI_- Qӧ'xi1#!ciuuJ[eTY*["9eY˚ȫ!lenPCEJIkaoЗ^W{^W{^O˫N ʶX[Rѥe7{^W{^W{>aoJ.AyᇲBAAA]\%ۂDe2e e˹{^W{^W{ލA9]۳e(x]Nb+b+b/SP*6b)   2 buo J{^W{^W{7e[vUkC.}v}ˀ6{^W{^W{-k/3Bwb:.w[S uޠ-b+b+bW-Djܙ   R]ZFD$gJAA JR{di$  }]Y0b25-L@+ GpAA|Lah<`@$B@&dI,2* RSE|!g'@BԈ9qyOe*Xd  V 9!r!EĨ,B&V`@`P)RDlgҐL15jl)F/.w RTAU!fb4P@ bpe0 1 +@O "8(ZRDƗ6%@(K~Š 1nRtkkkNklsi7X: po*Hy%@*>ET;! RR4*^@E/}DSݛ=m2v ƻX5+/ZEnǾ}:1']c#wz%'NNhf  ߫D($M f0hTR1)p4i"ԊEJihej]㱵 )b^n5lUnl&1W8~Ӗ̰I/XXH>+9c,ΙL*͗N0M)Lyxw>ïyׇoO+b K# B=$6z\}`P^(^S/遗4!hBTѤq4)TQ*=a|ջKLU.uЁգTzG'~ P1)ʛ\0[uoSOhmI}33D*jd4ͧ:wbH GU&t*敖t9Д+zSSW?;Aer[Sh^|o7Oqȉ6%3&?#I룶ϭ`wQ_clm=8ߌY4A> AgCeRIL%1 lvH0dbf0( X+BG"P ֨R48vQ9)ۿ;nBbk;hM|P=zdlx;hQ5al:c"DbIfҠ~elO:U`!A*Jq =3.«Hs];.LF-5ڙHPdG<$5 Kp- #z߿ѧ[OCCUsn>3/ҕHp""T;v܀e/IBjtI#ա؃2zcQgݰ ?~Uxd~O~笉O2?-Qf G|I#"3[̼1hfy`|+EJR(V!")" J+Q+4iVHxس޷o-&-[8ftsֲ;/olxK7{pSc܃D~*繉n P$b82g%WzۅcO8Ӷ_}\79~u_?kB~"-ݒEf!䆗<{zgNpHUHy\6&t|c|2 yfomH6'HOvb\L5f#.<{yo\nޜr'ͺvy]&?|uEF;N<Нq션Oą6=W:_ro;W~=V/$_rTUؘ bӘ\\rON~ulra"DivAA2+TTleܫT^,^AF\Z0χټz`|7Lc)"]@R4'D$N"ƢNDP4i[UթW|a#i|?M~>Zzt_Ta3s]ߩk'~sP3l#m a\l׿lu۟ 3}ɟ{o!Ug-ǣ!M]jM{8#וxSدnf?eޡ h< 3]?>=Of[ܰ"H L&L\#^Ľ gR#$(^X!(U(Al}6VXDTDJ)8!7DB]ƜDee"G#h$r7V?ooE^"Ie}2ڽ{QpQg]꒩kLǧ7K~v蘥/6|;Wp7P M$LW[/~\ N(t0O?w֞եZn2ܜ/k7_Y#{չmn r -<1og_|`'VQ2lA ݭjd`f,SY!*r'LRH*4Z*i ~xj J2 iD$:d4Nu<$6^*ܸq3{>C{ Vgݏ)[xߛ<o'\}Pb`\Δ20o/9Gi>A_;}&xfw{mwd/An]EM yο0?3??/H= ;%"xTE;v>A{5 ;{_2kQ{LRJ7,X^߾Cjc/lkuSGOdV,Xf4?A4B-Bp lM"!HÎqL H) ( DZGn4C*B GWؚQůgOu LF("ٸ=˿>8bu{FW65~cݼ)ݮ[mv;ร';yn={pN6…F`0ondZlLB7OK}O~EsVYkZ4i@ v̛'glZoDdZm]Z8B8Ay} we{[ "d$ g3f5~Qʗ&l62NDXUaI)@QBѡx,RAlYVVʑÿVyci77ir#~x{Ǎ"o,0\l*oK?=ԯ={;3~XMfs7uN:~?x)[AA(̬,8&YW' @ 6blmD Ê6\ބÇp}=+"1QqA1+HJabغEsfnST3xCx_:lOQ3N:]g:" BO ek 2[F tF Q.h 9&nRnPۛϜ@L4e*^ֱ!DZy/7.hel&)Ϛ_flkhzi>lhlkxuWd+1_zտ~zzO?(ĕ]3j]9PvgNes^ziqο8woG9@VAɫb{Z޷` &qs8."| ɺU87wȷn[ȮhNB@[ }ge`ZQ\sFM+5<~ossmykSN,<4}ȭK8ab&s" ض/?h!{ʭoq7$AASwttD" 뙙/co.XU<ǢL~]G5;mwn] Bڷ4P=xMe.'UUW ݙ[` 3dINd9I#!A;sT;ղ/["dJ r{.>sBaխ'j!߮o-'OY)_t  }H$:~I&iB%D}@L[{"^koIVTsj3 ?ݷc'Q3+/Y[/X@lg аg [`&i厈57տ ڇ?j:`0?)tώ.K._{oqf^u~ч[ Zݽm->AZuQrrAav$,͔w/#hH$R:'LfتK=gz'߻'䑧?`=<`,D |~ƳO~2aXx9<@79#"hx)w= Xeʦlxxm>(qD/HƏރGk}v^A2W}|WzQI(vյe;0bcZk-J`IDATԎ9xyO0 t!#2AuQjBj*c l.|ǟ>t'sCko٧s;[KYFXE3^A>\#PA(aDhuw=qVzd7?eO)ّ:n痗\m!QPlXrD@9_wcEqL:}Gyw_~e7=Y3lf˟{oeK}>ȉƥK-Y4>fuNyq#WO!r3mO_e o8dڒ< !멿-/°=sM[~}$ $>q7;n%L׻g13`>ؑaAAX't_~1e Q17W>jI Ӗuẉ-L[ 0޷DPt-jz /_Ԓ~%WĹO'd75|6=5!ahlY*s;Vt4Z,Lr+z޲3{mb`SCDK23mY#9ϼkvI*?>瘟Ύ4Ϸܸdޱ׿Z5ߨcܟ>}c;|IC@~L}K2)go<alS~݆HК\P9!GX )*y>jEJi`@\͹Bk,@h/nt|K"#w(ekfFҮ5|0r!grq] *. ( HvDUMf@f`#NZR`8gB;WEnjrc@x.2v~*F0HTWHĂL7ݥqo 9q%߮WstnW,u7^ i*_y}ϥsoڇm 8']z?f`#j+‚ J;pgk4V <@Ĵe0*k52 >fL\@h e]#p%z9;[{췣%Ik&1κ <{d;~0kFci:5i͔8tufoC^3s(yݚe  t(R;abwV<B 2BBhnsg-dF|Vgs n5is}[;2NAa];Ec/$PPn{K[G30nWiT,% ::a &QB2 T2 |ⅷOuomӞlcjMMws؞}CTqV,Kgqѯq\go{/>鼙c?z*c<YҾy"l>L04wCW,F} ``ϖVϬܬ >½[Ի6cʗuy rΫ*(bo~q%?C'-tό paUk뢡 ^h=g?o=|T 'zQ7lrK/1Ū̠t2TQSSS8$o=^gl?BvFR k]0mCvK ^ZwqUv[~A瀽w9&ASele460qH[jSlmG&#ܳcmm8FpgoD1) BTk2oX<ߴ\1pZ1}by6vir{vQebyK\To4|pÃc]cʩalxk-7?nEu,Y߾|~D*ܪ((W9ӹWR(tOG*B`j}7ڝθe9f4u,˰sNϧ|>"̜A^:"@ b:pP, nN9;_ԅ.@ꝇ 05L/h$H&}s9(ݿſksqߚ~枇wi%=<9AAQ[ 6ZwdNߐ̻02,~J\ir!voyt؀h,ܖ_,1  —^nQcO| 6+@4*\Y?J͘U0ʊ]AAR wup5 ~ooIW5~ag p#nd *]W1XH  geC(k=`r& {98tL@8o}rI[k|`41F!1p_} :o} VnBTˁQH9[dVnpr`Zra5h-4 gvDK8\~}OW[3Gj ,Bq:!"bP00f$􍩬@D d2 GVTp%]5]1|fF`lZфD hDLR\: y^}=1J1I)v4JyI&ٶñ )/?vJ  /N-^fpjT[[GDEE^s  Z`8dW`W:x_0a.!ÙLuMZYDTVkZfeZ}u@d/"[K<5K7 dO_ԍ.@ 3+"^`[0+ٖ[׼2ZT KC $`vTPԋx++DPȹ٥YVč֎Ffjjɦ\#3eYcu|k0 'A3X`^rd9ݑT:KE6Upbdj}߫vy|K5;ƭ#Asa711VN9V)*7= r9XIP"v@;uܞ|c9p89/ zQ1 c*E%}_夐 %(0cZlZKKR;C)BDk_GGg<_hgh,l=A|6㷵Zk[O^}}e;+;4?֖w)ٖ֖Z00{N*JX:뙩!|zuNP8Oװ-j 0>Qto_NG'}+~x_sPd"s6zܨgU:;rzT5 i{u6nuǢYƜa#M]bP8݅>+܍DDDJQ]]m8FHD"#h&6@J;LKF퐅 2ZFT\.A ʑ4"fEo(U(ACT{!H#1F+ڒS 8nIk9xK.;[DHE'e:#Xk"ߘ .$1@c-O %@nGky BR` T:HN71Y %@kGM_FVM<#Hw l6dȊځ.Տ}mpVSjQsPՐDݢ兌>T՚fثu# je0n2ul* F3)**JC\g>3\*X!T)T!X5l RkcQZk' Ҙ4"3j٨㴵UUV6=Q;7W[u L.nމQN*<l!S)vYDD1|0J&`π2_DU#Mܨ"/BhX]sv;嬂3P82c@"+yP1e$?3.<! ιA<Ȇ.bK,C %̹uVFWvSC[o⍫=&eH؋~Wű 6!BPߗfzV*КCæz{g 7kOau%BzBx_{j%k`OO9D$pfovcہo(<Vu+H" ~aI-=RLpr&9x%BDmB(`6.Gt$V%ccR\ ܛ"߅@6l˨ h`?S fSm ƣo%30Үk1@}VeׁRak5:[v `۰s)"<و*_Ϥ:~_i=zԷ.o{*fn%IF}?golELZR<LцBBGhUY=qɳ1dodJ%7CHZ*~&%]_%"~nd$%<.cG}bMn=0\YK3 q`ޔb3I  XIGю2'uBBc_JxZ5U9u-`p VGB J5k|Wx):!Mـp|A l7R[$cϵ{Q"ߛyB1V!zY 26JWJ8IX[]DG_d\T0L8g>8)!L ԶZ kG AjBr+ã6%.StMQG+58[ &\1'ħ5>$}< clA:Ɏi+8Qa3lA C0I[-d%i 8@ 0l&Zpu>QN&}XhB9+h T񐠈>6)fx BZrʹ;ͣ`޻ʸ&2AȻwBxRPf5~:vo[FRee>!lt3,X[1V!.6a7<#my -ޙ+EYlPŜ塴^v#<[60\\x\]zwV[gLxN&+ƷH!Z6.!) M4Pީ(+kSIP\x!!M/qsd#L!̈́Kn~ypv:BpM/ҭv`Nlwcs K9PAz9!͡/B,Rʀ:P_y-p^K0۪,!8k:ߑn:|ɷ zX3%퐀}o!Mh i^ |EH3`Ҏ"q'^ U!B0WOmb=%|7HIr+~=t@><V[P\iA QY !^J!7>Ҵ,gB+ yjs!> 1w>r+_T&چeP`i+W;.I#͒!)*é+_${ȫ}5`+)]F#4gw ta_B}78HD!$ x{fy>52 mRq9媜qaX 7fz'b1µ}Lޞ)b.ܺ,$}eJ>2yDvRbq&6)ek߉Qv%;<\Rz nmS Nv\Eǧ$[?!}\ AysbYb r@ҽ_3Q0EYUL&&<yZp9@) >CLu U4!YϋRXA/cDy~QޚkHB؆y\:B _jDBhj=a3qyJE7>?呍 g`W̥<_)r#>Q&^WIS#e{SDS`,n,=!/9Y$YrP59l4B!=K"$"Jq݁ 0n[myUBǦ1/I q?b3&* 9<_*}3{®-+Ls헸q8NvxKӉn^z_m۹@6IGcY?[}DQy%N@8:S㻈H!Pkzq3V<E q^̍jٚ !]DD$p4ٽ;+KGM,>.EDD `~J`yEDDB0saZGo_u&Şl sx9&L跈jC (IENDB`mopidy-0.17.0/docs/_static/mpd-client-gmpc.png000066400000000000000000005350761224420023200211530ustar00rootroot00000000000000PNG  IHDR5G IDATxwU8甙mK:iR)D,(TT^(B ٔ^ov$|9s g<#(((gb!I꺎Zt EQEQEmm͂Ki۶c;2P;|oH2~Bgڊ((!$ s@q*e|'e&'s[:Gx)%H)F;+y 5Cǒ;I舻+>cTͥ!@ҵ( Ѐ96|HEQEQ/^Ġiı%BC>/hg?$cRJ@bẆH4>$11zƚss@~n`7nq)·߰A{uSO~^uw־3.gAp7OI5 Nk6?YaCx7c1HZlL4nEMvY74i[Y!2 a wa(Ҿ'·.uNc,~wߓzQEQ ƸuǎR@!4{4/ĈqܾcwKk`bpEukZ=qB%@a1BbXOBp8L qB19Rp!\zt)B"7K'g ރ z<xV@pd0b=Oİ]tZGk v-ؓ œh{sO$t5q ,ee(Dүzc[EߺRбuTRJW#OUo[nk5~ou^'^B9};?{h iCL+_lBjH -}TCBfӋyvz[`ftz1w͎f:trogtDZ 5((]kk농&T !s=oN0p̞R]{;ƶmJH_fLi1;GMs媵9Y`0X GC s*U88륄`Pp!A15f]zx_g[ؙݻ{ m]͟?a"-H-4ǟ] 4^RL@d/!a ԓB&Wo9fO35~hͽLEc*R'dxQJ/ }yY! $% |坉YgMu[{}\yE7m$f51m7IuSgK'L׷X|k/$$txۺ1WWI6\%&^W:Α{cx@6jb+ف:ťEҹU{6nj2=Uӧ&{N##YKo|I@`$ 7شdXWL2:E2wCaIL4*u`pUu̸hNv݇C.*]GEl;+Txʹ㲎*(|b efd^)3V]ʤ*BО,R!eK[[}]}SskIqzP]]CzZjVfVFFz$@q:30?;33%9iޜ#P<#GF̶LP{Vop#yVm=֥K2ݟYb׽߿mg͒.YӤFaX>6zͯ]x7<_AO3=㦮K9sB$Hڦѱ9/Kޭ)qNmŢ7ϒo=4$,k8ښC1AON=KG];ټl%חϾ׾G k@an1tR%w:T_cO.tfmY־.i _ akZ{W(.:&5MjZnnv$G}w;*&e%ǻ1LFDytH,?}o sz_յCVҴ_lAre/n̸v ׿v]ϊK$ܭ[5W?₢_etr+=I2o [5o-{U=3,n?\aR>BڡnYLɧpscOMKUuBcYtݎ:Wxt݇v\WA.G^e~\5hQ fi,E;$w7otٟ]1goRe1!%*|_{{O|(*vŕN)N9@]r/POOߞ}`O5l=&@; )P__:n|((ljɉ<!%&(9D(H)t(a$ +8&.JJ 27=,[7 l4*DC2 pRT=SQQAJ Jov7EX&FͼBM,yX-=p#'0-!mO- pKKQFgkb*X/aDOzua>Sp7s%WN}󅇾xdW~ٖIsrPf{z Klues>3}~%\PvW^81I#ĥVeټa"i @@ؓ;-7>>q-]ï>WwYWWTgT.%I!iizEI>ko& i $w2Zv3W(/I+Zk.h7l)κ/_7aO7lڇ娌M}XKօh>/e ݋B`(j>t] 0wy {o~`cc'&1)Ge[,D cs^x e\wE_LI${z"Yk-;RpMPBRo:HyߏfUovA"gJ}Q;7LLc]2a[4H@$<>JEQEQ>tDcc &actCJxrs\eY>H ujgRt |&`Rc )c$PPZTp̼hOr*rTRI]ֲM $:SK >wIœVޯ(uӽWU'ڴwZ.AJcLH "9?H乳s7$hqcD7fV}Ii~z y4G ۿfy_`}[뛛6=,=c&\Rq\0JLi} F! _]m߱rW1ƅ3w sT;̾X3ƃI]o?ʀ&gΙ]!mR0Ƥ,/iw6J9syƼo˺w.X@hw: s׍;z}g{o1og$]Y \JvEQEQ300dBpAaql΅mŎCq0$PJ8cq:d@ a<~|eG{k_ RcDW,3;1eC^JO<|uOAqO^qakO,uidL1kUC3|ʏv_U2)34bֆְdf8qYPN!YLqzLauۈPbfa^ݭm!Rs R/hf.1X=;2=FqcPb#RsҤEƶ&Ƀf^a~ll1L'(װ{4qRY}Csw5RKJeo{{{%ѽD:󀛚7&/)jZ*OFQEQaiR)13v1&crOܧ%n4^u͝XH joWZ؃NjĢ{| ?=bK RZ:3őwʹzmLH)pєg]8J (UEQEQch*C9urٛS#LF1gΩIqꔛ;!PM3e]QEQE9hw|ǰ`YAK8SEQEQKDM6-˶⒲*q1*((rf٨rXaaAB +((GD|uaH!1{"t/|= 7;1AG|CN2pOt~ıAw̞y8 Q >Ÿ<5#B2c~r|>K*EQEX ~/VeWw HHd6,]&Q²qk wDhٷ} j CfN~LdÚe :ɮe/_]1#E-_~"Iu0-؛VeH!!ֺZ+[gk H4l[rӎ0N* H~l+mI(N"fQaV0+T=PoǮ;%iډ {hnQΈmk׮ٴc׾>'鱏#^vH `7~k9$nB宧9e^~\k)(| ]6o|]=.^~NMEc놡^t×g&La膮EZR] w&ZY,yni'd@0tB4]7 ` :ڵz=:Fj:ah:pFD"6{kz.6i뺡 @PM4Їšv=Qj (-ؽ|^A#g5{!)g;//Ln膮QQ iNq|O'$a }VD1tM7 iݶ>yj5xyhNO@o}_ձc#:$e. NGmm@10~'۩1m((D.F?{IUd+~mswמ:uu7{/'t̓dz_{qю Wqy=X.Ҹ%T"Q%+d|=;qLdmG/O;Nz{_9#e}S.F\z/FW^==G'jr9up'\0cQu;%KْfWǵ-S5uϦs6շ|ǶWlzb+/Ϟå4P27ѯnwÅtD.4}ۊ'vx~"ֻWmvA/3nu7VQB3nzw>gɒ8p!A {xz!}~w/.y;w}~\Bݺ-X|t @U_i<r)BBQKDk{q} k3;;e5w~+F_Qw;;{{; I{Kǟf֊j hGUCgO//GQEQ߅]p>vfkޯ.KG:Lxsv飻gM;gޤPq%o߻{A \{B]]eSR4 !G(I s@QKU%]3-WGnYYKv610!74}_CʄRx3g̛>ȑw)ERJg{iΒ5ZfI^AJeQեwtyn̚t#u_|h=v:r}&$Ouي((VF0'гhaWu)\Wcڼ;\ IDAT{x}`?"&YĖ@0KHj)oD?#!$Eu$@5J`9HZր.%@r]x=_S8lĉI_z>N0fz@kf,+j6O>n{ʨ-!, @B ڙR uRJ)\Ywst[˜Q3nߗ &]2)eûei`$D hݮncrF#ٶZER . fBH@hCxYP\@: #6H۳}_n=;.L$=c-+XKfC Lљ+0Q4b6JL.,@+@yw.¶wÛV \! k+JEQEQ3c|f_xf%ƭu8myC=\e?J/;or {ύ9V4k:j4lZuGHUٮ, ,_q_MVLΦNh#%6+66XùsO>I"ю=K6liJK/9U:#=H9׊* (8gj"i즿/x`CixXOT__L_6l +r7h]]ok{VZo}{wFQr/U*^~>3]Yv|"aò޳lG(13#ѯ"zfWݺ _]F>)}#^֎otfhgi"~ehP{Wwg:ghGz1"xb޵_Tr1:A ?Tco#Wu7.ќ/\j>д׏.7o[õ\s{ſ>M{e%:P5Dd̤@y3BRBFywendNa8ezm4AGOL_{(;{|~ 27Gb?fe߻h!PW`̾[3 ^~{|Kq|RܟY~0M:kSbIS].g~g{f~ W\χ.dM8+&i`Dm]OEpr7ݘ#H^uW_zzTL?3 r>*̺)3j"o.|? K_٤yOD5AHyL[zRQEQW|!CebC+h\醡2ƚQ R2ױ]AtC N1Hږ i%@ n@ -Ilx nǣ$̘C=:m!APM74R8r 3>;u=.k;L'5@rf.PMGtB 1=b6OFH:˄p:ŒFz=wLB54mD "ǛǧkZ.`!e4DT4 )9smQMգ{$ܱmKD4a$̘c;2>9NH׉C ,M ǴGlxbDX7 əm;=:Hp׶]!HbF0߬ㇽt$gq@sׇ]W1ӕ $cbWcĵREQE9c>ٟp.c{hfC m̔e:tb";еbGn ƢXȬmÊb 9cGra'w#Ux5a29)1fGk^'w"(R8i:N,*Ē̉ 0s`6v{N"fdh~LH`Vlx#҄9t٧0mcn 8aMvc~'܌TEQEQ>jۋvKP}ܼT[)(G VUgxQV(|Ѻ3]EQEQEQNS~((()Pι )W59%- hpC"<)(((7Q_JJc !8cBpƹ2E9gqΥsy|%1׶>B‚tf\QEQEQ|}i8ljFG5tCq0zǠ̱11%DJ ʙ>=!:<wɍ]QEQq]׶,1*8 \[eأd?H8zQm[hϔ4(us[HwN]vcRP[{ช۷I II mQ3jEQEQq%0!G&KHlHir#MJC?1M{Sƿ89hoJ!:: 5" !%]B R.dfB|>_zJRr/-\J)_z QJy#N?;Q`EQEQuݔ4G) . )HY\\f_Nv`V{?!o8cn?Kq^a,)t/C$hzӴĠq KIi$xu:LRS9?bɣX~YZ.Y5|K^/ !GN1+bU?'?׌ӏ{v]5nIJs׍YPqEQEQNsP{uNH0.diEe,a zܥB )$B1^_Wt R׍lR_D" |wƘ㸖猳˻:?ݛdH82Ӵ$gQ} K LJdq@ХNc$`@ G<@4,ie3H#bF1H!خ=zwktر'pZo AR+۷tshޥs$ahH)-ظX'#5AM7 h g$ )1-F a)ewEQEQNUo}_3SksؒD!t `Gh=Kpls&8#;ic |!"(H)tw9g.`u6orHFB1SKpKOń["D~BF\J&FdT冎ʳ⵳x{/uMOWܿ'eM>.*˽.3û汻쪻=U~X{8d:TOc]Ug|_\95r*n3jݦ:)pLm+*,>Ot;gTm.e޿=z2kZa9mL:<(1Y05ٮ[pc;ٰ{e7VO?ܩn=oN J#/mScċ/soy=Ǧ Zy 7$zÿXM]^׺VrAQ(٤Wlޔ`_ fM5Ѩb y^ --gǝ1|xAEGOy_DZt_=mꚦw1cq]H8nG[3.]ӫO@ 2{pgӉ1AJ%ªE9X h+ &iuYs7.~KdBd\'%-(wQN@f:ci=Ҽg32dm˻uWz !rgogٱ»lWMhw ((TM3r.#X8j$z~ug>qgIBMҴŕ-~=c((g {?BA ēx<P8g$p@厮$hXe,[ȁA]L˒w42xX,ʋOٶ}$pw]ױ2vutDbѮ1#{y4a$Q)APwT %Kݓأ! 2TfBFl"]Տ9IA{̈9,UkS޾摇VJ K{6SV?==[hMw\p880n6to5y=ݳOQEQ>Gc[MK81/?WJ1w!p4ʘҡc~YaL8%)1D;#7Jw>kٌ1.āUU=w\y4l:I c)?&a[hs詞Bh,"IJ)u]q\.\ܶv61]iy`1Љ("iw>A-. GC3%u޷ƎL@Fo`GAaTIZ;_; f&#Lkׅ(O,[W4 :]ntwxىADChR#8gE lulouEQEQ(%Ŝ$RJG>Bmݹ9+J`wAqIB7 FY3i Itؿo7x #ѰekWXv?L7xUBUc=#<3+9'tGB~$poR!aę{_cY1A`|AJ/F4"%p A#C)@5\<hs, ME[,(CQWw,\{%͙;nŢhz)}tokW ;jy RΛY^ZϹmH5{ Rf$y1BGΟ?yg}W SιrNݎ4OfN$XYsyy)g7&s,>g`jEd?yVz FbaA}DEQEH##RS'MdIk|\m8 EgΞ~)AJĺBgYn)ʒ{'T6cVl؋i:.?y,¨bn@J0A!4c̺Nu]RS|:GN39g 斌jlnBm$1]Ugx|.F$p&@ Tе̴y_ތϞ0<1`;81~Kܼ}6)8߃@ c4~FAq!%$%7|K1us3]q R2|÷@r埚H9sM)E+IakFIOJ-_p׉N:7LH a/ɹEj̯wH)\REQEn$FGLd@bfK{gaIi@/O5YJ`9 WbfضeQBCMi[mYR*Y%dάmmmjBxά f|ЄH$z$1cqyn]޳ͨ@ "Ѱ$s߃ K"n Ą/^{0?{x 0XVk_q(-&;uG^-wRYw9wğ/ƏƝ#;rgJϊ((8uth6u4&x̌ )zi3gq"0 yA% B8{:v!4-"t)M˶),:cxIS&>\k׮)SB}G4yJC}öR-@C9=Sx pR:sHqxrƕf4,65fddYHBr"0`G9 QobryZzs8wcH$>,8mSEQEQ,fҘmqVXXFBB)'d Ӫ˲,"4-˲Ir\۲x؜eYNRJrffضmq>x#{4`ҊqWfTH c=̌bD#Ƙ`B 03N0T\>ah}ԣcrN#B36M"dx=6Ludue2$"Ae?ST39BSEQۖL47EMXc-M}1+uܓI3O/i; Kˎَ19eǿ8mQJP,4<#Hr:wz=='&Hq ,D2.)UMܴvr@jOPa B!.=RHb(F &7:9tN{')E|1 D06xli˗?s~17߸i!<2FS?n )%cg) 40)[#5\`BB(샿)aJ7aw.x3/}_͟Ă#LHZRr0A͛z݃Z 2GMS;N~4qsjsHB 1D8~gؾ>1x$c@  P 9NPt]sآw“ܿ_? tb"1 u+ &c-w_/_ ^,w-!2^o"e }(p cC-hv=oUlv\$ںYxncʚ_{v軮K-ĞH\!EjÎeO-#-R3}o9S_|Y^}tʱc/?nި!Br!r#LutpHӴcGkh:n$I?bÇP߈u@dQ8秳iYdNh@JM8TjiiSQC* Ʈvyꠦٿtw C1 IDAT8@0N !]aL@-"Auɞxsc=؆{k0tp@A(CD($%]CIe`qT4{gjtEan~լ7|g W-umƬ:qw^/ZtMW{^߶sW{s5>F#S ψz/  PoϼrS{usFԯ{zʖD|M7ήR쭹XjۛWiλzEyQMC.XiWq@m'^U.~ڌw}=ߊ5}w.*ҙ-hxɕӮ.TJC9C9#QiQXD2TWtòPMUEJׅ'? C3a'9۶ "ȏ Lؾ^b0$Ϫ2rH]8; `k#a((/=eg'>JwDuZŊXw{gl֭<%? R@AR 5b,AcLXܲ,p̘&YQeܐ7MX X2_ ("1oeAՄtuyW}lݯ1ck[kҺ9 ZgK??!έy}ylߴW~sѻ$BZT0l%SI@@L EH%%@05h*hD)"ǎR{79e%'IJy$2LsܙO>@D;j RD%ev'Lo!jT{m.@DDpep$xD7SPy`Рe6]OYJ8H@b TQQ$'Ja0|PVʈ0ȡ6"k UOR$K-sŽK_)J.Z`AaQ7WE㼽LqѬHRDgQʋn]8ȴ䕔]{2VGs17 ^;D$2ADH:M+ECA*Γolq# EoZ3UBD+5QSuj( #iE,T,~Gw‚!3,<$T2P [z˴RRW ]T 7oAL&0dJ-b'YWhĪ#λh#:0`N-td#ζ/htapj4֦T͔SD-! =}ϡ=-gDUpB003`Y6,~y_ CM!TPޣSJTX~:S(Ig^!r!.~&VDфx?pwu@̯<+904,=/qʦUա2nt ל7@:IT=yciN?w{&iٻ°{?|sڨaWzaY=;hϿIk&} e~yV_> >UUYkY'̐Ǐq6"9hcDC= Ç \WuHe%FU^^fqEA5eL:TD^@baEa Ν;M.+*+UU8T7ygǝg]K{=?RF@<ߍ_󩙥شuZ0yi\@2M(s ]776L<9P*xˇ NhtI2#$a! H$U[R%""`JDD[DK%#yaXz.>0{66:C=k)20%aJ71oX=EŖ_tiQjLv`H=}HVWgw$IQ@ADMYXE(% =nHuTX,Lh꾯Oֵ}uf3M,4)?;QROS CEyဪJIʾ)c& x~]a0k98x##;Ģ6UZh"X󗗏} R9,j7Z& s.s.G[6Qr!D@XxRcG+jjwnܙ?&}𵿴`8Ey`D`fwKuoW4rB>1H XG=.d(P-"I7|!x&  8v_15y_׍M`pa=[o޵j58:;!ps$IIhX@97OZL1N㏄"!nKZ8(2T~raVo?Md~U4^n ]qs.<Ԓ]$!B!KB+[cQo<kϻ|y'1N7̯0M}Ê7:_}Uͯ??$v*)uCy!rS,Όޭc,~ͷƚg$X;nףvg-RaR5 2ֱICΟ?6,=vH@2R>8p]fL 5)Q:+ϳ 6t׿ua~7/[m:Z8f{7p$ga J|sْǏkinjYǎ<:{C"Q"dH CDR$c1)ym__c}Z#2)wSBhirA2nX2aHԀe&'DIQ'MLɉI`&*Z; .;L8/'A97rrB}: }t:LӲO.g IXa@Pn B&AuCծ)i^ωE-nTɾ2AU! n&)"!7uƀRS4?"a;-!j[20 j67'뚱j*aw吤(, 09$+,i'{jBXa @EUeعηrC9pm[v$ j۵_1/sZ2?Vw.Z:I"`z~o}uHC'`~YEIH!@q׫m|Y7?]|w Vx0mX@:>0mCΛn䊓}n3ONkFz*aI diX,2c$GBpS7tBQeUbC@,4d{`XgDxODYQ[n &++.9g |)'aeŒ1 |I|~Y+$Y &eP4ȴ̥N`()ޟ(AI1$d7a?f(9ml:bQrO~5 GV~f^·Xs!r k{ʩq |YQ'cIAU2+X}+/,O# .0,#B*D[ R .]HЉ =SesJ=R??]mfyea[( mn TUxw<8y<h0~Kd@ @$@@i)40TC@^8*+,˜[lRY"2c,U>/VS,8~30p<o`iHVNRr!r8hF|y/mya8Gesͽ-O<5|߯`_j߲%i%W~6} PK|e##_P! !2YSC;x_n>*Rz࢛?S_O?ە'g|jd 2PگH8_6|sْخ;!H)}'yJS29C9C9) o]eݿAĬ" ]]/!r!r!O4^-(7-9׽!r!r!!Kîmg 'Ln9C9C9> }1doH Yssciiy:?b"ळ&c=U`述+B@5^M @S-Gm?*ٲgGx94q3)2lmi.)){?~w~Φ.+Ngߕ.#'9gưo~d)X˓w.=@@:VԎ(/=>|%>B=Olu{~*~S(cz]{NA*ϰ}֗-|YoSVXT|Fz1b-H %>[I,5:{$f6W@Doi.IR21j 2Ɛen|"!ٽٛ7  " Ǘ b5^ O|"{NdS޾Gh_f9\6iEcqFc]j@&\e=z=$"fVtFȀ __W:L,ι''JH#3:sK{d^O 0=LY%L$Ѝx1)Jh詑Ǥq $-?-iʱiqϨڥsOmulKZy}f=Tҵ cA?H۽,DYUwWO)$WHk2EDWUɌ =WLfdbцGis`/ڝ;yxD@D>>{K#"+MY}76t {nic3i[?w$&q6|iigvf H0W`lBd`VDg1ˬ>2mȜaBo~J_Y–f*^+'QLGtxn| M%8#!1d0dpaC=@H`jF@,iAnicЎ["bY'z!+Q6w9\ۚZ1iQ93r_Dh[Y;N.㵵 uS$wU?;unU]g; >ۥKZLwwqo4n kN=틡]g@>nq. Ӵqk3TeKN{G'`z,=5ϰ`Ob8_O +!۸MHGm"9LGy,"p2M`tMw6!\m\grKXy04!<AF,)q@"kիU4 M?_웎}M??*B_严fQIH sd؅h%dXoT1Hٺ1 SnBYeYfn϶#3@ێ3 A' cr#sAp+$Lqn f-GH7Xw4l eU]N8fJ6 #疽XSF(HV4EX).0-(40t.l㤍\dk@x_rM-c;19cKGͱ+V"]N2}G vBH7 F_Nϑ]15gZ^Tə F |^q`Ls$et?`8'rre뇃#&D\WZ@}DO;eG2= Nʌ;zt LOHyMfP⾂i_fT7S*.L-Ss.̣ iu9L]%9B8⤴vy4wdJ0NД;i&6/HQj1Ðf7,UVqNI;*t$= ݚb{53e~FH.xz{fҒa:3㊩mc/2Č[=Si IDAT<4A^T#|9]~~Li%xZpKrnPMO)$n:8=&W=m"B!pC/"7b@2SPUUn lfsqxcmM!AhӚ5)a| Ƀ1L8)zw j-n!2-1OI0xCD}bӛR_rZ: rU 1[-ne@,KK{~,tBE^=^N\ #޼ӟ\0uoceR|Ҳ1t_*2Sye=~3iÜy3i t^z | Y[$$˲lzp"&5E:3MU[h֫}芻fTt8 |B]~>fh_9a%X-O~n,7fz?C3K~>gźy \v=1[et/ D;Ӛasp;n!_@Ϥo:GgioK&@FЫt5'5.[!K&NO|0YOl@x@k=;{^n2ci 7,B3,xUyцEܲEQ&8wy1d(ow•IUeaȊ&'n1]Ο^nuBp-!-%aY%;Dn\2#Kor,#4Y2j +,!"9 '1iMt:~5sL9se 㝡U>fu_2z)_+3]{^gfG gA sh-`\?qO޿d7ENk@'˸3֮|hQB%)M =jAEaxO{c*TS3!7d0^oW#2f0s=~Ĺ e#8̅]y}s~ ,$-!_yN*e"2v,#$v/.q+SJ}.d3Й4)H>)x 3}cPSԽ"9X{leָw?[|^R*Y'n'WBDF$lo$! NȘE`[ 0zۡA/b8{2'Nb?ʞ0/wL,Neo3lO2 ²^ByA!0ZT1uE,SNt;Z g_$!1$ ${~Ƙoas'BD^_& ƋI[3$&O۫yeXA)7p/T_uvLjGHnւ^w"2 ' L*2z\Xjx%65tb/_82";lgDW'};C(1 kAq_Ӛ!}+#xIu޽s5nZ+2G_$߯VߔVLuي > zS)wENQO@@a%_TWaxʰ,n& "!E& ēf?\XX(/<0޽7ٿ7k} sƽgTIp=sCL$b$`HI;Ov?4qᖃ:Kfq%$P$Dমl%R0YQ[? 3@ALXFJPRԀ* nJ'Sfg9# Dg9m)\Ro;L6=\1%yÛYȀjiyU.RQf6-_f\0@p!(#/<pK D BAۨ9c ]{KƭmF2#s/ ܲ8׿`[li(Z0 N4o>96Ǘ';zM><2j)+WE\Qdw+r"I_Po!f|#.ݱyeOH|<8q3M^aM?y|Sًa72?S> ub;ƴ {N /v-S]G^,{ќhbo[8|ǁ}\C>ݹMY ;e|wV:p9~3wK',ضݮA[/O6C,~m59@;huKSk ;[ÏDqؾ-g:PpJ6Z>NC&͚:5`[INLKD!r/%ݶgѝ7iS ۺn|W~f~QЀq3*O u \FNLj0"{W|xciTs}3 S;jK!ݟ-*8҆?QÙ[Jdܫ}-"z,ݙ'^+&xtc^{D]Wn=wԞ[kf\y׵5f^^ޚ?|rMȍ_i}{ D'6lmN+pn:Ƙ$֮iw-W.䝝•۸tk{lq00Âf'1'\YZdT8ʞ8piTbL!"$u޳%:ᕣ#54-KQ8g;\qs 6/ Xq}@{ ȥ]$vMP`zیBYc?%N\3!9+uk]>s B}m4go+ИIcIwŵR2{21`.2NJ臛chv^=TGe2NH9Jxh;[PyoV0A2h4w͹f-//ݼ~H4<13n8 g'/!5,9ɨ)SZC~iۅ u]Sg'Q8'\4)ZkWve]h5LwW~p1a<9-EgD?1y瞢nTʲOte×zD}cAaިC5MXSiw"CscnNnZ ɜm|{MB:Ec? ^T`Zcȩ i#ZلorſuMgtL/=zd"-:7]}Y0f7_"koqh'|ӛ?B``Y:;elчvU'yIBaNɦWvu,9ܛ@ ۞ DDB1@dL ::<R=^V]bƖMZQ5AZQJuʃo?u94pIF e *FW J ^ܒ\6`Zu=UK6vLY( x*']2a`^oG'$ᨮ@t6eUR_kwEg}sT+gmҢ%澸% Z"ŪF-]C/z  iQ,!|Fk64$f/Ζ>rzHRl':FOza3HK6l؛(0[h`YMYV%4[ LTႤ؎?򥼾\ >yZ lt<*Ƒi]CW=j-.?k|Y:o?sP佫rCD|!J2eBe)Ny;o/5Uߞ]PeW ]{]WhjݝG#Ҩy{[m)]|L{Ru]=e>И$+Z 6fc{v.NvԚp*kCϨ Ұc*Vdn~eϔT!LOAeIavd 5@MSP7 >vZ۠^  {Pn j.-w^^fl}CK&w9 w- 4i٣FZx( FUI .cX m^lW̭kvlbGslN.&rZsFQ>0Xǁ#QGу6̺һKqIF$4mT$Ie\pEΆW3:_F˷2e0+^O*X>|Qh7ef@-߳i#* ,t(BF4&Hp-1M@5Yђgf16apoXF8uH #}02fd:]YTb NEPH#JF~Ρzy"Y6|]7BYWFP$1TY0\q(E7+ z+hө|_ eqFS>pt/ήI Xͻ=O?͎x#= E&-D̖Y~w` c铲d#%oEy[ PV7tyڿV.P7i˶, n,-0Yɋ8xfx".zۑ=s H D11(>y{~Cwub#U㦜7w}1 ږc9Uaasoo#|S! =+IQ8ZP5dhsc?c1e]di$錑~0Kxg7 ([N/~H,}kk9gzqXՈգ{eڧWlrcvL@K_?%o.{ګ^O5hƢCjz7\z2j=bB F҄"L'|{}޲q*b}P5:e>4+_2_Y2tG޻v s Bx"CYuϼ)~HLDd>B 8˯QR:"h~ _10|d|W ySJ !Ha%$d`S%ҧ8>;I2CDB->do%.)cDd`[ s2g'tS^O՜Iv+[1{y>MƹPL&zVih;vְ^#IA~iLbs" K$>gn#Ĺ\`vvCL.Wsx`T TT)).?PM HTW[%InWDOs+_7z@v?&PAŐX25 ~kuO_~$~%m[&4r['EǍ,m~, C^zwyWRYPn1hܧJ@ ~G9s:G͆bTF XcJ[OAk"33 tV^p>Oj߻tiJ%uMatH|p)[a`!۹*qdC S@T5s7  9{7,+ I:#R7؇=ڬoL3X[;eBVZL1fv_z}pe~3:q9{NH/\xal*y)U7D XKc-%/ /,''͑JIsfOJ"}&g5Ө4"O'~aLy X]i}Gj#kVwVhG EQUkT}#H%^.I, ]eI%Ņ}u%^OkrȐq< ?ܞvⅷ:.$~;d? UrIc_VsS ߕVT~2_x˃%¯Q.ᵟMAVYտ=ªZH?gs*"۷FSx*!d555z{>`K Ԧdav ΋+K4$=A*Fk\FP͍mjqyi]w_R 4PI~HCʯʓM=TVUZYayDEfGk[QR 7w?R躮*"b g}*JԎ^ZA,dssg JJC ##%Ɲ@Ė暚T* jaiXe=1CfWO"ݍM@T: PKKC20ZZE EEv\˯"ݻGawhgX Hg{1bĩIͅ} 6\"O,<8xUK/<-9@CF?d,Kp#[C9C9'r~Ϲ'h%{ߣˆzN:C9C9'rI~Y"o#nwuxY; nZ Rw/KB)ZEw  H\r6BH(m vvgFy;7n(VX… oh_>л΁V{|Ǎǝ@νfYj;i:gm9?|nb6lv.(g,)Ř->.f8A_smOT;jOw .'TŔuܣ2Kwvim4mݺu-*`mÝbϤ&3.Yf-[GiqY+0~̩?d٫ۈ$ge= ^<^?)<]!ۉ_O(ĨPX}*?}+vJ:g-5i9k2@aJkӶM˦{xꆔjкMV-ޗ OvNleesoѲeƍZ"oCѭZM z[V/ܓ gM[hbs ͛{R۶iժUv<1v,u#ӫܜXѴUV-[w⟍. oׂSٺYZF0auP䭧d"g_`=1!oIB,gffB4_"/l_r;jæE^S]ZL[O|Mpk+)?e~$mcLý>mYŁ;UP~}6OZ[Ϥڕ:0u3XlYC=%Ya e6E|}I-uE*zZa_Jֻ^Dƃy&0{\?qv᪯^D'v-7gi:*Ұ.(~<"h^d3tj.fjWz7ek:JY>q'\3R%Mӿs 6ӦU'cV'}5l~3+ܦe~Y"Ö^EY<\0c`"Y3Ǧ}yzlWlCg=ڱwWe(Z3-(8V*u^uh>p˧պ.ɸ_rdn!Jľ?\72fn3V}/5|Նc6=EuIg񵐵|r yo2>5'l[  kU9W8= 8бzM/г:Xa,>_wf8 7/w`Vf+M}њ*ݬ{.;U;^c/r 4bʠg l'fK )k_ǬZ#Fڴ4O~P.jRpƶ_BWQ^^ZO{cNd9rd.][lw %;B Բ*-Sʤޡ~ZEFoVd^~J|~F3,Bj$CJ%U-m[a[˞OWlx+7nZZSBۼimElI8xdSǭذveK\aۢR;K&|Ykg$ /3O{+ =3vɪՒ?o71y_H<~⍳'!W 7+IkWOk8mσi觱/[ν "^PzVPEhgmH%Xt]`pPL+b7׵*U\ˁKE*`? I ]DB&JmP*8VHLUD*G(:EN~ԭcվZ8J,aar3DM]Ga}ZixU"&.X2e1ExɕߟrXiSݧk=yзr>1qoߕ\kև׮"qu:~dY+2Mzj &Rv{T9"K߼#isH&h!vq%`d凰]W>,&$PEѧm|}5_''DO NP; /xuTe"ހ("|tO?>&zG Q8fѠYGFܻaɜ=eT'E쭴OM;e:_@GQ}ƒ HWş|&7[Uz\i%c{4Z߂U{aݰ :wUWSItݼYeY0b]}}Bλl}0B@_MolT%{+G/1;WI,2SiqMo=,T2,dY>L/ޒ:TG9=S( ԉD~ 17L~ڞ'n=V M 8\aAgT;0ww|R!ӽH)@^ZWr-Vӻ/-4x,ɴ»/] g@ڴqQ &kIl׸:ܺ~\Zk^|Z-<eݏmOe]9oAXTv%Z|ۭ#%"jDb)oS@!01^=]*]s`:&~/P2L6x@&2AƢ$B2E5'EQXih/_NzrN, "LT[GOIIs!ֹm )8r,6w$!Ue̬hŚlU_z*5Ƴi]zIMG*8fYVAÛH'M &*߷ͭNGQ#ALDJ JWj(+y`E2[s8ԜČxwxPdӿ ػ\/7ϲZ]a¬oviܐe xX$ XR2WqܬgM 'd>*VPDfޡXI< RPՒ×^X4GM_ާ{]$s]zjשĸ{ rJJIe嚸˥i1usTsNtQYL@ɥk}XI]ϰu_[ҟsoDЄ,ǓJӿgRrM4Vy9PoO"v!\w/?X'{XZjl~F,w.us Nſ(m~,K.䠥s+ a1tUL2@yuCc66*O?d9hp:A^T_RJcq srV&*^!X ^^8 &'Z F@ K6+?bnbI>fOv/pK)>iϻT'rgO\IfKJA7F c'J~xzƟ^Kě{ٸM_y!n>|yeYf5ƴ[?\9C&]HH1!}1P1XC} #@_>Wh߼qha:j0&,c,PX&@ќQ() 0 heߓU "˲L(&90wAp:.ySETd\rCMf"2Ƅ`Ye ;VeIvnDeY&/]!nw Jg+P'%d,% <gúw]p!& EaIs ٢u}ܴ믿8S.@,hY/i (ofTc SL)DwWjD¯ Oe&u-AS **PxX_>O%K RHe F㙦ʭU)\0PJO-v]X (sUG_`e & H6*dR$8NJ1R&= ƒm]JԤ__RQH2ε N,_om߬zM:y5&9װC=ǯ#>#" Y0h))8P\rSٺb]R~ _p| (G_[PȽ/9EYt\Z;.A$vlphXEIԫ' -rB?5YSk}tF™K?MɂeX1!DeLPxX'8Yrw_Q{p 7*~#MD)m¥|"" {++>~!c,ˀ$|o>e,|M_W69SeE Dr^!=P_/Di>#JVS~je aYeY,/b:{F 8o~ӫ'D(8AAهNcIeL@~Y?R)‹NݰA|*;ZGXBʜBQe6L,l.匈B`@O:V:dsPr8B"taLS2(M:tıu*V"k>yM]< ,YϬ&"fU飲٢n.ظXzQ"+:cPwo Rf-<SRR q.wjhvp\a7'TROO/ֵgc#6Vl;ۓmC睺wdjzu5]=zSbǗޫ}CnE[)uZl#]YK5Xe,(woʊ2q9 _8az|rMZ/8 Հe)pa E*PiAa+n߃*T/_2Z士f'/ ǡq {c)5=7;XYVLXuvBmڄGky$ҍ&ȕI/_w0^txOuB0M4.PJ٠-?Ge@Jg0h8 BFjSffqԨL^ZΚ}4 ST F/4'g~A" v/1{Dthd)3%B:3ӳ"AZ(K4; IDATfnGz_5Æ)Ny55$q@\,Z t`6_bkF78Ғ)im3*eMy(ZWQD1UoY Bfr3[O C\[C4Qk`1+#.Ȉfu^cymBִL;9ɤb@gefehd3+#9ѻ,Oq*Ѡ3-!#{BUZ$''Fe)Ry jHL#HiN cwdWVZMFɤf~B܊1l6W9BZ/Hs(&@tC Viz9+=ˈQLFrddgdd"حVYѤf)nY))Ӭ[CA#tOcK,ĬT;V"6x##3Va4yđ$i|δ$^kPzpp46(D1/NIғja55ӎ)NK "gR#"YL*KLOu~o}q'/]yr^{zj(ɖrDh6GN_D @Ro/؈Мhxs*wۓk!&`r{@ AH T* @$8XȹB[GBV Çs;*… ?{ytuUסz߀\F?g$Ɏrc#_3T|S%qF+v*S/5N2*_.Dsfo.g yu)}b پF9 'GbFCjn(I'қ)f}iN2ɩdU|sU-2d#~Qy7349|Qhs9Rj)Vm5 Jo2羱s?RoRm<ڗg߯$g ;KAkrbf߼w~aTz?ի*4z_M_)~JIS*s1D ׏1Jk h.'@ } y^:W/qxi^}x myhǾXcY>\V7d'w9I:0f4b\idQT_ - WQ"%>y[MXRBc?x  ,Xb *ϟ=x`0x١hc|ܾD-yS=@ݻw/22%=/#w{*T% ~޿+ ^R ^oVCE'tU׫${dBʕ)EL ~uMOq߈B_H6;MӢ((._P Q@/(<ݦ͹DϦ"UyDv7"o*sw?ewm[Q2 բ50ÓlÓ3HK.n s Yp8\<DvfbrL @;6K{[ɰ`WʷЋHN#5iǏm{+Mwu4?ݺ\Ȩ.s4P*U-ˎ>%14b>Pf*!8h4CR)ה˘#)J-YLJx6PftCr$F̼mb>lAE=;0g;W=ӡV"Ig&P9d`8P`w2*#hyXPȻ}f9H"/HQ P4cJRH.OcB4ϮxG'K(=ꇲ {@f8%PQE Q`Ix*)DD\ /ʯ <&oyp[6gJBSB~=Akٳ"0JA$D*5%a)W'2v@rauu;JiQJ+^o!> am/u&b2e`TJ ͩtR/9?Ѽl. cP#T&G3v`ɔ BXyA ohٺ3Nfj4tm>{Wu];qw?gZb9%я-{&C%5Rq=F^5~-xKig\rZG38nAVİQ 髻<oP6]q#?6\zgt2%$o F'e;W ;ÎJLQ48Q(_n!=bY,bwO뚨Y.ZiN`8eKP,4P .A8P4'.1lN/93^#8&D\|X }T͢ U_EC-QzJVJ؍ . 8C#%@^?2N/} $>NQBH'O:q5S?HZgla6(jpkE˛4\R;[T R73)(D(V>ɃfKJ͕L (ZbUT CQO߾0f˙GWdQk2[8Uq1o;y٣ʳv/rUS/ZE*:>B1_:7nN/3@(FE!yr鋾B:(Ҕ3P4S/tqJ*|f՘_&~NR#оgle;SV1+<B5jԓgw^OZ 1>1ŕ_A~~զmX$%GSN9貉vC6-@3wŭtTQI0Kv/ _.cakt1Qڕ?qfOb_~4qI]e[jҁG:ƧCF&wCz>,V;v/94Xz\. ">gF@ty/)NS3P t|%{A@1*t3s5mr}&6_=`˝h'\Oִ^)69{v!Nd?)F9 7zj-_Lq"_.Cs[V6c݈fGk0a]_,;(x;qL@ڈBϭ|̜~Uw.o|*ƧV[Cm—"9)VVjt"8܆sC*> @}b\& Pɂ3RTĤX>VCУfgM]sB&UcWcxIkOs兮Gcʉ裪uo!޼AChl۶-,k6YJ!5;{3H+^?ę-J Xtf-KXqnwljJln!h=G,Bĕ މe!!!W!zyk;[SlѰpu~RGJCbBqUܾ%~p\'@L7VUaˢ6YNJCwڄB?<>~1#3>5d*9d,t+i>VdDhU_q|P?̴=;Ȍ/;zD!W4LVQ\ik_MqkؘA˖엾bˑ/avk8&Ue]u\(+1H@OmA)FժTΜuf&4{v;qf- }[ < J#*?&:wT :X`;)D7[=|Qiz%Kڣۇxǽ}ϺN]Qh{pbʌSg}?\[صiW9;Ozzޫ[tkC^2N֭]G?VJOxXsKVmɼyWpa~~DK~+կm{'K.`5-%ED(ͶX Fˍ" V2p(gD>3VG׎5˰nRM?%|W;fXɂ!zx 7SEӴ`pG&`$.+iBgQLA&JL6[&q`BqBQpKDwU}[^81>.,JfoReE-o!?_AX䏮;QJ~iheÆz; .(&!D'mk0T6vb[XC'eo 'Q9s㛵-PΧߝM )c2apC;⭆T ;tZͬjѭIYbpAxŃԔ][%QYtVۯhe2r";7ܺ*JVd  'Vќ|iӮJPL ɓCieNBq?Z(ǟuewǿ@mBˉuQFQ <{/R+mLByZ&*6i#NKhʲ9F ~qq﮻PZs.F+3T!:-Yu(MhٌW.+WGXN}K|32.S: {_EL$]Q7'V\{R .Q4iBE 'A7%XIjYBtkݕcc㇊ޱHfmuRvjPSRJi{]$.!@0f-mOw181Z m˿+7^=Q?u bpѪ(əe&]uZ.]y|yuW,4WǟI_ȥeg#K\A~W7DKmFƔSeϵ7% rmo۲>GL-z 9"x=޼kտ\tuܧd scYq$l-8y~+_*q,E]ޱHk&!qwuJ8%bZ҉ ONt@ڡ;)*l&UteO;|*FLF܁+tԻўpH@/"JvO}VjcA*MZ,` B9B3ܠk->XEyi2jj'D ,6CN'@DgKӎHPoj7Fj%IVͩiDƈDvZTB0&S)mVRD&oN=x{of5Od+># {`1˔)?|dPnёQe$ BGO @JɖXzO(O;R*/Ç?+ߨSY2xjsKg *qn! s?v'&RhYBȯr{Tj 6D\A7>6znK׺SK$}nZՠ{K|۝2J,H ʁ-FwBL9D)96.!YDmRהJӾ%k29B1i9 jKU.~ĵ(0Ʒ6N׼by&O1B¤C(*SD(Rdzy5JSII@\Vӥk"؁uvw2FREB@zva@yeL#[YHcd MrHDhDD! PD ӌWxUUl"R뚀 Ѧ悄[ M9o=GHzӼXe,adOT̴Z"#PGI^+˙ wM2G$QB8c:mK"" %Qtlj$ "&DEWA_nyRh8R2(ym['lp?Lqy; ^ZZ1ՎH ܘ{<D6{t% ON?%z.F!LXq l\/s2Ie0j$䷣jYRN -Ì]lԫ'm0o\ΛofM IDATDibi5y .>ejV,bG K^S'X\6 /Mu,iC+9fJ^AL (V 4KNL_N0P@&fyԑ$!@ 6mw6n޹~ѧdv K`֦*Mr~TABeb,ߘ\ܞlVqw<4.ʻ,,;ȯՃXǷo?u^^Z%=m}"?lD\O/7U=9`,K(@đ4! 9#5K$SL3HHKNw, YM0TFoz>$3p<~`n?/_9D9{#XEI.d>8MI@ecjQ QU,ReKjb!b,8Uݿ]Sf*U~; 14;%FIBq2=&m܃Ȓ}7ߞ1hѥqO&~lϻ6zXtYyTݲnuHC!NMySD='_ѳ,,*ܴ!5> \y;FҬI˲{h,=zD]!"l% $U 7!Rۜٚu 7| I[̏MIv;.ӺYI9l6A| So0mtI0Dnyu*5)lbNkf%OFлz펕/4\QaIY{Xȩ+ w?emT %˼faGk5nݩ~M x೬ @0-}3~xƃWIͣ ٥|,QJQO0n⧕,AƀuW)c>3}Q G|e1W(<Є1R+};cj:$\B[ؒ_xq0[y5Zjh{aާ#F2?4=Oog*S܈-ڵב5=NE}L9k)2$"kkW[`)ZeH2G|"ŌدR&=yz@q>I31CVawU]>3{$ %Kq-)VCZ(PxRܡP\z}m7 B`nvvgv3gΌZ\VwCF(DI[NaYE]|ָSjSnmK6L02ngTix^ ( ~F.X8g)5T \}w[&'pG҈U=*7dĨ,kBB!\ k1`0>xkȒV?&ڀ5hfIVpSLs̔?.|c!S1B$4# o_zʰN1B ~bBBĉ$-kSXKBϟ=cbٶm[ttt%J >\&ϟ+W.j"  1Q PGw~]ӈVX. !ȳ|C i}%S" ' L)!"HB`v90ESp!L(x(VZ"B^*!Ղ0ESX9ҊRJٴb0ozB 4Ex1?Οxŝ$%=},T]]Kxwsue"ĆIγF~BxTx"i( "AĶeQ4y( ۓEaDļDK% mM$"vEDy HM#a¢Q4&Gd2YժU333F#t&MHҿ a/gYL~i]sgHDI$ aZ[<h/:?OB4SȪJLi ~[JW W0,¶M5ǻRP^P*23&Aoh:f? WD.,ک¿ K/S % P[*`0z_{3;+R &" zr^I W'#ʿFPY 'p%'|{_+ޟW~ڕb>]Խ 58mxUqiyp؝EhuZ/b'ρZ5zw˷#bT{Suw.)Cؿj<]ݗJ~6سqr@9>ۣ|Isa\pOSm%(Q U;~w$ EGdKYԞϗ`GW(]ypJYƳ FSVvదaMA7.?b]-4إE sVpiFQyM"R8H^ @ID .#{kE+:Pp*8o1𸴇D 򭓋 p=rQįjs5J8eh fB*JR}HIQ2SToN>$ EGZy󞈏 BHVO|:F)'DQ4Meʔ0.Yç6l)¥~eJ94/OB6_3\@/ǯPuHU,$ǥ62f 9)xw?c5"z:+藑OlYǁD1rqK~ qMǏ!4MvWb| )l GkS؇"HE4iJO|mCE,DžF:ΎeHLïoLVvdxa&$+="O$#ȜE T}&%bXּ8՚CWlAڦfү7{ d )+BZunӞ=‹kh‹&]:nkh`t!N> ,^wqqh%cǎ;vرPwY3LY9i9zcbnMzՐ2Qj2w'a&SaJ)o5LsfrF->\N F+ Z3* 5RQ(!ΘgUM;((3+*#b%*l v?@`г'JE%''o޼}n U"AZS}Re{JJ'*aaaeʔإ%22ǧvBFV*.6_ۗӣA@/ I'hV)Z*dJJJ\svr(djDH! AMBx%N9`ΎZ; NNDvtrrvr(eiTrhhB())iڵ ,8pz2ޘ`D9/#"#yMMىYy1xSFB\LdD7!-酑c\tT֐BZBׯlVqݣJdM /_LČmmE0Dly FɅ4Sc"#"2XQ|}FU G ;L*| :eޘed2uly>r :L =9;5&*222*EgL06ʿi$bS =+.".Mo|vRLsy#䘄Ts+ ~:&=ο7DFDƼ0pCZtTdddddDxlZC̄H<ƌؘȈX{bNZbLTddTLZ%w>LIqёQ1iz g%DE'5ޔo!٬ܺζ q𝺱1)6 r༽x 1z>!E"}jOGHIN׋ oүܮ¤k"C78BB@-0cBT*rV}5>r͔5ݲ=zm.u1 N•jv51񏥛CG6~vI]FU@w9t=%psGܿ v{զg޻UJ}!o84+w`a5=ȓOsc 0<nD]dC ɏY~D^IѴ^SR[ LviZIT5Ȟ]>ʊ~V޽~Q|jjOCƒVʸHrKnR}#bc~s慯I㶱- yz|RM/Q~{|W Oxwȣ2?>OLҷKWI\}˭KOϴoч.0+¹szPR[>ݺ^xIW{vlBn E/vbZSk]{;׺:+')VRaI Mwtz7ɕ"gYeS\<3c{1| 0Ƹt:[5G7ܧ'g-k5tҨ$47ܘ׾7~lw|9pJ؆̿ڻ$>>N]zrdudZf="_?yrUJEJ7B*w`RVA~!DQW*޾;Z܍⃆7k`ίӕdb˃) -[~d4;أԵ"S#jz3ꈁԭ3Εtr $ꮯFlrk|j.ؒxɤ)g3*ttJ!b..SwKqfOmhC 9ZVR y/Cѧ:3)۹"F9F.9xx4^em#PQѻtOutvRLJ<Zw?[V˽*e-8dP_Kqk1 ^vǃ7UG 96r fOοy暖#=2ϴ[hpa3*Ϙvm%'FgɋĈu=q$>2ȟc;/JiS~" _֘qaǶ!Ē>fE̶ˮOg-Uk]מmaŠs)kV/y{jk 7~dfnw-=w{ww1K1)KYwWEryôYۮ3.ڲS!oZԴ)m!8;͞5{yGWdy^᥉+K(LI˵)my=Q4mwΑM(ǯ+дD*ap%3RO2ʗ:wsN_{n}L5<e]?̍'|?:H./?pt䫷n550ʖ'R߶},Ĝ;Ôn{Fz=BBF_o޿wqו / wb\+Beh c݋1#woUAXcp} `ys1]=y5(425ЅOA/:{ҽ&Z/L}86EY! :>`Ȧ%-i8cP^\ÃlC~;6񷛇Wvk{IÛW$Y]Wv;3+}#mV-9i1I..ݸ}i_ӆ/ q/L9I``A,Lz3oyQh;iplRvl@\`sRnk7@dpbk4?VHXZh6g߼4qKn\&O yekR|'hѽFm}O1\COeyXbLJ jKWP@(_FIf IDATo;ϭretz$z>p.[5ej] ל?{x| Ŷ%XUd&ވb5r5(*kf+}=&nBDby8OkˢOy0< LtXp`:;.Ecv|wћyd-Y;(f=VqCڿ#XY^#A͙FE>? bX3` g֗=qJ@iޮ:mzys<6M.Uv"0#2 e|ZUڼRMY1 0ŧ?hh+Ww##KKN=Ku|A<&j֨ طe+G,M&xMUZrL@S^K?wL2#@$!MM0=vN1 ' 4q{FZGV֝}Gףw ?V$|P &;;'/Xp¢|C&_y]&hT<  km>HB7lVp%;kdPy̰L 0-f>y֡k5UZz3[3S뜊0ϠX:~N%ogC `MM!]響Yx$#)<ٕ'99I1o]) /VΪ[X h֜, 2]t!˝wL!e!XU(mʧ:{ׯ^z p޷YwbE32Յ/͹Kr4\eW ZYba9 ^ɝ37^p"dĥ}>>SSr#>ҡS3 fcV蹃Z"ŋdžGMf}_\*8DX"rV_w-]x}ajDS4ΐx~\ƥVEjay/x|t6kLy(ܡ]srRdu.K!(ִk(RME[܋BH``Ç m;I1$Tnf#PvۻvL#l:sM IF7ij5F1%Pa}`\;θx` \>uYK˷Ig!~-ĜD`KW<CZ1%ui5jưAl[_"9W$_kggD}!!&vнx, VλFv-'.*W1 S) $w5guBͼ[nsG1Z&-f1fĔ &}RΘc~?zA$Nܵ\6ܑ¶2y#Rg92ȘͲSzz?tvȡ<Ϫ|1yC{ZiIC6`VT_.Kfk6u{ ȋY`1Z>K#'Dd rWUc*rV ;vqwTDbj0i$GUwW^L*M mM$ahR[^,+OFnk~-z/X]h7yZU<1 \B3LVlNNqDKNx0856iPj'Ѩτ3w툰_8Wm_ukܚ|yE^Om/(JMY<ȕ?us/ERhX1ק4MN^27?&BkZR /Vϛxnx ۝7qRûwBXZcE]*8!Ҝ.]Ԣ@-1w)[o%,[2kfPʊzLesmHʎYfO=֒m\Y6 #gv|ܔEM99@{s>bFyAΧ9=3(F:f?~.W8z5'T`[5'~..=6 @J9U}*Ɣ:t.V#,PrҒQj9RWl%'ӀH]&;L&ի/.]z„ r`u^ƋK;nOL|R`Ob?NvL |;Lv{P(6mj2B۷M&,tVc^cǎ;v|rŰaW܋ׯ_R%łj|Һ.۱cǎ;ocΧ[w'C?sWI2p+cyc;vرcǎ;v>wQ%$Q!MTΎJ,@H@9a bv8#L*Z-z-ah@XBy%lޚcEÝQ2ST )|.؇"H999OpsgQ{v>_l,''HR0<RSst@n^`V$$ϓDwmա(p_ON5 k}ݥsl#*h. em?t/:Hvբ#Eȸ&oN/6vnLvن_iQv줯K(ӑe+2;ٲo`]EON)RF`Lcc"5J|ŗҷי_,__~%%았#sB?89ρ }.!k9M\2nzQEK]:i#D!hSۤھgRZ_Eri_"K zԼڞ&,%IBiNQO*B&I+Vn+cǎ;vرo(3I1wwx^LJNiEC˔i^tցRFP} /([M][O5ӵ+J/oGŕkzw;Yw˷'gZyo+f/]ZBcҞf/gwtRz^(|BޞٖVqű9Tzbwz 8rO۷7o޼F6/OgHgI}yhyҎ@0>Ď"Jdi/0pg. {vyv Exx5 !t^q٬Nbo/* |olxVh:qWZ,FZS_!*[eG]z8XbeM<'Bl4aB xȆ vOݲ9ϰ/`Lqn_DW(!'"x sNjM:~(g9M< ȚI+;KPbx&+]O[a"z+WR5=&,6d>\wP|jLD\j6OKhњbReUD`S^[2%$ ̄XVwQEN^ T*Ղ5lT&ӝ+9Ui/N&_f1&(T(QQ(H!h/OfЂ&r> ?Bؐ #^eO{)9E&n\/dzu&(UQ[FZRexg8+09nc=uNlZ!;GF*wy,}N1YKo"TJDQִ,ٻpHJNvzk76񅉑CvBpR $j8[gm>_kOMGgG 1VTnnn:BՎsϘ뽈',>fK$0sMAV椛;^J <ˁck.:gf No.:F6(%S=<ك[֯{cD)ZNV0B\UNcL+ZV#sOKNșo0$4q؄GNvhRtZ,;!Q:N!@rVJH,bڿtIPM~ $(J4*9!H*e2VU+(lKr SFi5 7 Fj=d_.D^9ssADr$ Vi5ĴTtZ\+j4r%Pi:V)c%*BhT/9Zz'yO-?RKNөRޮSV"vi5*z 2l/Qxfޠ6% @S(†?>gd(ZNQI0,jd:V J0y))uc@pa߯hbOnmRػg=]2uo0aIV& 釧1`^7%O^X؉5 k{v`O(D3BďK7~ɣkLY`}ߎ||y5zN4\8v‰V1g̶ 3K%E^k 0JJbXTku:Z;!JnjR#yyV5 RH0 NըmS'-SeRJi?p̬+u:V#}]:}W4#͟DtZ)4-זh`HHHppXB_&Ԗ vA³_WOX+042#%qDUJMȫ}\%GVPI:@HYL[ V+"[_St!J}yҩ@¸5]kF͚"/oL\DDZQ_\5g9L>2rmˋ &N[7A@ [.3yI.Y)ѕ>ב\}.8c0Ʒo߾xݻwy/U |K0g|G\)ɈP|s-b-e=M~Ơrb]o޾tDY0ozdvfwc,'sVtj0{Ųu _pNfUF pߢ4h|TA(J6۷7z~4HF7kPz~j e^U5;Oؘ`Y};Lr'ͼJb$40l`Yj(͝j]FM4>lUk5]qf,]Oǭ1{'Vp+:4]J#Biܩ+:4ݤ[ujTÀ4XtsVRKKTaߟHIpހ >UfhAxiJ5c@Z&tXv´$_ncJ|y){g=b)^kT9}}#>=wq&8oӭM{Áe\㏔u+ ""a?~?f 9548uekI?{#0Gԫ12v^GJr;Uwj1]DܺxWyo`¼Bi9pP~ݳKMS~Ujw+uPI1l]l?a*=og.r挱 j^#Ǝ m,>6ڽK4dhlG.h}h^7㡅~H L4d_:Vj ;kb %UWiڽ7׳~e3zeƘ&vAY' o)Syҹ3,%ebkӤV*-1aKzТAmjT"5ݫ x5%ބ)!L @V}7t 鶤@93ˁ0[gvw@xj>PBBHӇij5Ƹb^$y IDATٚfN^q)$#cMNZ: Ik'gQ䵽Ʉm/ 473#iFd4:3[|/j fC2v5FMKIGm?ީK{=q|~{# ŔǏgSazVV@Y.uq5e39& YQ 7Yk~y1yDSٔIG<ɵU4vΌ6vS+ yȸ1[,^z4i3%^4.ї?仝w Erð((KxO)Qҷdqk=*ty ~zZiy]<A<ʘ]8x*Y¥aQNhZ 1%{FղgSMKFwׅ[Ld5bd8>F7vl~fRwq%Wl@e9`9ǮC<=7^pnŹU܉3/Vsk?ϓїVT7WZRjȖ\휷е4+GH.]exE&%kSs6_2|vlHuqp1T:^SԾts" ;.h`=GSLdr%K: ljuNe|; En?ƴrt* JвJhF>9Y{NOYwj KcZi 9%׌"[&) |nRJqGS__n/K1(z~ምW;NDDYj^) +: QA"~S"z`լuYlI D%^ZJm<2ʽiӤ19-]UٜboJe^UEQ4 jɥ%c)Ԡ`lZ*VKʖ`1 A0[ (G7 x/ɔ@Μ;I%g5r ͚s|KEQr\V-b+9S0jr~˘_~ĘVj%r$]_sY) -E1D ˹r^g])6fdYS F͛w([SToi~/qCzSݴ; חؼr h!+-C/BqL`)C1"PKN˦8`zKaDt+f$7|ibƕެUMl+O{s?N"֨(L|ViތV3sl%(I՝)Fi/:6w|5"*~FC[\Jd9"!BR[U`_ ˌ}tէOuG[DYp#gs'2t'[OxSzb o}rK!.;y<'XZaFe(NVk2)ł*/9HB TwZVC3`Fb&eh%s8U佗JY>/D,ϊ9!x1#i" .)G‰2~l ~ZW쨒Oܛ+jFkFS0@`,D1OswWAdtx[_❓%qn2X4^PDuNu\@g;o=o KJPj,18E?}D"PLZ|܀F{-ܪS@2%L&4ڿ )SGzhے^6\0%Jh%#dpA٬zCz؜!VK(E\iƙ$ )),y wQ iAE(4I(#  "jj2|2&(AE`Qc$*P`|BghUHoHmX~p&zmƄoZl'z߼NIqX\4l@6 H IX PzzzlllBBBZZ': ?'ȡ,)/].ܺ=9c1[9xmԢPDe%ExE(r< gtZ5.\wY.+9KU}X;y+Gڝ)y|޻IY)Ks cYx3}=X 9L4htѺN9W"&*XNOIaEQVDž'?X~_I2|[.[0bC!(wG]ƚc_B*ͤ\֜DR !g'PE{/?WdѦx_`Kz!c9S=Ύ Kbc_Ƅ_A(Ճ[Y /ҲZ",H!#Z ~Ew @@p&|;oÇ4[#|A[2ת ^|K4߼fVA YvNV&KoBȱ,˲,.ƨ,uЕ]oe̩ 1wbg7l)#āWuj6yFliUܶxk̴{E.m>&'7i=TX8>"BB_:)c)<$EMz'S~|acê_d%Δœ.9eHs7.㣝8r@RrSg1+.&JfţIJFU۸nL ĜW i/^Jj('qDݿRqDOU_Sk}:]iPw|nf/cbRePiu\9'~M"r 5D?@PL RJgmKtA!Fՙ5Mc^dԤp+AUjhBT8Oשs]@*hVA0ѝOTGܛfȹJQ nՈW;0Add(*Ύ^?nުAe̬7vXFsiǐ!ӆ;k5'6%][ o7gwV; (t躾w?u+9lGޔ4osQ9Jg-;4\jߔePZk?"6b0Dۍ]DEP;L3NR$ʦb.>qmTN&ϨХgh̛޺ݣ=Z4ZG[8ֈersHddZ%eNUN\9ŕ7qne0藩lܶbʽܛPDH0p r̢ŀG!pMI6*#z׬\Ge=PzM6gzJc,r̠:w?(Bk{VZv#wnT*ۓI|𡿿tiel~'Rt$ozznǒ%fw( Z7>X&BtI0o h4IVDx #R$R$le4b (Y^D kbVk " JW*ϲTi XEX F J"D3օu:5J#\T I(`6q2Fj\^ƈTk`Y#BQ Y@P*FE Pd3T_h>/b4E!XYPtk:5M`E$ +h4jaYLiD$)(5 2A$V$J2bc__-0G$7e$ʀ\lm͉j}gWu̦6nSQ,&IeU %AJE[,M`Id h&`EE M_o?yA!H&?zS3R XEQJ|.>^|M6 )/ӭMj*(6n_"$=~u(SʃQ0A wg(~׮*FQ #bRy!l<[ttsѪwro\doY1+3@o_>yKtB°q/bN5Bs %ReDbeD7#Ny(3EjԨTDG@r 2wo}*SR 0n\4NUUq.D0n8}CI+gNToY #JEc݆m>g'^<{!ҲV~R2c_Jo*֖An_"È68xV)eM r GUFy7vd_8kQsEUZ碁Hؓ;+F+ΤhªT`ux\lVf{┤g.'Z{(G[Ne9o^Y??̱ZGPQ>zذY[~Ft *,=He9ou)^#$XCv>"ح4a]qm7^{JT-Zzŧ)o + *}|+kFeIQTq6a>ś6:Jx]6*$#YڵY^ɹjC :?<뾕AESK@[_fOT:U8PboX U_J5|l\\Vٻ~^O8YWA.Cq_GG^<9ǂIE 5gMdllIg:ɲ2 gϮ޶)6|EjRLJU)[hA.p%/mPlf IDATÏCzy̺lfԉ'ڦ-)_Qm/@ktj{[kRt.m9 A+VY%%(B,cb:^UK^SyGlSƿ_sFLȸqgs~'lkǏ&ciٓ.yeTeRYA<{?&{ǫ$I@1rim/.u2kX*(zqu^`]k=KֻE+f.Z!}+ `!UJ7]~R$Ǣ9);q=:v<^9 ~qpށ&l.| ,+wQ]~{bXû{`2|Z~ W *T=i=? $0^ȗ3W2ekM|(c>RE* ]8}EcM_nvƯ/݈wE|o`y;2E]L^۫o>K/iY%o`X>Ggavis!ojLݐCWJ[{4T_pxG<ۻGd!AUJ(ܨ{Y{/ !νẸipTT/>%D=mbYLZ: ߻Tşf1=>s~~~ABl2^L999yI[lZG#$]`h/`UKL<QBX"\Ӿ¬ ZN9$mKS9Ċ +$c_FLՐg9 D@PM>Lx@;]`:~LRB\J=PT+̗/g͸cUovظ˴q?e^k*ѼY( FFc"NiIHY\5Pgr lӼϏ17th?sOYw.W\wyc~=N{hպMX1;1>4u7q#hF'y^auߪsVm\S%\CLV3TC;ʎee9M1qa ml$;`Q`$Z &SwٸVY݈ܪ_bK8~aWZ-;hsY< KbxlGM'w8֦HIWJMK|8vs#V,dsٷn3zMS=s"k2vci\TW}W/~`,ưBbam=G܄l?.m1]J@?@0gg_O=8Að;x#3 JbED+d܋an7 r"  \P6͙$FxelJ1bqS?E 2 J<Ә]5ǝ,],0 1b Vve^yw.~̝ELn:2jڭ~hïpHmgj AI%DH@ B+Z^4 刪5 D‹RZ8fY&ʁQOť99Tylo?aYF$I/#ONPx[i#~ "8uԐ]5/pjJ9!u9WlIѷ 74O:|=E6 N(wOTiGlJp{ǀ9,T;$`r5Mm[uxBޠ5dJ_Wn9Hީl~ zdg0ekbe|謴ǏLRt߲D /,*pgIywypjRa S:x! []Ѐ_6̍"*^5TȔssԂ*뢾SlI MUhAk7yI EQڀ'rT9x"kx"J8/Xr@Wؿ};ޕ]3l63dڥt>.yP>MM3#z|sJ_TWv w֕ rCK ?_1}ޫsK]>fG|v3iP&މײXֽuRAKmwlR|68q}"bY52"WN9n:o `w`ߪ1f,˻-~Ry6a%^X5wnRg/ei_PS~IG\;5-_k{`ļXO|sM뗰xs'rz],gM;KŶ~VC7Θ.VsVMΏM|gM O8Q2Dz(,r,9qv\)lϱ4gYNʌzm2^`BsĜAS--p_2dvċkǬKp Y鯒^eJ{훷jeLXUo?n4MLݺxi+fME}QsU*Ġ_IzKW\lO~;(HZx㤼  p0v%봣@dƜ,nX8`1Һè9&bBN_ǬY bY8/EcI?a:5V|sYp+dv*x<˒Ϩ:Y6i&/h~ٲqq;W-tխΝ:Ws+V9QvV|Z; w@(J0J 0\D8Κ^`_2CVʿ ܏`&n(+21h?~>| ?DЭiηk߃龹)Nvdɯ \N=慿hp}L-Z|B, &%%֖D'Ox{{ӵ|3+یv]쟮!66x;w˗*͙m$-or(`WA`ECTz:>7~b qg2};fhcn ,|%`»j-VQ3IV30;^}Xw&> fT ,X`oa¿KN ,X` ,)m ƙ?rS XBw$[/brCý@gs $-_  PhaI =33SQ/ps*IYYY2iA666R -{aI#}>B^Y߄E$- _EGbkU= _/ۻʱ rrr={9,X` ,|ڵk> Es WlB87=I1=p~ ,#n䤧N.g _sGRG ^+dd:)),)iи gd{fywR3XnV|^>w MaN4,*=ziuHfSOɐ [j{GZA)z>:Þ N(MEs"ED 'Ҽ=5A*LZQyϲ8W_'-տvL6o}A ۷;s96󠰒wfƵkz굫WU~g8wvbSQHfYV$ .qҵр0VGvn=v`iW#{"xdF$ɿe %|DԌ t{{{1" -® IDAT^tڵkofy:|ܸUVk=oS }T/ԾAD>KD2]8}իWߑm~aӸ/N̴IHՈ{#N_zk8tM#_㢦R;7=ٹ3$ͨjVkT4aQ UT +S{38ssKVo2S'DCzs (o'>5{ʩtDH"Ȉ',CTR;]>| <6"TNӨ1Za:VTi:& I:Ne:IJQGwKȼvxg&+iJ7/;ZZՍYXӪH04t:5ND}~Ӯ{FIxk7$D5:^QjU~"^hCV)n0?|(hRit iV4 @4䟃ޜf(@$VM77R9y3]\ٍnҫhm^#h10ZZhZBɽph9 gD\l$D;n$o)cLxӯH@JuZ5E ZU!`t:0BRo"("CsQRgm(i}.կI?(7n4jƢ>p⹁~ռً?_j ]~cYA;ATWewj7kmм[||{c7C:Z{~ˈRER7\ijּi/eV ۼl_G}lO{TRe(N ?0ƛThѻ_Z`o[t 9?$iJ1~=tp_LG6 rc laAdr\znMj`W^_~h78myh=muu2ͫzL;ۙf>lwiI" 1ܣs/)|iVӐ1i˽q#!sk7oi tǒH>DH/;GŧUwȉ|4zpC>Wk۔`k9\L ue$v7.|PA91BNiQ9yӗELߔ85}-KUyYӬ-xjt6#&ƻTco]zx>F7C=s__SOeڮը%]iggpgL;t7}w1V0&aG:SeZSO]K3vqҔYT>˜}~~¸FH7#X€7g ^V~o>[L\ʶ9o蛬ޘ$ 1]훾HYA*NUьJ,1JM("yVZŐE9^T4j$)$MHT™X#B I`y_ُ0!ХKJEZF Wi^+J}hQL=K7fܽWeFTʙ@XQxwNX,܏}QuunYYFHC$M9׹;._'pRF6=A7 +>oɬN>GtpAѱʕ*+HZ}˦o\L/ҢRkی{{B2#6Y{'񑕯_:vwSwߦ۬2\>I2^k uL͏+Us3=f^̩%tl_8XMC!nU .VRP7b]&7_ʍ=eWȕ'^Ĥf42$azĪ5lY-McvMz MF߬Z:Io/,-뇎Wt*ռm욶o;!U,gH=9-m]k̚22'L;hg,i/FE=bGg=b龬]5)ӵ3UP({ٹ-f\\\NcbԲ^gg/w56;ګi" >VoPv'?-g?[.Zubu-!u`Tgi$ Ip'w{HP܋ qI)-}{oolJ@AO" ҹٻ7٘ۗmȯH@.?e/a+ٸu]4 9lST7KU? GЋ}x#ZL"7Ejf%_\XIzRj 37IVlJ[^J!>\;t~I)֥bqwFy~2bҍ5rʆS{iF.UjwB˷NU9۫(q [O,yLƒ{蒯bvUO vyXL֣m9e7} |7/Wt{O|S{fmOTյRB(e54<%Yb(dw+VqlgXAּ[dw\pn`XYB#:`\e"Kv]</ޭs]| &$xtlp@OVDm5 i /\e3g_Y- yr% ?X,H+H͠m:gׯLƈek#(DonK(|"S%JD@TڙwYG)v -l"B@$Ȕ!qZB YPQ$xD$#LYρ9" #L\C‰2X - )h =(zi]Y.jW;q? @Hgu? 2lFEd9\lrܧEc7uкڝ`Š?ݵ}9'?K4aa HӺcNV`voG_ZEvqLL̵kbbbdY'E &_w"o6)L")W|fIm#`uQ@d5 (L*ɢO5h1M1S5jݹzsˇ[W.i)R?GԺ 81? GIW?UP;veJ7S|ju>d^2An{AjTeHJV46" !Z Bd"J$O*eқs Sap;c)qW;*26Vvp6>;q" n-x/ ³IGc%-NKN{j(cG7O>rUrj\cwg V3XF14'/#.w(D)!ivI&_jʕfzD-/Ӱ~cBDТ:k3[G}<^]JꇅDAzɚ5tV~EEˎLH;/ׄUT;VĜ޶;PlkX+7?q+^W|zxՖ"c=w×I)/ﭞH]1Nz̆0} _Fݳ^w1SQ7c] AxfIx|\ ~/ZM$ި7$\cLqWxآNdޘʥM\:|\Q+E)%6Y"qQK ^43y \ڱ;B/]=jo`&wӒҒYdaO-hgԍ$ޤOz˛mҝϬBGZ!JUjeFYJN9u1)ćv_SnNȮeF9ɲr|m:kńgR $J Z fЬo$2qqG(;mU8|䴝#qqR)^}S55 Є !'N8x^Gy{{iIuobU*cUكUOR2wmmTwY1zϣZ~p4wi e:ưۮNnk>dOwڪMYft\Wзw|KmDUɹu8'b*-BJ((g3+u)^咀CjcN= Λ*I`FٹZ£{מxoOLkLARJ$R4޴)ú5Zhcj[ǭ6"+\ZdBSE!S~sr+f-9r c:2MKa$"Z&3EG0uJeoD\')a]Q)_45 ]5 UwR~O'MPoK\ oYDHVV)ty.[U+)Q@J@=KÎ ]Nw\|{$SHUFٷI5#Uhc@eYV* q{S(L1{6O@T7&ؾf#[ LxM"ߠm[ גz5umkL/,R~+LڶZ:5rcvmٱ9osƛDUh)t;@pzݧo"۹~ӓ,xc[mkS`~1gtSĘbuS$md/ĊZ=A]V!)ǩ5ZY~ԩ_ݣK5e({Wl9L,wviI[}C[̩scL_UziTzhKVҏP&WrªIȚ&2bB귆`*aل^^i\-,oUL{) 6Uq~i\b{6+:EqţF7Y\cA)ZgtAʉ26*H,JzMP[=tSi8i*nNn]}NɮRH|qNûEp v+?r\S19uէ6yډdFKԭ {B›qի?~lON)RHvP/_Ø(VQHVH+2g1E_gJЈ9qubg"fIf.ı-YkPJbi D9T K!IY,DIx (F$MDi5 "PRT`[L!j e2E:iԒUoj%Qfjdll  [ )-đRj4[D$5dd@ FPgwo4LjQ*HjEQUi &(H䬼$cZV+)D$jIjD)JY8ӬJ1"h5[DhXdr'GFFEQJd( @DfQ*op2Q+,r| IDATh QUMF*9K\ڏ"#l1@V]uBqkX'g3Z8:::w܄D .Dl'1Zg7xB*YxBS5Ka" f81fRE2j sakwo!> ?DGnkC7Qj'ׄww7?j˗P6 G̿ H:,|I$NGQ:[N>gTdr(8ݿkuY;n+g`l_o] 8p?<+6ns^o28?E||Ldɑa=22굻V6}?6Q_6]O&$G=Mud )QS?GwcзqO1Sdɐ,\?9~E\Vn!tqR?;W{ި\e/ĺë\KZN"Ͽ5[IgWd*9~Ȋ"Ѩ(&P.%|ZSkm-y]wG!GҨݼuʵ#ZI4_2bTEyCt#E_ڵzTd ,ӹg EMsU]dA0?߲J6u"3e?Wo|^Ot+F% y*qԆ=i+2'/)r}A7k7Ce63W2:oA%:9(oힱZSkxqhD(VQbV?ڞ {/7IU[w)4'R,^~7ڮw~-'.=u[K熮ybɚ<-E2K9iћF0h8p2귶Ʈ M?/vN?uz+8,oj[-s~}.ϧ,gkQ|b|^,=t㹨v+VaJ]`Ki>EȰNܨeAVYfz*U3gV_jsw_um=9 |wٳW>=*C'n=# J_S\x> CCʷEtG;¿̴).u}gc,GAsz;gM*=u TÎ@xV׶[Gڂx$\8;6.v=W׸)N K:藞={v֭dv ,K}|uYWp뷂A .Ks y^={\6${iV;:UAZV$Xqn4Jd07*!Revƍ{W^]Re*">}V3w 0zÆ)7G=~VĜأ ꦹd|߯ٓ?BoI8нvra߬~'*Թ|w/=c'UxPݓ'I:pڴUrjzQmpn= Gʍܿx\i`o7%ӝH{<3dHY-OO&ˠ}|;N3X%gevD-vSΑ@Z$'T [5^X.>lidLCŴoP7!;<ڒ:Ƥ~? rҹIu&- 9cc}Ȣ&ߕXG2_VܹZ,kȯy{hnzD( 䤈 Iq%W$kxSvP<^֮!ٸjzC>d֣{oww:Z)mlX;vn- =b4s"^lSgd|PQ;;+)eH64;hB E -҆VKҐbQ:7u\ \ Wsmr傩gI"FP}G.D2ox$!08@/}(*=+SԻ/Dn@n:QD o9lqU1B2O0FaȠp{4s . 6mQ~3 Bij@!phIJk-fj".~~>9X !^Ejezv,N>/ؠ^&6$VE/6,:(qZ5lE%ZlX37;ciœNC JWٰ5[gx}+ Z_EH:!eT`uwv:M䄈}VZbsG4o 卉7E0otZb8g*dY:rdd -2% /=CKȆ_J_s/R3"S.9g؊nPJdr{}ŌC )55P{JOVHM˭'[ FwMTCS h%-?,DY7 f+ߠ}<~e bX_llKtuP=RAo߆I.ްY9l=?~&!IulȱSG2ioX=w9lÒmD=mI߲`DDѣGϝ;.?Z h;xb9zsbNPB7TmH7 f7$ & 76I'MONQVΈ-\-lӺ?F ;oy{|]rmf +I 2^ʑ/NJ)趣υ w{ף G\MCa DZSyyxDŽy6n\%_dBguk7ao^r"R,9mU81wzE>!J8TRd4&Q3ǜĚwB{1")o,Y_lwy/^`Ƹ#,jVhҬ3a@Mq1'vm{f*og'G)9G,v;B)1%59111hx"jZ'6KG~r7 :bgyj>l\ 9?gEgmd"L]x^jة-^hba Y: yp2ش\ѯkTfC$$$8Q+Zc_@2IfA$ħZDTdIӁ{"/mpdK"}kXFu]$$<>zPZOgMݛ#E%IO.Vh=aK|ﶫR]33-2{;C%IoLILLLJ͹aҲ'&yf@iVPLڐ)_6OCJظtSsrC0T:b%\>oTK#OScHRsʑ+/04tB)b{W$ߵe%Z-& 'ʄbNlIo^Iҧj^$j},F[LVB+TZDZ̜ #L5jB VE#$l*#~-$IڳgϞ={2̙|MLӵvk)3g e@xx{5tӐO #yڣёr#6SJ<,$u'{DNֺ4<@S$ bN w_NN5Wj3u:ZH(7:gziYd 2b&Y,7PΧsj6 &ޫ`8a]"צ_jpStZJΜ}NK ha w(wunSwҲ#ƍ[eX~zȦ9;-{?/'%);?Ru`En*t-֣KWи{2 딣EͪfYg-$5|X v,1Qg.Æ&o*fۄ>kkn 4uvtS1~oZ[rVRO6ClOHXnin/3]}ݢf6+"RNتSXgӨy`yYz!Ӑ_[T4vVh 9:v/9p)nv_Ŏ m\*8aF~qvsoᩢq ]f s[s ;wИ#gd Ռ[ K!k-mûUV\o߻֐I5*u.B7%ȸ8wo@i.lY.+.p ާA jyȠ5ƻ=}naZΞ<(J(ɒ,aQ,9:m"V~-zu~ԋmqlwxėBͻTQ=Yb+yuUy~h]YfU5ܳUK2~JĽLB ; c&5%WS[^ssrz9iZh%D;7_B=[A6hѢ /{2evʲ神|}ܽ{C~G=ldna4c;!x_OupS$_yqP/|{k"::: smxB<ϛa$yyK|m IDATyX"2D<$Z`%T 9r*re n n ZXOl_o*JK:CCn;bRuZU*h?ǍϞѴi^w#\g2JzٮT&FVwaݏ=B.\qYOJZ]Ė9v8p}qwAE T* E|$?|]BD(d|ziI6B!+Q;eoy`;+, u6O0}c6nq+ƈbp|l\8W`BE 6'<8f̍Ɛ#*{1B>>>} 8p_:0s1 !n/_' iqb_@E0ӴVøsN?o%A#!OF({SKڼ'@x/#1lI$cr'< T6Pll̙ÇhUTXfHRL0Mg6 =3Ё8pDD L&_v_G<{Iڸ}Nd!9!G'.:qۢHIIf/nY,#ykj̚.0 m=Z5dDܼd񂛗CD$/Gv\yὧ^Iw̏?H.?߳n+g`Y" BEk ț?o$IE d?_2z߽%ºT8J~m_ '<+VP 7kЌ9iS=ny M++)D)ըJ]y<**xo\yCj$;{^B~E6ʣs ߷$/.W!LQj(JzIz;C|.L&<~0tCYͩE%qJJߟ~7*CQg/dY&J(*O9[ e! ˒$F@$0QҮ QBY PZ@dYdD A$QLQ!Q_B$~v;B(AxVE1EIsa7ܵڿ?n8222$$!~ݞ}AAAc;we:v KVYlj^T)/?'ɓϞSd}ǡYL?8M2]!RU&!(~d)$ gB)))Ϟ=+Tx8p8;ޱ%I*Wx!ޞ6jHJJ,Vt}-[<~811Q[ֿNE+ M?_Q kBg80S,P(tz´ iaTP )_EtZQ*ʅWYf|?fiޝ*zS߀isLtj ގ|=,|e#$D^``K ׯp64&,"JEaJ({\4̸(FC?ŒMWW/Xi(G]?&1r|}(0v-Jһf{;\LD_Ad2P~C7_ #H1=1Cp>bG4JʌWɲSw%G<[gH__;s? 2.Z!2vMyBs7%ܺu+::ۻDP,zkR[yOf=.7|JN|˵Jm~qpF9foX\k93-\ز|́66OZ={7‚i>x)VDf3'p!esƗ|a~dl`ƥsP˥Sg*O^E_+i^}=/-jP>P#I1W6c }dž VNp;qgPö^ؒ~!gŊK-ЂRP4$Ȁ ΙST qcԂ7zĦS:zP,tmˤ{np|"(ٕUę-RT F#6La*DdjJ2skAX;ۡWO˧eJSv6 'HaVRdIYmUkY)4p *$X6 ZT4 aYIh?;bɵc7{i>u6)Y$Z9^&@1JHLffd7&?zb0*dZͦ+ BWn߈PoMwIB7կU'<}̄)!)J̀`J I+N,,&+҈̡4DyZh޸c\l~1H6UE+JJa{}?ϛ7bEӷڬk6i+C"Ev̛l/Glat =!]HA-\c 2wӒFN02y̨f/hzS6ן8m}O^}`F.Cfp/F(o_JGF ‰2´JȜʋ2´ÛQQUJj% '%歜@T I#%qJb]plˠ]+T 0Z2٬0T2/# %?>CEW I-5$Ŋse)ˮ}ϙT!*mElVNHW "sV+/Ɉbh0yaB*?::ŋ~~~]ۿsDַ^ƹoP*(>o R1gBTѷ\Ҝ;_yV\dP-Y}&Ջq6b/MäP9 'hLVYY"=}\2{=7+X{Ec%Ec&{Dvwm~ jb,_73޼\\]޹e't?UU{ﻕ%|r15"k`)I-z';W9+G?\8L䧛7 ['}oCJU `~wi6VD_w )L@>ʷҥ(7բuX\*V3dq3;5a+%\̅ k:^oNtڜI[jh@2 7y(^s66:U^;C1Ɲ1Q5Ok׻˱.qc޷ʱ'Iq* Sb!KnoyV!kuiCͽzxփDžz[K?pM pG}l?xZϯzs;'馬̓Wfw${eKjuI>9<ˊȧOY|A-j)nҪO~ſ+ !ؿk#aڍW<yNS|tѽ}߯M&ϵg]Zæ2 Q7jO:nYcqM~#7DҨu1 STpT?NT1Jxj/u_xxkN9,$cfߚ򾁀MX`}Jɰn2-U_tc HvgWZDix/{qE' c7TL@_W}PE9Zeogs>?˲]ܦר!!QpB0JuJ̛ayAbʭ1tz)7ZDVv 8R)U44hdϔ -zԨרSNUkXaI-kDRlq~uX%h났, (AA"]ǀ1lj_/ cquzU@4tQ\LaQNjKn^Go*.ܦ*?+!;l`'ȴiL{ `JfD]4fjszE ҲCd/BБw0S#6wYZyaݜv*Jςg`g[>gwԟԴQ-93Bɻ Kj+[1~aX61 3Pe_IXƦUeݕ#rf\Ӱ+0TegMp jJ!D )ɒ~*s.]t=} LZoPtGNU/:5?_%N DD$ߌ{ս ">9_eMwo5(x#jD PfB \n@hUOv7T]܊կ.|Fx=UuY"a҄ +./qaε!7U7FTB"U0@tDZmooADANcګ}hA6LH$ʢ -q)>lIi6 t Q(@x}|.+rY|^;f̸SRyCR40Ps+t/3D96|[W._EUW94EOiwcs)TTŶqBJeM) f\ɗ`5L]Y3[YՒ1 i3ie))LbS );  Xap:#DJBeU!O~F5K]#L:m="h )B@hL xqZ  ;tȁP :MLa`W!=qM[a#wݽ-+{!] Tٜ۴% Tv:PM"ɁG $QZ-':w+2ɝ4׎ߺ6 h'5CSE([:BڪQ4yZi0m6ϴ)~?YpdD iݎJc6UE&%& B"Di924+f-!VUy4J1FSBej|>#ah0}NNNB \ILr‰S,x^Dԃr>+[GW`) F%*ERYP>CCRH$/KBy5"B 8"Eωϕ5ZD("@)0-vuj6N$h9-*3[;; P}$Awnu[!iQW:Ү: g9W 3c D7ڝ}ݻwsݿ_ݩjkǝ6*ۧU5++~j?CU][< 6'1!Y$gs}VBB3&fN؋m8pyr/[GM&_䪌hZ>t)ɱ;6+|΂qPMz٦S))mΆ _gھѪ !\`FT:y׶v C oآג򂳛vS'䆈]|,%95JB%UHO8"kْkhm^Q@j=nwұm͢vzzz?Ȍg}wsNJ G_$G_w)ֻ^E{e%aFPHC#_(`Z鹅 B.胢'L77vPѓoMh֢m!AQaHkþ$%%εkRfZո4Ce< OMHHLHHHI~sa/rm.mKI{x/h*A͂KKk1Q (s54Q@>0e\ʩWW,tyyfn];LVݎF0rO&jU1sEuU_OKbV`r2 xNςw5n<8@^:!0I%s`Z}t d) O÷mޖh8p__?)'Ă&̋H+xrC ƞ 7MwQsS~37uP.l]& 0|טW,LȆyQ̚m9Ԓ(kœUo1sTODt%/;vOۙV6u_W|72G4 RC'n_ /Ėu Ӄ%~([j Dib#j/s\r;&X|K:ժB hRR#V-7;{NjnΈY{)`Л[hS{|B1oi*{]ڳbvv!gh bcZ?5ʥn!m,eXf]y1+~Z8|Tn5,MĻ6ukKf|kA4"fnWKrfܩEFkvEC ͼCfIct/td0J.m=VNt6 /J<1@fZ9tj^D YW4S3i;Jހ|Ǯ|ɪ5ȼv>uxn =[)A_(]HWM$ͧ# ptTQXi[Њ'XV7+=K6Ost-˻{N;wKD-9֦O,0FUݔ8ˌ}zQaxZ4k(!qf9a;g^D3b؆CuʯSMhu;V!frXW! 7j~ޜe)=gg˹dv{j ZvO犮jʠZnvŸcu qv͛1)h_}Weڒ=bku\2BhXE6"!Pss`xǯ]\z[i|ekYKo+;EnzKƌbl`鸗^D$n^zS忠vr4R׫auʽZ}-]*9"}ሩ_Ibbxa)vT zr& tIS04B5wD*!\'~B(NryPڵkwڕ_ގP(FP ")vOSӺCxp"fD k Tj L8^LK#Q9'(a(\Ζ/k)0%aPb H%"k L1X^@b$4F/yW?R'@Kd Pt1cYf$0h 8=/b0A!_;"jR CQQ9^ H$g P0 Q L3 <"#a Ќ'S `<ˊb(Ė7 #eGGG{yytIi),#  H( HhDD^ _uh Cc",˓Ic^yd$qh @X"`YxRYN1-`Qϖiy@@"#Ho(V#y3z+wjBaK*Q %7J"x=bp,'%%QDC"++KVR䕇cKBr kxJ] 2Uw7z [RF%"[:ϫ\D}po<_JO2aykB7L}5t$A 7[[Sy+ ‡-9c+k`_|c}?Xe_@JC*`4DJKM%]񯑏`^-?ԙnP*bc=c>ݕ ۢ4} (e@ͫCb(@)%Q$!1D^_%Xh"G BZӧOƝy]1o5bĈ#F|Y n})#߽pA ԧi:))yŊa7[5eÓCCƌB_tBPA/}ĵ,Jiפ]s/+Y~K/ X " 2:1pᅝ.'Q:tm*a?!!`(sF1bĈ#F۔33AhѢYaa!qvUVxr 2t: ټepĎ'{vc^4ƆoyQ]^޳u~=K7>OұSO۷ۍ~H'0|^Zwvҍc}H7$T|SS./rl`ew !Z(Q !E"\Dh};.-QX5iVݸ f3D&jiÌNU|\Q'E["2UR\/h?al5 %b5u ?Iq`A>sss˗Z655E25R.I4N5`FʅO!qgnn^333ZKa̬Rc$L^䠐+~^qjK.j2 "VT(qخܚg&NR67{lTYi) j"ED" D$AJ &G?/,gj\0Z ~C"|d fˠXܬEFǎ8h(Xq'^*,|N`$\ejkcC SMm]Ԕ-rT")H ed#I'y>U_.UVVփԩ#?vY1bĈ#F|ƔAegc Z%$ܸygmZJKD_W)h>+3RVJSٱڔ[6r"^ ;r21db2vmfud"PX}  ~zlmme2L&w.8Ѥ\sTXfu! 37֑n~ᇎ7j5]Ԥ\*x̚t5:R,سQga8k֕/E>_ߦȿ yv|[w[/£[aoN<ϫզ5| ڸjOczڌ lZ!pL;B̵;6kԢU:nrאロ Ȳvil!|:0ڵkGMKK{k뻇vs6ѡ U0[l5I{'4gYaFܤAxv oo-NkjOŧuX=p11Z>C:2:] y7תEF 5j%;t#fM.Q=Fm_ڵy{.#wJ~~w,Ԥq&-z_sF۟niz;Ͻ:p¶ _׹뤽W.?/*hEyә,0ĝF. okh^Yaι,3ŌGc#NVNQvZ\ƒĜV|<,փ&twb@ Ԍ<K2drL%" jeS6m][Z$SФw >^.j7ܾ`F>xrŭZԧW~%{@'W*˫2&F{VŪt^ıoc弅'hJ4:n \9.vn~m>|W{6/!^.Yv+pܸ|"zӇjT{}::Ѧf겖BoԢC |~a qHܮk31VqP9߾Y~6*`Ygܘư}"A=7{`H{079xp}'̣0p7& tfBx.Ngn8(KKX🿴:oU]fڔ':X͸QkRR+m뮾6Pk<#TQytgs{ٜ+%´I;'\CCCӏu٭@#{bא~NԁdϞ<)+y{BH_~o?hm*YA{Q0P-TAx)qt)(վNGYg aTCjB-w;a3`)|d3΀(FYf~#+ґc:V7>3F#CdL믶$w ƭ~-hL`OL oqvAӘ;HzϞ߮чl8H,^G\ w0R;ڰ+hd7] Q%YAúH^OMjz]vmLQ@X(K+SP1O?P"^$+B}blR6]疟튐zi ;bյ k.k( qg*.@3DBaj9ѧvf0(m?=H j ܤ{G6պ k>C-d کŊ2SQ W)B}oԶ"OTܩcs/N7]=uF+w;~VE4v\7&ܵsmn* `H1aY+/[^*{{L]DD?|Ko;{~>VԭSKZ>!H̙ 5b^8z:1ֽI 5g”YeYe\A %ȋS޲Z8M†c[>?Sv1܇LiW"E"wa$(qoWD}nImV/ Lph6/fp"`SZ(%"!и (|Y<~7bt..ٟ&]l6c~ ?wy+rb'οY(M 5+-Kfw|tM L@b^oH^XW \1''{;Sx?û[XI!|| /EW# W]mܾG*N*+Bei!;! LӺKiŖ!y#i(:MIԸ1̙;٘WZgӻ< `J8Zd݌ܫ`1F$( P7̞Zwꚾ5>\1Y,adC`,8 !5ӮˢaENx\ItbJ$݇ 8lCOq^b0[g)?0&ijz}Ⱥ=YJ "8ap@Vm"REvdy/IijM-PQ>-R# x +(8}m U[}R$/zYןvB0}iiWv˰ 4oO=7dLV,U{wSZ@*itfIUe=Ř0yщZ<ȁF>xQ'g߰^Tm9|헞?INرvML`6E{96{XMamاQQQ ;3c˳ԇmz|s00 Cܲ0Ɩ~~cb? =kSOePje#ɋf@`ܛ9-FOa)z'VkP5"_̌:a.m'o>wo͊6pТ _/9&/S^$j_۸;e߾ ^Y'W ̇1|Z*IqVpqZSv%MsIF>vRLF!R,)!+9-Hϯ نU%.S~TV +{n2i* ;O\ @IH2 lp˷"̟Z [<LCZxc󉫏ލkViܯv_Lm>]RARzHeKL|9YN;w<~Х͕Hʲ,R-LΝ8~bΡٜI$+9ٿ隵[og86kYֱA=ϛ6;{ J̭< xzrckZ@a& j<+ζ*1J.>z:IF`9ںJ],/|V=r񾫹G7Sy f5B=%Q9foݴ%-4 |Fe>}Ka322ߘ?bbb<==?v)ކSkE8CQ^=@~!C888DEEa}}}7oV9Er+#F1b0c'Qp/Je׮]?v)1bĈ#F|QCpiO5Z61bĈ#F1).pc<[Zr(- jJWYL`iELAF@޼X?Ku9W)e VJ)@ Qz/K(dj\(8c}aroHK>vA'[0#_!ew{,0.\XYRR!i H!P'[E[l91Vszߺ+sRK/bE w2j/=pFĥkZQ UuY+0t9w7H1m[,: \c]_HUEi4F#()/Z-KclFw>! RUPPPPP`ݍ|0GZp'D>fyIzX}il YefjW:pDA{ۧ0:ĎJ0D3BX^ #" "%EK l`y0%eqi7bkyp@ʹw64Y\ ( % Ea% %p#G J*ܼ%m&#z=WR "r,Nj 48_n۱B7ST=/gj(5c}IrhHl ?g|[Wq:wQk@JiRDT(զ7W ur6-;ĆLs~=L#7Mw/zdٮgwZZ%9KWgWY+25սGeݺs]@TVn+p""(n bVCiZnǫ.%g^6QMݧDвsZ16oJQyƛMUW򉳳߿ʑf60FFUħƘ,4b0F/B ABD#f!s/b FJ /+>I6yy: Mo#)8:t|yF , hӿ)Hظlbuw/qϘJ#<%콒1#QH(]Z *(23r0%ղNJ=kr, /GDg<86WJDY9mSn]'N,{C|e*Ś*'ZBl+Zo'yd*m>ZNbGRQϞ=C\.-,,<)L.a`꣤ r"BL8"Ih @fd CQH_wČL% lqX8҄ 8#4o )R#iJ%c5:o0*S -4hJH(r #^ wWK"ǶҮ[j F*I \Q2k_WB\.to  2W HMovj.\˥B -"tP)*DQPRCB IHruT@fg+fjuF Riհ%E|CDÛOX mAsj˗&aoMPIۍg K49Vc7iTik4,jD"ĬQqؤPh?P$-$tzeFY@P$$ZޤH*"ZQ܄P"E6i<nj <1dfA^k8 n>'E2)jGs+\Lbp|å/?Ό/+HDBS<!/>ԗcW[Su@-j khuz ;vq/,%6f2Yi6#pG༷xoUFalސ ⁠S k׬uE8k0y3.;w ecz(0\gch4ٲPo {wP^![ jC.I~~3g*I1‚ԃ)u=W$U%$$H`cG?)ȿO@)z E mGoЋv[@4ǧ!qOcdL>_ydF7ϟԷ ^snے>1Q]a~Gp5NҒ>޷l@܂dBvfx (dی\tj`JB%5z}D!Èh/^y\ڜ~Vvß3`M~rua]^e-ЊqCqhc^!Zc+?+5::Ygý,d;#Pѩ FSj'G$|n訨.q[0eN^*4jN M -qYiY|G;@F2:,,2:**CtOXЮdJ frٵ߬OՒTcd1$sGnjq'{RbCF*Bv'A ޼`}GOrd40l DJM@-::J" ;u{iʿؕa }eY3$ŖN^*Up4@EA]`CtppBPT%Ay093q|{h+rOͼ}$_N`#zȄ{S )r1V{6nzrj1Zp7/16y{v6ɒ?XMMʜj, WZiF՗fdьI+(eBX;E5:o!DW߷.9Fh5{u<;y-DiHOϪ5"scc9DIE)f*ҲX_Sk+ t@\.ܫ{V,'/1b`(ϭ'_@^tmjNֈjw L e5yK]ht vs0 Z̶b هksABEVrfI )\I]C`0!98B-Ԥ\KS*3f!:{OTWYp.>PxVzfޫ7l\n?]^?暱eEN#);F5% 0&͓H tf7ﻯ>HH_zm?=!×lkmL[}kG dՋ|f}zw͋]AԚiH92kS9P\m 븠>0hX@%u I9eLmF´* !Qۋ4iW -}½jx޽JkHWU ~>6"$7Ir ; wKqX*S39RhgFg,azc pU~zZ~Xx+(h9,0mkq"D/P*IvX_ m=z–D 74OD }yvjnA>.Vǀi)zu`Ss=m,HY]lm $ TCF3rD R1uLa$ K3Iuw*1)Sk]LN79Nۂ)`UFA*|;_Jɉ⫉V'u $ {"5|ٜ#h$I  lA h4;Gs5s~a]O;R^F5%W-`viV|}J/gf߸V0k40ΛϗzC]n >9Qxw5g.Xdf? !G5y ss!͏6w^+sNrbRA\Z9:htFר.j;^UQB_N/,˿9Sf% f uY<)&/ _ئ~Eǎdt lvF}v& iI#܍Ls3"97l3·d' /7^d(; ^s lZ4"D]BW3u6#N:Gkzּ)evkM _׿Ɔ#(p͆K>5 t5c\}.TRe5EnԞ+/APh+sJҳs0Q}̛[gxXG' rzj&a;0WYqpq, }Tġ~wMe ByrDk٢PS[59ȼlpO'M 5i)Иr![v񠨕3V*>٘Sx@!5dv݌犑yKZō;:`:{{K8M҅|z`%b ֯;hHn80q}8 mgB 玝v<=j޽Clc^ka81l%+/yJ>ߖ?z8K#FH(4$L;]\~MpjRrM)D0VǕ$UP 9RDkKlc: SxBFFVK#%aXZ1efYs(߈ w1v~}.ߘPkmԽ XWU5{/ x#I@\$4|V9ܐ딍՘c vs1:F u,T>j\MՎs9Hwgqo ]s|Hagnq@ 0soۊe LLf4856d,If/+ o(].@3I)hڟ-Hע-^Hu'^mBד">^\Fr@ $W< BwkJ8V=!T2 uS)XSQkKxy, 3V~=[_k?>~ߛA2HNfe7Σy:p@z@#MW»|bϡ:CED.E|C \XC m٥}6Pq}yΤ$Y'hAC$7$g7XtpyA" -,G?sY`2:x?'PS]`TA6\b^C7LXX,H1labMEN9S̰DZ0g(c; &nJMnz AfN lo_n@-%_ .x֭-|e[6D ~Pki.lIc(˩xx(1$.(iqi@ `hodVqmC9> :Ơc Wp@iLet,56'qx^wNm+8 F]ZYm$c VqSEQ$Ib D$I HܭDnWݬ0PDt\|)#´ L}ןo`@@1 9B܌g1 "%n5 il>s`c .X ^37FP&(͍; DlKzS-d s"}UB5AES6Gn7k:X򴥓ҏ'K&4r_$y V~}y5ms$A$"͝ ?dOܳR=ڄ|S" A@΃[sF K8}J2ՉB')CB@"uza%֥͝ѽ,x-_$&A> Q1OZ:[^]EDyA:(-'W{Pl{ 2ⷻ/])_*vҼXsGEQT^rmG4$cLrqCt{[V&)z+v3oXoG#N:}\Nrʯ]M9ǭvV=Vd*[&jc#T[x2i3xQKL}%V!%tdhnɟ~<#cea;(Ad}DǾޒs+'Ggy-o8SII$Y.pVy8t/;p>MeÚG^N{E7r4뗮Ew_,,e`+S/\F"L( ;yڴʹ#UlFz_.z_&jL{}ء׋n7"uYQk)ɉ\8MTEr;Z^rD\c !Et _ןX6zIlTͧ|wπ- VU7^nJ  sWH-uh}(=6[1E٪X#Ǐ\)P(Nvjc,6&^uBo@(˲{=~xQQQnnnffYǎ[ 6*+:"fŸ:^;s2fa?9O$_r8]XUPPmtx.,aDu_. zg4sg>/6&_)&XyxpYvUGoTt3}798=7eQ:.\LΩVw 49Y^sm ft!fz-*6QE~6B9pd.]_MDD:ZI;h0/TK4)ws={Rzת3z;8;6Kn\HHɗ:x;Y rpWt,\c#jS/]t 60ZgLYYYaDrnJɯ_1%NM[sYo䁣!;K6~a2 ( _<{!8 ؅X_WU't tc#LI<'ZFʕ6234]n#QpRJNCH W%IYoxL(QXun8d<.>BΠJw`N})ndjs_FܯPn=;܉/=wr&`2'XMy&4^)BUs\"8W㤀9G, 2JySj"$I@.!:'7//'޶s}ǿLݥC7 ^OSsgrc]QYYT*1009E`{)/q BW.]M+ҹzښ;s0_r ƙg\Be9 a_+dYF-ζZF=(q6g0lYoZ7i4W(:sYwFc[])0~Kcهrp;5mdCza̛5Oc{e2ygYO1xWu^ҷhG;u̢ю( 'XbhG;юvxJh7ܟ$LY Y S>?6d4ڎ'mǿ쟋W}XDH-awO$&lwﮨ iG;юvh?m ws MÂGH2T*ԓ<"HTL&z?3g|uVVVVYY_M#a<_skNÙoW}ӊn{ IpL_ec&'E/+omIZanܩ-Ha42=>Y&N~^ 3.;mkSf:wV3"`(--t+Ö\_+VLGrd]wsуzDlHmԈ>ַSOK\2_ku YpJ  76c^Ojc)m6XoR?75JtOi5P}@ᫌ-_+#iw?\%_YH+ЯSDXhl_O`snUw IIuڪlu;]#C#KW #Wޞ:"̓1ni?=0]l5Io&O|ijO@lm _YA܎M~װ?ޮ  a年 |f@NaaAqoz*^u6oZƄGv9^P="[V5ҙoeψCRDXWM}!口:OMlaG"eਹ7ޝݼC#]bjmo .ia1=.YDEJbZ̗޾UjU99'\\<ܝ,-nqඃ'tj* o̬ pyfrJ~+ClDl᭛laV-oq6ukTvRFPx[KyQXfwҨIa+_.M~ܖ4âI7ӫ$hlY_p3Cof-έy)5!tq!^u'For r"]FR:RE-+1aڵ(JR_Vƾ4S?f4O\1hV}^qPǾ5T mæ-vF6Paq*j2*E{OBF)K;r`=Z)ۧ/8_LP3s+۳mχ M/KR}c_(>9oިjfY]AR>\Z٤|^йow{ N^M`kޑpwvNJЛ{{&(Gy#m6AтV h@|n⩫e!竖@}Q҉¾`3gَ]򍭃򂆪sgjA#K}7ֽS9SLOuV^˫$zn. /CǧwnXݤ;<?{FHJN%h7W3.Jk䊪{wmSۅD7ר.S 6pQ`>OKĈ4mRAҵo[&YM~!tݟOUp@cNz-ZG*`wLvG{_9Րpd=GdHO?- _Ip:w|~f] X|Ppq{ Hs(w F9hJ&s[z-b;kA$^[i׼i%{ccxWҫ}OqH~zvU cL-j8{+WccI5)M~ssMa.dҌ|/{oIU1=9ʻn"jr}@Zh#mdܢ.&i}fm6;'-l6h+G/v+$E*]5NfƈuIV!;䵌:=a).݊?RT'r\e xg8V7Deo&'\ed6'gA2]@;sL˽ځ5?'S %Uzi7D?9O]-.o*C?t4Inb-h8koF3vIaZ&$ KK9 I$u"{s|zۖŜPspێ>T$~[z_koo{ )"h2)u܋\+9Rj]ܜ-Jɀ ss_~^,WR:w sQohzv &uxG N=,-dԣK& <`.߮T%W# vb܃{( IDAT%k{F)koe& wg-q0q{%VUS_ 5l‡G)['Q)SW^XN>9]߰dǛ5f'lFKY*4S!&10fLY;ʍi+ʫ㘺|[ ~=wq |"{psAY3O!YA^)r"7>ܛ0qK]o/<]j7ڄxHן:t5Jzj=,,FVzb)R:ْGN=~yDenfnu(u}CckC.Q,+|0ӳ";'2c [~Pw~BC~wJ̽8_MYyٛrW8jʖն?.DړvfqjP˶){I^^6p)쨥 _Z1r:nߚcbM#2L&&8tT4t /ҩC\=T_ D8 /vq:p:Q>fx7/zQ1v"es#\v '%*@,d2 lYy{rG sk7] @!O.u'CBLLI5 QdΛ u_>3wA'e?+l0kJ]so~DDȭLǍچc|h=xV` J k -SDwbp/xR`dw:j‡;v&Bc@[$ ;*N_QYdV廻:UTV8֦|G$W]EY,h 8ƍieDz-،u Kp}Cc OTB m^}4\F{ >(^*%.99x+p 7+;ޭQl)ܛg9 HK3lLԲQѯNg!H[0݂hwT״J@:eK. a>Y'N:^10[a yB9Xro&.ɭ3(_jN"c~4Rv H 7 ʻ$L'|Գ b"%')na]-%{|?u pº Nٕ{4k;Pav!JhH i7ڟ%cG?cԏ>1} ZXdUM/7A;yٞk>~vF{7wܵYqЪW?wkũnCv奏/2 bu7IfE'Sou 1'TW @XXKRO_{D0w 7;PK'N Uj}L> UMU5G\󪻾8X$ Ҽ:gfPSW1e*XVٓ'J 30ƔXƕ-N?-0D{)Ӗ喨m*rjK_IJLJ}%eeİߛ{{ߛG~5y n_PEtA$A \hW{4S > 9\0aG /k , 1ފ.@ÒI.1b.kB^1Zꯜ9x#QDWG2 !c"8 Jj!@2g >O-3`K;֮;^'m)A+c7+K9V&âwՔH`)?aDwmV\o=sޞaOErדk+}x۵\}okY@RE=I+l!gͺMy8[K ao :R<A_[hh 7 ],|Ĺ3r$` c?\pzxGeP'!o셅2;ZePGYVo>3LM=x[K򂺲 [U?[2SG]IRa#9Cv A9/rRnKSvΕ6 %U~% pV5Ec!$PwۻUH.Z?=ôG}qn'PWawE}rAc>o܏K=XX|?VI5G}+dl0GPi  $Q<XD3U_Pu|?&ے[qWNDOfg[CCsUldXJBQmk <-y&0AʳYDH4nܔa%rWd>%b*)Up1Op0}Cќ$BjX ?ghMfC o *nS{f $Ȧ(w_x$35ڱOFkg/Q+!ֈ=[|I\ݿEf=1E"!p;IQ(X[YR$ρ2GkZvwOZL!Z*,6`ұ@Rtmf4nB-{I"eQ4-$)w+>ZnD'OymAR13!QOޒM ^xἐnNiPPzcF8I=_2!}i^L=NM@{ P]nl JBMO$ܽo5Oź؉>tKqwl=RtZ"{Z644D몹h>ˬ6\Ú词S:hԟC `$<1z}QTٞl䧶~3Iannnn&Hs  s[[5WcU s- ֒Z\l9o7{Z˹|[[My 1+UGxZggq=8CEׯ$mK}$8T2"w3Vw- Wپ㺡`ۚ;,h9ònuz֟>|MqĥoM_]]}鼼/8"ϖՔ=(t{> miHՌ3뙯ko'f>!Qs跻V߭K[Ao}q7yB%7d*_5aA;ە^ rg.\Ow8]WpuƤ~Y7Õ4<^ШW6mї~~ҬQ8,b'[3m^bW篲z1!e:Ot62*IdŐ%62CɳץpG^_OvVLJ -u'9{ړ\Eځ %=i>Dq`gy}1ƀY\' ~næ9Ε\gtO>N"6I tߎ9*i&9Rn*^!ϬK C='X,ןܺU?x-_hx+SchK_ea6p6tM14*>u? xC;4ogT~и>]zynyoV*+}±F 7k&AKgu;elp[Z`6yWmN_MΗƎ _{`!!fީ$K( Ϸү]Ü1/Od=kU}zKC]Q2~Ơ>"g/48R(}w|pzSa[#٥wBkv!{zdl-e6V"@$r[[^A"iOoZ[UV6c>*g朳}ջd{$jL3I$\޴{s\PS .`lcろdYlKV2~l$!$,x~|ٝ3gveg3G?Z\3R|7KȴIg>ʯJi5"ϝ[ "ߟ[6FUgsQ9&o䊲$ ddbX[[븉S}ݷrʖ`08k,]t-{{{FFFkjLiz4I%[ZULW˽%7ΫbD[||Xڹv9n4;0K s6Da#gOt?dKRt"MN/0.\)JL{m,pgJ 3JԷGU}]9ť5o] e%;;{MM8ڛ/5WjFNI~&g.r̖aV6isƄk7hwq͹%ř䴢c^P2,KܟZXT?VJɭ(: 7L)(H(7>EiFԽg]US4M"r\DDZzy/{W|&s_Rڰ`R o05)mIháxNiUYnj 98h|Fi%9gp4<1kLII)KĠ#==/9'Yvyʂ+ot-pxjva:sL=*K 3i5Y^u_yyzc݁DRyy _=D0'ŝWSȓ2:+i9eecǎ.{f/p#UM; E@QqISrI'-J8=t"h<O^6r\yEYi=IYEU^"*~fP͝<~TFئnv_s묜_ũ xYwPB^p]; "9ǖx 5r,7Rq͜:>w2kQ"} ưbѸ/0-X=qZp{uȉ۵6e|(]=o}7,I ii/%'x"W]_^U"?3B?t<8=;ɑf'P8t)Y"j*ҳ3ҊN?g5+_$ń';,C})UyI%ei|μWc^J3ao__l-tHJz{"x͞_4mٳg'vpG0|NIi 8dCzo~PHjc9){#܏{ Ipn?ZdggSO̓) Bpw!`81 ?=t%IbPF^q-"x<#uh[KkT2Ƅ'wrWW=-GzAҊ=pCGmBٹ{_~^NZJjw{sˎް:1ֳ_ᮇ7J 2\nO :_/Έ.}.1r{|>mpFD\.ϫbNmo߀I*%HR߻>))tHIq mۉEW4#!lszǝNGK6]3T$U1L;wdt˛3!f/9EU?JGbjxݦ *~6%}޵ ;̑ǖysnkjDe(N1j GiYY-km Li, j&9pQ1&P8ʉGC-)%?79V3/haE,OHe"p\ecm G:"&-'wlL}W3URwsΞa魉ѽmY̛nx/o;" J0{׿iR )LD*Ԧ | t9wXk:QuGm#II}nYoX ǝwHF{tR]r߹y +5twr0q8~ޙ0bWWm*aR\hұgfP5yS9qo3%+]_Woܰ_./k1S֛sUB"ƻBCy; '3 IDATV2fLoCGgiR)8N,++)RM[K~?㶳5$7}.QŎU3bGb .KgE[%xd'lImY;|%"sDY_{Gڑ;1bٙI˟\^zW}UAbmG#qMM;{5ރ+G-;:։BwmHO9mbӡ#{HIIͪݽu;$"!],)$E's6"O1\MD$Q>grOUnЎEUhr%O?ӫWT/O,ItɏvO_xea7D1H:Vp~஥J+|BI<'5Q gMM9y˶N=‑vuN50+%MDM1näۃO*x wt#R<N]x5]LwgH޽g?FDԩS$cԷ"N*|`}OmI>*hL]]WͦeLK\/891aٗR[|zgrԿ.M3;noG›J}M%H/H)E)n3Oavpp`J6ul=VWʄNkmWog^IS/>VV2elF}uqy759ɧ#GI\{mEEE_K0;vkH$Zm3ժ˾rѮFMɮvۗrɅƬxˁmg],T}ͷOtwnlvoL1=/O5oM vDDgLZ&_U*EyN=Kܑo79)chܗ=Utn|Upr OL0([n޽Ǘ/MONͬolw잮z"E]2#2bSVq_4M﩮ki)6!"R<"Xəc+wm{!tc^i3>۸&o%l'ɗ >xp~nx\h8۷e]0mmmk=Gpg&X IG7qec+:/[6"xTV5gd7/_i=y95A_)D퇷Ym}ΰ FabƔbG y4n5>N1ޏ5{_ض# l{i:L|ŧ6 h Pw,s=zMQEל6ܲEUNlw~班lڷo]O.Xe };7X]6K}j5u5_ncw(Omڳ{_{7%-c: gS|Y/1Jѽ[{lDɎ;D**.ҝV*zwY!,p}*_J8GH+NfT)%9BF:R+krYZώa.ޣ^vΜ;o|ZOMvMΟ7& :B)?wq3m.9,,qN?gւ-wVpͺ;ʼnu0ʶ^8\yI娑Fn!I>Z])%.6DŽDaKgL)bsF{^EȠKWKXioͫEDL$E:uۻ*&ϴ=sS}h̝&9h >bKO$!5i+`H8V*XaGen~k٣W0'|oKz܁# uFDO9pef0HON"t@{쮜3YS3vRD$|9>_cwƥ(@y:n ZEtj=\$͝:Y/Y.)dN!/N“sW?m:ξv#u,+bC}bD"qY;XOqa.+;o=ߵ!3p#D,l[6ur5=zxgxܔj*3%L4g"rB]KGb›D8|E{zJ~9c=cEb Gqoל+i{CK QN8lL hL%hR{<CI1G K3qM Y<4pf%XR_*ݹ}э o%OHx wp= w&""OCDJ'ygO4'wøHN!P;Ä?Wߢ"3ʅ+ԿO ?ߤHSVA)|w!`@pw!`@pA{s:cڱLZ=^Yf`Ŭ9U>G5e,eι?ol:_\\)w~,=YuQ]+:5&9g204k ""%-rbk.8#i9d=cxϜl $Cg'Tc@yW.]\z/mkKo¼T$whpp6+2}7"s3i.uis.s S'Uewl)*1Qstybwzmw|3f/ ֮x6) G-&`L*El>weYA2dzhz+8+GtW^S^+`zʃw}7ߚMe3q!gD~|"MRZb_Q#_%Sԃ0  C;0  C;0  C;0  C;0  C;0  C;0  C;0  C;0  C;0030\.CW4ah}te܉>]ݥweێm >cum5YOWܕn>z_ qMF;65wGL"R҉E#p8w"RX7b# ‘iΖ#vv1Io/+;{CP8$D,k%)i{CᄕFzC)#FzCeBP,H9H7P kiF^{c;W.~t!/ܶ{_<8o~闏ls'~`?ƏG"Th{Mg|h6kبvKG|lO_~7x_!mpp6+5U>&_m,}nٖe ߝ{v޳?n=џ3]C?9 _2'K1 5)m_~ 1#:+oB_?7k3|7.W^o4۞Kqwz]:#b;oXY2ܕe+uo4"Ұ]KEtgoI1욅ovOr;x)&|@0&[ W,`\Nosg^paF*sJe%qϔ'6meL9v%) vuQ:&gYuDD$YMAv,na>%Q*`en0 PJI)Ƕٝ{vw-k}?_㱘HF._=1c6 J""bʉ49Sj2RrR{:X0rXFfQDw[GHGϚ5.߳yݚ^}u4D"eŪ"ƌqckйK'rKGwٹ.Q:eZ.pR~JL*3h5fẅlږ+=TfˑfM;L?ecmm&NllՊ=e1XLJ:#1&)%H)۲&(4McD?HBw[&ǖ6|ѷ\hȮ|nF(ӶteYif4wJp"x4.%Ts{ȧ A"l81JXJS.8.vGS*3#9LIKfI1wDHpJO1eG}CGBIƄb$'WwŘmx4M2D%[u]$w1ገٶypG0^dN7׭;x%"nL1OVSrh3K7i_<_&4ncJq67tu xcG?cK0Gg}{]Q TJ1|{x\Ѿܟ˸[]&B(;?&mۉx,YӵuPW .Asq]caL2Er46>c9Ew[z\w{_Sw[/韗&R'ֈ}/m>5gTe?42Λ7wKZr zvQDjiF}|f?הSXb\葦h̢QRv>l'y҂G᣻6~vS +Ex"af,okm%iGZ|rKes84 +>5`x%J̙+I&[GRp+K4F̢&7%*Kk4G*"dT껣R*FWdR1Q1:IHǢ|U_|k3> ;U+ߵ{#? j>z41FUɁU:g.܆ 5ݼnM<⢫R;W(EJI3)%1ΩqƉLu##)=qY_ ce^#c;/V!oyn+;7)E`.˲rgҭ+ +۲uk4xLqRcu>ލt'?R헥tTuA ?ǡysH_3iU/"IӴ-tΕ #I Nnt]+)+njh2tͲq&خ2JiT mXB,c+h))iqN3S͘żrqeǥ)G7\Bщ$Pwgkk[k[gO$y+rVč#XKiqLQJy5EL J*7{;W-9Hi,.% C 5&HR ܹPk{ൣE‰+O.AD5å ȱLrY]ƙrlEHni+ q&4-vTXF:|wGJFd("&*+#wG@zfRwer#L)[s T#^[%(/|ʣ+&FvϬmi=~&L'v<]0AO4?zsFFK nr`:\ţ vlݶIM 2eG[Kr辺d_SG~)I%i WoSSXWy~=xfMZrBc Ž-G7+<}?}s;|ޙqǣ1[{C{v>}Lc,6aU@iahB^4"KOBp"G:R*i̱S#1!gr ɔH"&H8RиOݹ}э }sO,ֈ(%9iꔉŅp1FգcH:CJu ۻ]_">X΀='}o QnNG۳84B#"$l0RIG#U|H%#===;vZ3^PWSS>'YpߺuW\SV^!%`;S W,d=gdfs.8?MӼ>c/Wp?!zJ)>r^1SNGӎƎٞH|@tK/ݭzMMM7TRRO^=t/:Eķҳ[́诌l+,^k_x+}n'L8n\͍]|ië;;A>,+>5$wнK̃>xϘ1222~_,\愃|'xt }Fw(WNQrz[nӛ^k|ry.UI%$,DK†& t(h68L46a df*T1+3rzoÙks˗CVfI{޹~Ywʧ \xZίϛ_7\ʇU{QRJ)utby̙۷{_UՕOF\7._>KϾ3# ş L{ '6 _ }~GǏ?@{MoZO~< hPpXy|#6omQk}xcN<+/=C#w]yVJ)RZD0z[oy{(>|mua0;gĎW3f)g;:wq{!j宔RJ)i}o鉈wuׯYXX?__z߽7c}79Wel/vxO>ě?{ՄEiU~/п_;P=qrqVyK<Փ#kr&I;KrβZ+RJk;~N>W[_='>ϿLӴ=dW'o? Vx6zS^v͙KrnxsU~侗>fnt{G|8G?w[ہ$FDl۟;L׫rmm"{V)RJ}=W O}_Ԧi~~w&_2"rgw][0N$%?_޿ks'?ceRJ)zURe*I__m1 ÷:;,s3su{+QJ)Rp<~~Z7(BX>tۯ|RJ)!"~~݃I)RJ]sӎ7~sǝwMNRJ)בDDfN[IQgm4DW-Uw;s"+!4Kas!Cô:K"b)˲;t2VRJ)vIBt{Wj MMD1K^'F!Y ""x67z bUգq0?kD\e˲*r(Y~{Y_D:y=}sٹ^m3DѨۛY9B6qRJ)wizer@ ssmѨ* |wv<5Mt̳leUCy Ū1Y5Yk<\X%@SWsu]f9"@Ax2!,J)Rs$4u{vd!A+"PֹdXc&Uav.-. zA'"1F !& =vi clۉq vLh}mufv9chMbcm{F!"&I#Wu0:qܹ6+RJ)Q"j*r)˲Ɋt‘4 O\5ސ$Mc4&:|zYC!UEƓ`ow'ETu] MRC"#e/sDL\[ ,w[֚ɪ#70Dl'RJ)wIn rYƘ4Mn{7rdى,M.@$kDd1jWJ)RsۑvW"b \jkҾM=VsK̶S>n & b{3`8.~c,_R)RJoXW)ܯp9wyHÙ\,[;?:SjծRJ)\z6\QJ)ROJ)RJ}iyYsZJ)RJp󑏉 d-^C{ G.)AE"Da~72 c?ZRJ)Ե`?"""LDm푶gn"& !mLSS%؉*c2Oe݆m_1p(1ˎmm1}{ Aے+@Ch ;${R)RAc 03\]wKgF÷kᮔRJ)騌D ("@]Jj'AnMQumYZf^$a_uR}xhT`ZsqŪ(H9Ζ(^>{ksݍQ a[;w3^Z+RJ)u;2/ii"4@P-͡wf 8!h8lr33I7_NX.F$ 3y x IDAT')`3IM82{cgw0n_%OH]{t|2?7w6l#2\^#{VtՇ.jx e_fvNh wݕRJ)upI%=fL\ 9k$Ҍ @??>?\eN#{rs?w7[/l>E3t)?>g6|]J[D KKmDiyt"zi\oKxJ)R[Ƌ# 1 pdfP~rrsfŃfz_;z˙ˎrfSM&em bǐ4y3'KK^ٖ¥ 7Zy&ҭlg1 A`"d! 3 : A3Cݢ4WEs?_aO-F)RJKFe!IH70xVݼK}kg'~³}lWMjޱB1 y ;\AM KI Oxѣf~oT6>w~RI€pn",{Kvkmj/|qv]zՆūLJ)Rxpw٥'CD!]jIfnqks0A!v&YH(#yCm(#AmloMB"w(!g/.[!`i朥{m7uo= s}H:5X_ޗDwhq>C&U)2";kffffΜ9[z祵)±}TۙvfN=J)R:50 ݃, ƘA@@D4A"^ |Â"ދh429:spy"[z0?;T&rA33wٴ7_|2m4GC DpRL.(SfSS}iBZk$iu_iZ+RJ{[Զ3!mߝwcENL"E\ 5F"ru" %Jx u] pL]~{{kq>JHc,mPqLKN?dLojaߴ:0V{٭ËA^".NUJ)RnmNf#^"_Q@b@hQMhsaq!I:EYY`&2dpRP%.I[%)*=E!MDL ͠:y5)H cdaDhۿ^ divK޻CLh 1Z|,J)RJ]*;mi[N] .d 1c676:` 4L&[ YkB"\Ufi@bRk0Kہo,_|E) ܍lc#ᒚ*RJ)in%.*@h Sޭ_LYi@B0BAM(&$cd@}DH_>ԃߙew|3_6bzAࡃӧH!2;qD3 vP@.("QvsuGɶdo JKSRJ)53*#o߾M1N{{ "8)B@t<9XM !lHX Bk;v|L,i=Y 1n*;y: 4q7ֶͤ>}!("c,CƘBD04]TMD`F sABȜIMsA_cv6epI RJ)RN{V7͸$t{U9j;,dxg.H 0 0 "2HUnC@hckʗ0YK, M_}7-{{ǏT51_X]}d@f@%23CDd 9qIUV1xh[*ʒ0 bQ{vmqO<"*J)RJ}A "sXBkhA$EУ0G1ߔQ@m Ax-!B+  ud،BHpzsȣOzde~G=w _p_cϝ" C&gB 41L_`FĢB$:Ur қu{3>MH"G<˄  #,%RJ)ԷTvЭ  `fE Hƴ߻S5 خOj (((\x`Q# xd @'O6|bxϫ^6VϜ:41RfLJ 1७&,n Y=DK0vױJb#XEX{|q4CpD&đucJ)RYL&cA0 `#( ʤE4$YvQ1$` C`!hP <_g>O5oz}s}*<؉R`?{~X>EDtY#_UF" f͉Md,$s7~<_hȆPCGTRJ)u̸ DF#MWe(ί  H,qkC8Jhkq$lX:Yu);Olo.aM\'x[>:,*PgfMuU7M4@pr86tFI (Ƙ҇<ԁcOĦ&@}`Q{3[CF"{%z8N{6\}Ž~z[p˽75!|so}=ggVdBhGlji23G 0!fYzr16M%`"ɘ..Z9EKj5\{c`LS.`YL33茻RJ)vvNEӖmqڎybD@,QPАmdY= SǧѲ5b]:l&/f4뻴OM,._:$uٹALo,-#OCy=u9Z]  F 1!H]L1&u;iOqAQ$(n^X9w}L5 2 v,Ynow:}/I?ARJ)-Y!ĊE(@uج69J & &E1)MU;|S֟)V֊bLz=4MNhBd47ڙq6QE$DoowӸȿ5?˝vwVun.Gi`#B0dc8ȥ`fyyy<3H@Єf… cϞzox>R L%B k4O_P)RJ} h;YRoB, I 1RDEUg]h].ܚn1Ε@#H\J*Fd <3ab  0 ‰ز*֛^8C7~`o;N|c;h\fm7KD`fg)elomY'ZH{B0cCpvexȁrX@=NM 3?wx7:㮔RJ)8HJ:몙 Ch L5;@~f.na:d-115nuf{{)@`u£Nؐ%IEFk :?7z 'O:اΜ?:I'.0T r0GF(ЦWUNY!"FF  'rI n26Y/RJ)L $;E]`sO ,&]{7םYf]M= ئfa0hr//J=^8uQQj{kK(Q!$ "y'x6yvz{ص^';{~MɺFK^o,MAdafF !JQx@DVV{K$r 0P@&`.mtmKƒwRJ)uY@7s Iif.uf\zB! B`c[8'8xܰ47QA/8aњ o /MHdI1"F(h ׈H2 mm3QMlJ΁;NCE$ƈ " s8|afM9[f4dmC (L@RpY} CܶRJ),xWgvm'1Ylb,1@"2(ZZ3B/1.a}gֶ676ä6@R@ط3IC(#W( A DdgPo0@aeY݄޶#3ȑxw["b$qFssd8ZkME"s@0n;mjO]D &A*RJpo{%@Ia2ѐ!Rn0b$t졹žcϝ:1NƱ=waդY fLVLA(:Yw09,`qnpٚę !lmmood61l8I[" 0G0Da6]O?):1{왃dYfn̝;{vaq&I.$  ]=¡RJ)ԵgEH8Gd'3i6q3k֟8Ϝ_Y c,E_k" dy?ٿ<0?O~.Oڹ+&fR(0x0;0coijNq!L. 4ckF b\ 0a@L!rd&t$| ^8f}wA5J(PٷTUeo#[[,Q ""B nǝ"^5PF)RJ]m4\=?u[nZV xT 7ѶF$kL;\cc+o) 'nI3ZߨW}iD3.QvèjbP"QAc X"X! ibs#"ݵT$6>C(zbB+?] K_xr<A<֦eQ cB ܮ|MWU勾:(RJ) f7goӏ| , pYI{ 1qL.,'O}J]y1ٞ_\&v2ױ6  t G! 3-A)M2 ;rd2doX|G214M` 7&1u=˲AJhJ=32yvRJ)uLGeѹ>c8/8lmnAl>g.S=&FTg*@Y؄ Diܭ9㛧NoYQ0PT10p ``fB!skm]&"jTvcӆ0C$ɗܰ>oԏg$&&U}l7-O o&[,4tf`2. fz`hS)-RSi=~wƟ/.#/Iلځnw,X#'B'1 D$E@DbYDb愛, 7bs{iBqL tYPy{D3hvB XH@AJ)RJ]sm]8?alYI=ƱK6R^pGܬ˺[YJ- ,`lB".Pp$thg/F+Μ4e8/<4a[B0u&"="웝[ `7>3/?",xGf޳%vA˷[JtB aRJ)uX@tR#>I(&i]Fޟ'&&bzήTfb nmfn¹zx?t5Zcv5|={}*ʱsg`*zICRfB# i3"H$:Dbٿ8hyylvЃEO ІԱ'~ם,lf~6]wrkaZǹؓB;"qg]d6nAd7his4E{/?n޸[ ZX#̆3Y\a`$ Mv]5\LR{=%I_O÷NMW##[#YX)m!*l$BED#VJ8yvs_yMk065DUDaj4l ,,A)EHZkE@"4iX$p8x-\e@< NBSΗq 8$h|8Н˩ZN&;w\{mgs<Aرّ 6( o-ynףO :HlW%Jh3K8B-溌/9-Wv=}8M( |eR L (\X`Q#[F@d'DhSEZ ,RHzup8 2!bCj?82 t t_yCKre(6 {:s7 ]cL9Nd`ݐy މhFXbZT*}Ežjb4l@ *blQih{gdGnPIҎBW 0XƤ%h %"Xˉeu!q@ NeQ'es8ps#;(8)#h<*M@97|7vLTΜ{𡣓caM%} ^y!8>g?c<ݑlwq_\M&9ptd SO&EA_+/J# %n3(Z89 |GF|OaRˀ@Z5f6x*P*VIʉk-ƐIj|?8I_+p8gIU=,V*LllVN?tmp񥩩Jjʇ/;;ݰkv7ţU8=IZs=cGOQRFeD( UB ^ :6\{A7DB큈I (B(aT-BMT1BJcjj>ij׬;aP˯%s8ps$bT_zHleIc k| c'?hX_O㚫1w3?J+35=6zeN0;}@>  6w\K&oٚ?P G*S`3I0'9,bbe^d )11y_>?85356 ŨtN:p"*VT9TA)D*WzAIJe`bgOEOl(p8Dǝs#vUdvX!Z ˪PMpmwM\:N[5v.г&өɁWKlU8UlݺAU˵\410_sub+GǸUXLL")7%ud,҈)d]b3ҐMI/tm 6O-0LOv>j-N,0ڳ ,A/0L4MHYqp8q ek5N*@{)է[/=5XWqB]3SPHOU\CD  biPVBZakGw,'f}n#c{ȞQ, t*Z0 (}2!vu7\~߿FF5JbQm"FĊ%AB@|d5":zjg#_zC|71,CRADS60)ImNM7꣈"1lL 5|cqnvvR `bQep8bAp) ??@;䡱re%`4Z"T!R 2Tb `HR", 1xq \uu7^vفa{bVJ!*bT؊Zk-YD$dD`# P)ZA$?=( HNT)ɩŇ.:;xZK#3: r\uDB`&fijQD,#"+c E"eF*X6ј&$g1$"HJhpknk֯ r>yp慑K&) p!bm$ e%AA&B 3?r`JiOA 9<=Cvsq{^>xU+Vse 51HE0Ԟ1)i-"1,ր0R$ Z\sssHr"4>5׉EwN Ԝ7[ p#xz0 ]@9kO.̗g q΍۹]:Kh t DbhSLZ+"DDD( H1ZK.s.D:R}/ݿ6q{pb/0zh,eAD #5Y)SSSDhL=ec*"1@ 1#jP*Rztd/92Ȓ1B a h8Cʂfni}뇯*1.YpP_2ߴ jל6<~62/NdZ22|# (cKn%͓\^Yyw"\v|[46]*E-XV9" ګn+vE^W2:A[NL5_}O3l/ՋGAhUnoW\-Œ7ǁѲ*Ϗs鳲h'e-=Ȳ6vs}cEM[괸;Yx嗲z*]v l *GMKR#Nջy_jp)0}Qm-93 UڟWڳf8[xCC7tyEHDIMQA'̬N)mU$0,T`Vd)mE@|p×|l9Mԓ X -vDF8-uep:S,vfm<7T#`}A"ĺ])stABFLR_.xqbS^xK%kX0@k1 Mz2^b 4/Mgw!fCD$֊p voYa*a!-26Rdq!ɩ_7v/6.y5c |fX@\LKO5I*mTK{T.ȴ;lAbm.d+|ą6[KtpUWmm o-}jN^eTtnwQ~<{rj3_@i(%N K6|EN? /cXMYmNڼ}JyXuh9y(lq6@$"ط寧T,3 Y8Hԕgs6BP)hVXP"%~ZD6+d!@?vm'~Bm,K&eg@3i\1*ڜNNG&M˵Jhd  ٲ@E"dQWghJre -[zԒ$a@$+@HI`cr֚ <󬵙jւTjX;JW^yss 3ql˴&aKh6 fH+Geo+-"@X0BJ|IP(Vh76g[l鎬'Kd VB-z+-;iD $2UiҢ]PzK8o_E LUuA`K, \yq渶ove?UWo6'e,,T^gi[ZtgX$ȢbdQ jHInXtGYƂmA,N_ r慝WN=e %oخ{q} ـӜ9,rڂS |?H]Ksh 􍾗%8ޢxE)Etl9b3]t7xy"] ܁,*&U$E<-dT-h a# €((4m=D[ M"%vvvbϋ5K-YbkmnqN Z_˸mO9m&վraTo&/ =!:۴Uf?3}o<ٝӮekЮ4gM4rS+ XiYBҾ6iX܀ցEӂZҳM- d~j,7iRY*Uqsnm[HOt/?8 @'dO ]R9 e}fF@{qW.6pX g , :-0x,ja5">XZ߸<7 =Mi$pA< ׋zrp 2yZVggg?SQ-Qm`pM$I(bq;: Xq[tl!33SbQHL+X*MOO]p8$*KkZ03V\z(!wqGZ~|p8Lf"ȡ3'*v(uv:3=z',TqV( B>zp8Ǜ#Z>3;X|.~a#,1g.'Zk֯rzYk֮]8|HY,D}C8@a¥GMݳk(A),Lc: ƮyY_~a#H7hV,.(W}(r`q<{f?>4"56:y|np8D7{"Z>;ޱ}ۢu0jȡ/"Mӷ햟>CHǎJ%J{Ƥӄ^# a}vaa+F{Vf\Pra (2zzlѦo3MHSS.+ %gZ5/heP\p|fº@o_/`qf)ӏf?Q;F-60jJAmkԈؘ_n{"jp8qȓܢtl9?sL )}7+91]56`Dp8,]tr-bOk"̇ri$?wow/=OR!hYg5gƘ?r?|ǥ{;rGs *IMfVJ=]J)f‹3 wMr:55ZB."֢45jiƤ8ijY$I%Gt;1Hv? zꥹ;>| |j\]~˕Xc üIձݷ9Z.nCw߲6S~謹^;?tే]'+>e@1FmjZ||-F ĭ—K}>yosx"gt-z^Q=p_=|욷r[/BRKM釞KoЭ̅7vp8"t淬ڱqpPϾcl]itt+@:kI#I#yYTX9Km9hH$&Qлcc~yD ]&)U^|eY)iZ'" ]89~Z֚T*0sj,,V,.&Ґrul"7>{cNw{_<#@?嫻{?K?Ll}pHōw5Cz MafkIX ƤB&5HIR{K7_;5z\;?]W1q|x{%Gzy8O>ww9&Fe`,Z43|l{_;Ϟ}i];\ܲr@krp8E)^cl/|-xF$d;4QjV(l$~7|7nFَugZNGw@$R΁&J$STJ=9RJ3d..f6 DilJh o%zo{{:/yڞ/r5p,ow[" ֫'v>$"Q)px WdfdѰ[af6k@o/d(iZT =7|ܮs69k;O}އ. $BXhM]Gv\tE)(LP43|EUp8R^יL"2+ʂ\ZNO"\Quvv^y}7u)5ɖםwӦ_~f*E_DSF+I uWky;l⮖Db% Ik*@Ռ9ئfF(T^ZQw"$BA0z-kW1 AP^{맶ٵkϗ;?/-HJ=%@\κG7|]ik~G.̱]OǷp8L,sJ\3ۉALCn $H+oק-th_34Xi&rjlZxPaZ b}2D#LJHD֬nj}$ `}"A&uy/~k逸^zINZ4]>ul}zj*ܺ?3|%\Am:_e0s?vȪWY0 csMPZw;̷^6j0l}>viCO=tUChT6/x۪kvlϰw'~ x#׿cCsvAtù-ۇ0{d7i -":z9p=G~IjH!tW\*]yE\߃cDPE{87=bf VDHIOF ~& E e%D20swOO,uwv<1>uȈViU':%I|&3b#нg_3w J[}G7vɉfg$+?p}sB|pu}zؿL&qB`~)tY]$?cG"M]3>g<ɗ_>n@= ՇwMD)z;;ZTIbhxhmI]FT=WG{}{~\?;wؓ?E Pk,p8^7`.JjI RaWy)Hŏ VQ%[Fww]}pa]/|wyWl8-xЙ]ۻǎ \+%I=Ӥ EdvMI7A4i7b6;q&IjS$6x+4e\[ֿӿ m,woք Wi6ޭ8̵,(aff*hj6 QT?x&V8FlMwMb74ݵ>9M'jԥ,3L-6)\?yc#(,7}+~U%5Gi)^@Ԟv>p8^GnjcSeֵEI7ת?{$F ?1:;ޔϽ!<ԓJrc_hp6TУkVv2J󶌎Oc_v@)Z"c?:ydj`P4VE@6Mbc( Ic[L㽿|x Һ]znmŧq<kZ#r@0q`Q9 M#?{ϟx|BiRk-ȴ~ĭX5ְ:YhAi|bֵAZ=Mڔ( Z9Wp8lR6׆no/d$^.l j( tʟG_n"zss};zyb^DQ(V>hdf(/e+NImQJAf_$]ì_p)3?xվ>oR".x@7^dIp87]sǙ*džG>{57"vvvZ% DRtcNN@EKGQWWw/>mPE.Sd~(6+9b]~cZ9uVp8WRꩇ#gAךןR=+# )r|.ß#'mъk FwS" +ssw8qVIZ+C@ 9~`fRtUrfIENDB`mopidy-0.17.0/docs/_static/mpd-client-mpad.jpg000066400000000000000000001672421224420023200211360ustar00rootroot00000000000000JFIFHHC     C   h  j !1A"Qa 2RSVq#BW3TUrs$45Cbu%6FG&7cde8Etv';!Q1RAa"q2Sb#B$3Cr ?-=ᆖv1wU*< IvV[Js-{,;Zh>/Y5/SmJiD8J*@gN={An Fe&DI>G'Hr@A<[qZC RS@N;( T@@@@@@@@@@@@\!TUc>~X'_Lp:w且su%J!yWۻjgx>J[ڟlxr^pn. `!]PPPPPPPPPPPPP[4}uѽ_%m";k_W %i#i*Zy]?{l'd"PrC$-oCdASǬ<3Ϙ <%nm_6VTɂ年Pێn`@ek- \wקᛀE..xuKϤx6y1ƑHOX!kV;ԣ̚aM5<ǩ{ m쩧'Cqn<=e*j9:Sp/SNN`=x{6Tӓ87+1ωo87a|L}4vǩ{ pe쩧'C=M{ pm쩧'C=McleM9:qnު𰽅[!ҒOEl շ-|>p5m.|ҔDN_geE]ĢVNɗ=Gfe[pgٛa{jǃ!iei)\DyraO JXgpKC:h aJAT \,/alշ={>3z,/a7xswAZFpTbG ݲY+=8{6T.=Mg,eMO'Cqn<&Yʚrt;1߉o؇1߉o87aL}4v!ǩ{ pm쩩v=McleM9:Sp7SNN`=8{2QQCSp7E9:qn=f('C=MgleM9*Sp7SNJjz>&پʚrT;G1߉ort;=8{2QNNbz~&Y)CSp32͏?N`=x{6Tӓ87aM4v=Mc,e87aL}S؇1߉ort;=8{2QNNbz~&~ʚN`=x{6Tӓ87aL}587aL}4v!ǩ{ pe쩧'C=MgleMO'Cqn>9ƌs?ܢǩbww>Α2AOX! N{ҡղǨ=zg0xyf#KPV!ږq֊z eUEj [[%tԘR̕\A$5m%v"*2þ $_%ohRZזQiNjĎFMyQ&`>ء稔"ht5FYJ $vk+6t63?%k~luM۔PȤ k9Aǩfнٚ\byVOfu$W\UZ-va{Kϝ ף*yi3ݧ]ʙ|mҔ`<<ˬГͱuy-7.]uK*V|8=sEBuܭдܔ^tUMRlw+؄2+ӼQ*nb9Ȑy}V !55K]M԰?Y[n2g᝶"mPƌK8yK䬣nW6GkMhhJxeJLt>Vێ%G JڜZ}]Vg%xX񏦖M, L}4) ?*=,.ScbCc`3KXǟ3!fs鏽LY[tm#mYe)$|Uz'QO[8F1I cL!]k6_ZDG70U=CQK%lYa_{\,mgUO/Kk}sw~qfkz'F9z]߱OkswƝW lyܙm2w+ݡ)T%f 4͝К{Bqn|m7G9v1-@ڐwUYc+/# BzEiQii< +) (0RZ)B>4 }K My98'~U)ބу6*m[`f4UŹYJ#-p@"v"t% -V~GNe/3/l![?2\~DŠR9$s#d|?Yt7m{g5}K'R|?UR: ,FwVh.joW\^rr'_9[RJ{^cֆ15Od▢h11Q^2î_Z#8ܾSKr[T┙ H)l[;Zf˲G/.uڊ;T*8-Kq%k m5>%@c8{6W;~4eƃS(x{e}L ʨmW|~U,IiW?+I-PI\X<U.MRq`TO\h{]?܋Ǿtx{Kue%oݬnžV'5ImvwM6C qN:q\I'5dЋ)p,.{j]C>RH"EŻ>X,VѵߚmP B +a(E2^'knQ}&;2)0%1姙BPyVkbtݕ=:kJd7=ZnI8t$gJvj]q\9m̼ܞ[:rIr!$] '$=ib?륅gap񟦖6F?te'`YV޴`yd1џ x@![w/rA9#'0j9ܞ>\=x< MAunsU߸SEc] x))Q$z{{9Ӛ j2=*W5_>|QӚ jjw (iW~O;_aq§ڃڝ¯0u~sU߸SANW:m9pp?_~8TP{SUFίNj{*} ?O_~8TQ' $|kwq§ڄ FiHә 56KjSj2T3EhdʸR^Hm iE}X9=܌F">'RG~*>F|Os'--M~ω~--mSߦ|Osilo*>3{mSߦ|OsmSߦ|OsmSߦ|OsmSߦ|OsmSߦ|OsmSߦ|OsP8;Pɵ- Cߩω~ZdupL/J+ߦ|OsildSUFίNf{*}=*W5_>|QӚ ju7C>_OFcMBCrb8DYP%k↖}z\?n|ŧ* l5^bjnxUaÛ(p R7KJR2R9+Ԝ&JqZ baQ^;9B$-Npq^7~5zgOY$:Ѧ*ZáBRT {N750ud|kEJ ʰyua8#I9@xy 9xq=,ϜQb8,χ#~T-Yhǣ*奃?;^uR?ݳW^LƟ$$:V@RrQQVF x},.War|>sAp4>zXǥ~p|<jes2wlR JA9+KNf8R"6rҒrit(v`c8^+_[;j].ƥNMOoKo؞=. q˃ -XY6HI"'߰FlNyCTz9n?vr3}%#Ӓz]=%#Ӓz]=%#Ӓz]ȧM=~rX~ÏKM=~rX~ÏKM=~rX~ÏK󇧾܏NKoqw 󇧾܏NKoqw#˝'M4ۑib~=."ܺOpiIvCPᲷuZȿMWŧs6psjVU)j}?Ɠ(Z9Rڎ@P.yDH|wv*J:2S EPd $CGNZ QR6NAPܵ@(|:25/.7Zs:vDJGN3v$e Y WP緡+I>Ru=/rgGHԞm27)=Wv{(f5j2my6R q݌w#RN<NY^]3|{)%B|?7aIŅ.uC-J@ʀI98i! *K)mIA+>{IQ|Hgu y(RT+JO5en28IUls q n*igً [:ԡ!*OM4R莞7D=f^w+]c:HH->>Yȫrx~q!Q='Ki#X~qw"q4ۑib~=.q4ۑib~=.%j~Iơz8vĶ@Z hi\_w)GaLtVǬ"5eq\"C%ީG.40'i¹\\_w)GaBۣvňUq.RTW%G~rv2<^u [ Mtvsًۊ[XB [#o⸾RuApXfLBRi\_w)GbNu&$ԷWa04嶞K[B6OD9J;#+ m| ߑcq}ߢON;k^.M\;N pJPppAǦ~rv3RY't/C~ ~|6O~+(__ S|W!Q?QXt/C tSn,Κ~%]昊3fghOb9zX}z\Ih#ȺۻϷp)+\t=6$(@WK,z3١_hYZ>k[= 3Gݦyn,bGi[-ؾ>}g`/|,ϭv帲= 3Gݦyn,bGi[-{>g֏LYlŶ<[mf}h-Ŗ} 3Gݦyn,bGi[-+}eh-ŖdZ+9N[Il5z&ʹ9'+sH׋iD):5S X󚛐+i]s[Lw;%KlDe'^zRS0;!>0s]9uâKv+CqBR8ϧNUk 6G'-H fRTdff$zIд,!'v+'()Y=5*)vR>EB[[F9WR v39y{i^ɴ;PXʼnSM3TPTm*#$nZY*MA1rIAnB O`{01S(ff <[ֵWR?/0ʯr.\PԖ.EH! 6`˘j"Qxk:Ӎq\qR2 2<˼S"n3.?ī_wd4bs@ DDsHK)z]RV BvvC OHsV phV*Zܚ;-'kN$` sIl9pԴ/쮵9ndPO _֏W-i_bGi[-ؾ>}g`.|,ϭv帲= 3Gݦyn,)]hYZ>3qe_bGi[-غ}g`.|,v帲~t}Aς;/] T^jp(yHQJG<4'A- (Eܫzܐ-}ԺNY$`@:[裦[$&!̵6á' iAN⒠pG* ЋEE*y=Ii S!JZJ9G@4/]QuIQV!Gj꡽ $m{>9%àx͒r!mN4⒜$S#?]oh" f\.&N򳌌#A:"ojk;[Rҥ`5s)#blyۑ6ӎܑ9r}le&CH-R'ewϾsˉu(H ,%@$)IH*>zB ѭ5ya =WA2dZO1s4E -'K36 y6 ,8ٴjUx>jpxPFrק*e!E1i|J[ p'wWĐ;N+Շaq V]Z}R)S]*Xvru7A_ʖ?wtM{E5>)Gg'StMucwOQ?1S]*Xvru7A_ʖ?wtM{E5>)Gg'StMucwOQ?1S]*Xvru7A_ʖ?wtM{E5>)Gg'StZBI>)Gf9:{YbEw+1sVo&Ya&jތT]l8?sM3U}M@R5>n)BBjHX>sXbkJGk҂m 냘tf_@Ύ"&W Q2:BJKy4*6=:)ߚif|Fmq'B 6 ]9V;{p 9ÇhGe{Ә8qس#gG8i v&AO 2{\s{; OiB"= !K@lQPiU8g@:=~k,;~^ÎۡF{ ]χn܇7*T%JpT|Nbpc1i#!IZ 1WpYf*vhGe{Ә8q=5oNbp`zYw9Ç[fO}s{t{پw9Ç otc6ɥobRa-4ꃩVzqϙ5Ҭ.S5,G诪HlpW+h%̞o̔ e+}^'Ey37_oP Y>+GfW=E5>)Gg'StMucwOQ?1S]*Xvru7A_ʖ?wtM{E5>)Gg'StMucwOQ?1S]*Y>'tE5i=R>)J܇hݿ cS_.{O\Dm1rjJ= ʀk8a=|A+Pp8) y` v@f7s.j%Isl,a[w,gi$"hXT cS@¦<5Mxk >4|*h޻_ij3cFqj}rpB0AI ݮ-y PK8R@I3j ֕vsven2)^+ۛe{ҿnoӕz҄r[e{ҿnoӕJB}NV[/~+ 9Yn8^+ۛe{ҿnoӕJB}NV[/~+ 9Yn8^+ۛe{ҿnoӕTzZT+ۛe"}WM,vsvdkƩ}$:]}XŭVxKPb T{;VOBHBY&oebhĖfZN HmDWwjk|ʻ2 }.J _U9 ϙ;$ sKIf5lICDKPI4>qLωs,+MfC @X}4{ A'#*mr h?b]sWw&rH%$s0 9ǟUpaR3K-HzE2aO ]($vjS IammY>.hCm-)@HH@6hOxi~ؘУKr((%*W,$aP WWme{Zh2+J0ø3Wh se:YvMTqn $F؇CEPݒ:Gr jPz,l ~^|2\BŔ\`U!Mj߱(Zy9v 71m=)~KJ@PpHT9jѤݳS;-1l-䰤%ARUrSP@ Z ZjsnFr JGq>^@ \ ϳ6އI Wx;B=gїkTr9{?dJ5nSLHnR$:(s#ö_a*tQ>% qw6C]!4vu3A/ȶqG[Ip\=a|"TgK lg ҁw+ߝGZN:Gytu&OBMO_Wrbӱ-8ܔV3rN!=YBȐ;**Q.62JeR} _H@.z|4ǮRCae$x+'{YGok_ [1.qq–Tx*p9ОYiHF2-/kʏ/T·!ޚ;FI'Tc8T;2 hk$ 3~Wוk"F*x;BmGLF1l:B#8*u$ӳ_Ps/j?5G?ܮ^$Hbz6pBlqpFB?"Km RJpiğs#z/ΰ,U=%LHRZT6ܤdq'H>}hrI1;N )8{RdZ k y)*8q'Hc5=l\[zu4|Zۅn#OXps[q'H"jt|-0C;3oKJlXm82{P(I}d´^\eaqx+IOػXHSGO=hrI1;c#MS>2Gcѿf[zíCrM3iSPJ O5)D;I4Os]q'HTtZ%MӋ>2Gc1fNm8,vzf(d1[qEʂ$9iBu0]Y+B6J0LJgr\bL7')RT2x):u?Hȝն[ Q|}ݿ=ynUF|o=* ɍ쥵AI?u[Y-KEUj+&IA@ *'<|՜jѨtOPESmڔ$95ڢ`Pٙv->ڜRI~^tt,΀3%,:u2aR+'#iPvU pj@5>:pw).suiv;.cķʜBmNZTN7Tp9΀Sr;MJj2崉)UqHHfEwo]܉3*Z:6eD̷_16ˬK>DWP}kN -xv4 j^]O:JWy$${w=1ת!)d»m6TKQ I myJS8T6*NFsymݶM%i1֔6#r9('g %M/SNCr\Ѱ\^bI}|fQYKJ`xՐf.lvaj[P4\ZR]Rps *n=%m돇:[/VIXnFqzZ\<9Ki' Q'  b*/,;~SUQA:r^nu"܂ nBҤ[歡 I%Xܰ2:u'xNRr7DfAt kQpRϲW#gOP-Yh+-K*_<q12Vc䷩ [Hۼ>K. DoU'd,=a9 a?)6+VгՁss>1T]>ȷXZaG+,.H$J@ {xJiI:RbӋxIj#k%?:a}~j)#u]l CJq Σ(:*j 28}aݟZw^ E eT3dpg 띿R\%ڭ\Ǹ-ͫp! 8U̴ !\2sb8K„"*"_\ӥ!-T~1A͊P<*rr,=xmvd& &/a2)A%[.GHvv(I4/8w}f)me(͸[a ]BmŐyd%o8۝Y< e"vW.d^aR@vncpn+ @jrC\(RTVL"^U3Po6.F?q=L6Eq6T.n.ێF-#,$sQf,_:q7Q7x9}kwR9bzv%[BU#bjr԰[@|3=|EBz35}ozYEބ} /QA"J[aFJwÌyF167'%hџ xXmKܓ5O]OWUh$2R(?IwZ5\:HZ=B~D.%;\Y}iJVG$z9ZFn7:jZ`te֏[Qf--î;r@OuZ*U^VZJqm''qnRpFP~|<=X]rdѺ;ڎRB ̀G/EfWp2bv\ q[R*VyJ$yVdi.V| k̥oGs"A$; ^q]cW diL{kSm u% !J\zݎ}魡mjX R!#Hm !Jyv ]vF [4q-(s o=űhwZi%r[{@8{H )N먅NfW65:, œZz$乍`=trrŸ]2IJSݡi3M:1j)#:5,%BbuH!Z:oO@fjޏZZ,M FQ#P5ZQε8<(އZ78`jRv2Òd[!XJ5hA]-ZSla;y)IXrN0zN,i&{8gy3B(\teJvYp|+z65ヒZczaO!(_2Oe|xMh]J[&%;bN'iR2ّq_Δ*O;R_=RQVЏGT::P겤-g\z"Oќӓ{F^z+W25-K.4ۊ))PXW4(_%r6 ^?s_ޣy0WOU~W~+'wNWq^`֪9_Q{Ⴘ~n"Zt}G jӕW~+'wNWq^`֪9_Q{Ⴘ~n"Zt}G jӕW~+'wNWq^`֪9_Q{Ⴘ~n"Zt}GTz7?Ur.#{?U~oCZK4û"2[,ۢmҤ)r3J$㳺Ia|wAj"S&&*6niS@YVӂ1,mu=sF@UZ\(3-1Ԥ!/YfӒP`d;Uh揙%6-Vy%- xGDFK!INԨ^IҩxA}KE% Pa֐֕gg* g~GNpK<Ѩ Wqz2Yֹ3i& !C  sy49yDG>EahH=bs_9dwF O[p|FHSdRnLY-̌ԆUR`2 A&.{ViId#-P#5׳iukU92ZSd9`Zշ HqeŞ r]w^B]#, 6U'@w#[#ym, E$dP{ Xa7h=75_Xm"Dwvа@j12j~GM')=D^~intpKX0J!vv Zi *Z LYޒ׽̨lr3 ˰V[iiiLYs|%ˤh. &S4Gphl8mQ9粙-O !jwFt%KP6 Ý3Kqd']m-& EŔ?Sv]BR@("1躉,8]pW [iR$@)[!n>d܍#|.Z\R%,=-ŐvZa˵G?B 5QM&uopMlӒ96jLYAZ`q )[#xMÅ%:7KDZN3qLY}m7QS4[?DLY鍣lz/q6vhҭdRjpU HBs^[xi\i*iz1=^D7Uϳ yr,]HKWdj[%"Ҟ^vqm)J\VJ!cT6EY>@]zl+h gI|̊ûƯv 3-ϔ! (' `s5,%H;FtqQϒ/#tmv3ps^rj[Ҟk};6z"q!$-E]mbR_-E$G d BRBّ<3ENF +Hps>%6ub:]^&j=; UZ"R% )9J1;jCW3aDFkwVirpMA#lm$G"uMq# if&ٕ:vnk'xCe.-89l%Jws G%ܭMqz][@TeCSh}% x'}d^j]]ieQ[ېqO2\i()H j^ Ӝ C\|F1d";C%.:JA81L=; I]oM9|5#2|,4m+)XmݜwrmKыklORyLaAdA'@yih`Cȇ8XmFARn\hF(95in\(Li ӎT)aL/7`^<€60 (h <€602, h OV!<*Ĕs%j( ޞzO){%]r+:|[ufOگP_/G)a1uJq[rެgW5j4hvM6] իW.:k@ி?-k6PT@W~NPO)翝||k gj]l{S E$5%A+a1JNlI;yg+fpsir9:Ri_x˭5X_BJJRSْ|sϲRVԕFܼĭEڮҘL R6r~RO}lWmEDB(itݰ97j"ܕ)('`<u͞ղ ?ӿLrH5DY]4b!npjDFEmuciQ7r;I9G+Cͦi<|¸{ŘS u~M̼Z%eom+u!%Dq TeBD=q^^jL+ܯݔ:Ф2XF䂠\8Mfч:hDX*#D\ ж she&3Nж=x阏vD|.8eHJq-X q"}m葩 \#],KCW.$*$h )Ղv n9QsV ?šZZl["b#୰r=EŝE%GirtS7G\oirr33 Y]5=s71ڎUҬ;ɵ[܋pTYl>bu(yJ BF$\B\grSzcm҉R\=0/OAi[kȺɖekZ-RfFbƐauSt*[/xS.? j;})N{V)f7PϻBv?{ߏS*g1zlTΟ%j9>*?\Ji}L[Tl5<^cuJYgs-|NqgNk!~{E  7<O:ӂJgI*ؓ`faM%q<85z-ѕ2QeDK͋n|fP.ou$%Dwg+J.V[A¼хvZܜ}u  eoIV9dZR,^UB _tF&:H'=5j 40 u- ^2V#)I%x6tDz.n%)eq?6jU; eו!Uڲ2~ZMnc=v+zEK?4s`r] R)"Dÿ?N7ڿ[NΦk-/Z4֣ɿ#]7na.ܒG?_;αh8iHy~,4́M‡vЃ6(ۿ ?/鷹që6R%$D@Q4t5jxqF[]cHlduѶWV!mY\4тBCG( -p,RQLǀ-IsI>+./uFC ;4;JGȐ; U,_[V^Xqo551!ũjRĸo9T@@%jMKJ[:zԖP7-d~@h놿瘅o/[Sm +ٞgyzr6L2{PFE#/vYM1|Yx'\['9ڞ5M}A\웗"zm[Q#ҷJGx=o*q=OzKe-ODf"_e-lH)O㹔(z9#Zk+]&˷U[3 o$%$r^NAV5BWb]&jD~[aqJV2=o0Euݜ.ߛH?k]IîcOxϫ\vnٝV1]z7pI(u/>5㷉ԉ M}(@)BNV3NW/eƥcjŅ B-f46d$IQD$ߘ:[pR{~?UO hOrsb\h\Q B9ܠAԪp]r}Y\+:0k=VBk6H)P 篣tRnq5WzMdϖqn+ش}U(H}LNZq^ǟ»/C#? ~$XOߧ-{]ċ8`+Qb?~;w?,_S'Ӗp=GEdrG~(H}LNZÈ»/CqWx"2~9hw#? ~$XOߧ-{]ċ8cV"2~9hw+غU_6!S'Ӗp⽈8_A:QF n"4D)-(JT4$Q6tw@Djt}l$:䉦:8;V4=szT༉m.p i6QQb8bv.,nRD(Rpwli o)O(6W=A h.[eƜsT CJGXWA):;ʚEJ -X&Se3!ݥxR tP?0nqtJ"ԷC'rvIP9e?Z?*tkᵮS2"xÈXy܅>U7} 6 NLy6T%#$9̚Cv=)&<èѫ]A^M#qɊ}#+sG2yF{xgĻי.Kei#虓֕ɵ_ZZC!nItXZFv(fk0 n%RNĂr1a\ _M0sӄ HTR=~xx@#'PHTR=~xx@`޴}EĹ@bdsc8>qˑ(7ʑ׻s2O>G#e- %F)mЬ@vAv)&lg#xT|uoIN#pF{kGX[V1Zl& KS8>ůz bIS[)m"> 1؍O$n̝;9" f%͉F8r((~ mp!JfWZe#$+<€C}>%V_eiRnR^Jz3ڠǜ`q ^aj붫xN}Dy+ԁEB2ٌ3!W7^ߌn)S!9i}t\Ho3oVio}}B;@49҂̫5]|k"z+SK$+# lH-{EṦ\{>mBZH*xI '9>#ko6[ ;mB/io-h miP ^ZR Ѧ-.{L$I( 8ʉHHHPPPP E@@D'w3c @-⒆<3H= ]Uq"K'Pm6Gv{h:Nj%.) AyX|w( (3@<ARNTFW3@@@@n@bv}OJ_ZX P'52e-:NZm TPv۷G6Ԏu ӷb/p33@D>ێZ5<*l-]%ޙ.}b8U)[ғ9wf HmӅtkoTvdR?QGr3M:٥޷ͼ\kTGg.I{%+q %$saN`BRZUZ_$DuOgH RhVԩ'$Aq΢u%4Tv"%#Zn q;ys2{4HsSX\>/$tdvgc:RbߵEnǵ[f xt)D-*)8Gb@FaolFoT\{H^L0Е*y-iwKܘ.VkBT3!cG,(,q[#v ߢi[Ll,0PQZTA.4't,l**ÆnV}fw$2i5]u-\[}$Lo\y!&6=ChNIWQfW^箩oUw~y7uZP iH!CFbx( wKUBYq}{RI b(hLgpA)Q*&sdWbVHǭTma!^jNr!_-TXCy*S*_;݃y` k.j\\vN0ydtk&;iת1q8h5, Վ@OFDmw%L =cjB@AsߎT\)אeR%ڜRi[HK J{KvZNJB#[lw<}vvPi[H{nO9z;Mu=EbENxCXސA##Jv!WJdX*RIZrJsww BA#;rJ@HoP Whvc*@ o^ R*@'׈%JeOV y;!ۺ_G,xBpvoA; r}o9u%ORB(jqẁ%XaJR6 ޸] H(D} I*H LP,ڑr6dDd;n]^Ͻ?H*iH ێKl<Ǻz^0Q%p8Wf]A]G W;kzzRKtɀr\XۆkK 48evRV&E-޼.PGYP3f2-֏Cߤз^UiZ7l)v+Rk'x>#jHgޟmx>#h+YڈP|3hEƽU\Ar"6H9mrčY3DY#a]\PS6}-czT =ꪴjB*n_JTjeN? ?y}i>}Oڮ3N_ -W`d/{wLS}w UeQx[ѤGHeeT%e*/# ~K}_W`*]=-%~n]qwؕڛệv¡߃bWi|7pT8z[J?zgKc+wjKwNnhLKlr#>N"’FATJI$lvu|?j"v>H8+Jm[RaBmDOիIRpBϚFi|K-鲲h)V2v p;y^ci+''d!ںkp>;(RqJJ2aI>#.vLǥWg>*K=*>%^ݬ%~o[ |d7M&3Oa{FGñ=,ҟ(FRG#?¯ñ]1KIbH'Ḯ`t|XQ8rLRM4j8^>#V^L~Ͻ?HO8Lғ` qU-Ȭ԰;;RAI0)i=$o_¹qg*B!7})n­+ghWZ=<ҿ7?*+R !ꚕ %Tq;S.sAP'8zMA}/e[ȋ* z8%AN8a[ܭV1@eM!{s2b!Q 9PNIZ4;S',VMvC Z6cjo8< ք陡9s%9?Mb+s&KK48'jDsD/4Ds]IqH!A x8j'<8˗I l&+O-T!\+V# 82)>c@gxk7" sQBʐkK̰˘Q{t7᳏\v-n[O*RTF{O)NO1z6hkH1zHPzvԂVG*j3/+ms Y$ 4QuE7hkK|)j g#9Ê9d@`&h;maə') z4岰1%thn>.ȶ6-)I\Ve Z@":XJKY[8<rĨERq!I*FYƫoIA vY7d[$BBA H(`+$:488+ A>zWv Z*=ʛQI#nO^争eFdJj#N ZZq桢v/pQcg]YjX ,Yz=ȸLf[IQDZfSɴ?iKq*)R[VN"WHݚ$kS'[u VsSčp2 2f d%G r ˙y tJ2 g*)F坩yCMJbzo4q!iP ZQ|P=oBC[>jmjQ"_xm]>=ҥpPY3X%:KmhSmvH|Cn, %t:Z ϐI ''њ\%˵rm-a 2]i(RJI<3(DC'ozcr8*%;@>N5$4IO@n;:="A [tAmKԀGX|ݚ'm?{h[> kDPd #Tj-ɠ0o:!9&0cuIiM^q^w8u5dX/Wdwd͸|YJPihm=Qq|\:Khe$YmpaT =JthմtR?&E{{mu)q1Jn;LqT4֯sЙcmsG`+*0jRaZ1[us} )u8MyѓSQᾝCxiw I syM}. /.5qZnRPl;uQԫKN,6tt:_r~b]5?֧o\i2Jmmؐcl(Hd:~ 5xҫ\K?.6RBҽ ۉkd%SJ .!avաIG-$`@f]\]R8' JF ;Fs'G{ NKoJzH#>Hr<  p:S*CG>_5Ӻ6)mJR7̅Y;r@.j%;KwPP/߂_@"ޭͶJ)qG;/D׃}2m% Bs>}n ;a%-3M( t_IN%m߮Zeo3Iޒ^!WZ׃墨%[RZHX>4ˉQF{3ʓ6gFr q.(rO"Gn}j„sh Wa@EӼh_-~(J:;jeWtq?u=M*'z@Y?'M IZ(ն4ϲ:=$N@H_$9#'cRYJbweDRətXZLu8-ޝ#+$կbcztջkiłCa`ħy %JAiFZfӶim:DŽ !M-DVwmڴ(g)fסZQZQ9lY4) u#TT23UrPͰ3dFyBl($q(+;UbȹyHxjp@|qGH^$YǮ 7&C* hqeO$6ZSO$m ȧ%O|3@$RASep֌,h~m -x mKMD2˭ԚTuiE;9֋Bl*+k |bgx m KAY!?SZ./L @!qj{WVu~ľއ6G $w_!Jr_,*NDOzr2@{mk%N|jMe6V 9+R0J Sq'q!ڌPCqRW,o@+7 {$sZ@ pp8*N`gQUjk/gW^޴H?{*vvNw)#VZ>B2v62?MZ'/>:_~~"oL^Vmn &Gx+[sR}Hw3l~ Ɉqje%:(]nTa$:D^kUFvy\)G K E[r(FqVVl61ι|8)+uT$ 83G~ojk.ޭBf2>ԅmqh _V `Y# Ǟ68Kz>-zd zKsJ!%)h6Rž& Z6=!-1C1 '(Cn$(` ;N1KeZZi yjXC%de NP4}kM Ӷ WnkWC BT\Ԅ! R(1怵'ul' EC(1mےe$5 Kid_%^(މ@?ЫYVRɼe~^)IR ɪI'AZkPeK 1N ” 0 z.uvѢT:}}ښv ϶Ž\wy0dUeе4V5p.Q_^%1-hvgz{.T/!h|8t9`% cjy%99ܳwhTm}f$ۢ!(B,gGMId\ P(._W.?%ɂ= :Ìb6>ٖ@C\%GsXI^eҹa^/1eNIQj9Wli. Rs+^PwV/&] _QR@P & U/J>P?U/ hPNG*%lH;?.$L%1l)k*-ڮS>[%,75- 6w`Js\aʢWfYl]l 6s7%yܬvkω+ -59J \rdvBTJJ Q<~j(sjR登%>|<Ze'w{XtbtpKkcvW&2VTTz}Q)[R?}YxƱ[-E) )NO"+m@0hO$f eC-SJ@N֒+'$@%G3ZzPKkX.:!ZH  ܞt"qe7bShm(@m*[Brv'9xDgL(@m~9Rˉ :}#x梓 :/ FJ@)=Y#pӡ,V):Bw-d)M@I <ʀz7{5ň)KZ-!`-n 4srVVzvVhp2zBO>XR&tW^^g,"CPI; syJ0Fu++ԗcOv2cE mOVj.蹯;@HPrSZRW~'?}auDރ9ɈKPS.ĊGe;i!NI'E yد7Zֳg;%{cu¾հXQbW-G1+^ǁCXE_z܎eW#~¯T=n9vH}_{CY}_{?X= >=ţ*>=ţ*>=?zs/bgf~[eZsVo{l` P]dJ7f?hs98wǗhz,rpylK\uWۊnuBF ~C9\،:O*Fɺ^ڂO 7,sGoL@ڔF$wv]tu#kiq;!˝Tk_n1VIaI)N6?j5Q[j[OQ¹X.Tgu$Rd9Ԁ b%J{ $VR>bַxo7vt¤(Og@@vP>69㨼|5v3-&CS8݊NWЕbm>,̏'!EW7 EDP!6g$Q`ogo-hJD9h~#vTP|:aeJV@YGOol-%]c%ߞW-qYLsr'ʂjx!R^QʜQ'kUɏmq1%\8`vL3 ,֗_*QX!_-k})>d!GwߎgdMV:$},.i-9PEd(}zq;F3Mr/c3vBJ\eKjB N+YSmIjJ*Dw4{ u IJrI\uWs谉*i I]r^KPO U#L 5K.H)qAgZӍoÑIN҉RH%KyaMn2۶p|zVz*2 ݐ]jˡ~IJ)I!@l^[K},:7k&tsV[+i<wr$iXBIZ'-sy Gg66K1s \t45P,#J m4$Ar5tT|ԀECj(]*%KSJJG?IYki4EG)ЇWfV!R2;|uO O/C'էOv )%l6Bn7c<:Xq/ RVz5O%䗜x)7(?YG JN:ėЛF:PO$111)c+JO xӧKJ[ZdөJ9 oxZ_d‡}&Ӫ.VDHpC2\O읯Ǽ7Wmia+G8hqj}O`)q|gçBpEvu.H:+frwAn8g8TrsLߙ~w"^YezOçgrezO}1L^,)3ù{NjoҦTnJgVNh^B6OYR^{0v /.fzfj]gzGH;8ޡrEߩۃ:_6z@KZQ22㲤09;v:GORijkR"1E iշ@0(oNRE3l9!pte|cfʊVV@ @@@A$x׭xQ6;ZOK H,;*8mJʓ?^PX4r<ٗ)tT<ˎ6QAZ -c8}B6RCC4T^oI7|?DaWTۖ@S@Iʛ* A䤜8[XtxpV*&;nJ_SMZ~Z;R:w @'ZN\@nUfuC3Im2$%!(W\$a#n1@'P/NF֚2[KWw^Xi-A[dud ܔ9NyPct̸JӳoIB#Td#scӈYX%|!/VFpAϰ@;Z]Zv;2ﰠdKioc JHQNqa飪Ϝx ffF|eIRU旱>NQMH[ƶtdnη%K)ۏ)B$$ǚʀүۡl/ν ʊs.C Q[TvW(b:__" Pe!?Դ)^zs8bŝ6pBKlkepBe%A)(R O zᆴ$]v!!RGв#i%$灎t((( KGJ>$'T\-i%كn.Ʊ.CNK74vJcj34-xM&\+[ϸR[H Hkq쑴,]W)]T oۓah( oA`cymPRHP4CXH;hsHMPP}f;9= ;jMǞ며(Ϸ5>t@S$f9:/5n%]^ u\eL8>JZB@ Z^fn*:Kb2umP) #A*H(Zmf\N+#c3q 3Nq@)nCs[)snw*ۀ7-)Ϝ q߃1.4(ܧԕnrOx3Yï7w*mӇ| )qqB;IrhA旐x$(-!I9I@@F"6ƽ#w${QҸ'W ԍ8$T(J$KCTl)h^VqaXRO%$wRj7eNB>̈́;*7v͋*?-bIlz( IzLHi:V۩ JXAEU聺:>%F)oJ\@[9 aCnij#"6[w+m 92RJ@(M9Z:-3tB8:]&kvLH K!$5o1eO#7Z1C̻yq!IP/' *t+|{ o4%>KH !T$z U6YpCE:dfŖ K)ӼHA{jU9jҗYRmP\}H%JϝXm-zƋ"i<%mԑR0AT´& H_^ 1<ӷY#ch8(ģZ+ *>/HAԮ~ڸ(`RA=捰 H^EhnМg@ Ui  ;f3[Bwn<Z6vqPyq1 R|JN|jyVV|sW3Ϸ=,Rsiaþ3G*¾_)\'@y,${uiksZQ؜mNH@- ʀ~W*~W*~W*-i߈opVT2 @?GwEi\vo $g>d@d,;֬r1`qrP }j_8֯U@j_8֯U@j_8֯U@j_8d7i+2$j9* 'y  LgX4 aD}@RNL Kڽ%}D|mK xRv`cI/OPSn \V v{BRJR0)ZW?mu|ZW?mu|ZW?mu|2l&>~áBY=g( }:N$b#Czq\9P)0y)KG2֯U@j_8֯U@$ꋍn`ThCk)i ' ;*U~uFTjim%2%xI$tC$V@NEPWD6{9%=Y_?}棠nOJjВY)ei`,)rGq[J#&Sl8[-R^|_=JAsng:oN=[QI@$s;Qo*c}8g]#vx}m$N%{7s ճyhsτ"9,$LiB㌜IJ@ebٯsxAPRuR]-ەhDIn:--ng'RA-#f0Od`UOAWBG(`)R:̫?.*].xᤘp*6I1 TT-8y~-'~hlsƎ#·fŵͳ/%(] ++w%)2*^R~fz[([DpbShZ Tr<0zA':΀H[ ${bw/PiE?<K)_OC}H[9( G:St MJU Ce-X.1ݓd+17u+f.m) PNy~|W3:jw:ׅW~ҿL>B1+VaRrڂ8wSg=:=͠Uvព]gcɐ$$R!w,vT(< R{24{vv$sKDud3kX_ZSKJ*zǑ?Ic$VӖ^'љ ƶ ޥQL$yKWJT}E"ČC*1-PJHSҼzԗGRPP `5GM%rѯ&6rhR V $d@yXLˈ_w;(iy,Aŝk4nmJ) mK ##`C=bvja$\J-eHOa<$&#6sNb2uI}k[ ^T{{xڸ\;f۝mQ`)Xi89֐SA-:YqFKpB Im(K ${OuieԭJ&ni}jC`%Cm68=QO17^D F|L7qe$^d-N p>LS{Lg'׉_Zĩ,FPljy M8J 'm@bi&J4`)Qg*, ^m6RtڡsI?\#Gu*8Y3XtMֵ䖑r*eV;F2k>#Ⱥze} |nciS!ڶ6ښSy%zJy%)-CzUؕP6M KʜK?)@=5GTtqTHPtbKiIGERw!Y+w0H u<ҩ%kY^ݤvR7e@ʳXOg\'#]hTPJ$@9:,*ʲMkp0wYXh܊\¼Z#m(/QJYBФRs";ԥOڣ[ӱbՎj<7}Bxf$NMn9DBܒ6*1u / T2R[KvtdB*2GfO.UZvF;]SuϖPT{BR kSGet.HeY_̨ddUo/Bɲ̎$O]:qnr\[)Z\B`IJ}<f}n4xVfMgcIoi$nrQ,2 m>Z[67w-q +JA9Ƕ[xdn' rB@8[mq''\WSlׂa |3R+T ҋ:@4\+rtIz8W"tyg)خEi&]ոn$Vx,&Ce$8e] |WQ"989.WĈ \m[|q䌨i&/|FZb+wЖה0!(DVbI?.SpX?߉h/$hl*ZcL,%IJ@ϣ:-yk^K%+ҮT,95VFrO"(Eb=:vXEjMEI@1./R} ! N kб\ʵ!y RrJ,Z; 0OxVZ6V$ðd_8[pw dTʆh.HVM!f9xrYZ9sx=x}n5yǮ"kKi~=n7GNV?C#M3>65us!&K<iQ.5,'E-ݧ8 ]$0->JJSzYqT-=Jr{?WZO@=xzt 6rg7k:FۦrgAKiH?_!iIY %k}Ln -rH=ηŽHT^UvR@ B P]D1rYIA9VD˚qEJ9hiZH#$vE1,Uެ,-enejܦ[IQ #&àf>ϼw-TT9':+5n%]^ u\?(. Wm)[h(wdY#t; ֓b@9 Л\gu8%C--Cs2SCYRZا| mrP0}pP A^^,cŒYZ=?B?@@piYUGdծ|RiM4H;O/Yvkd=8lm]xbվB^>K ?AɋJT⺣ )$#^:,m1&Tk-"bpԦCtpʼ ׆vnՈŹnÛC .9lc*4R-m%,y@0Mz,>y-F:L\5\i en v]RTdrxQQսMgS] E8WY ^ Tξve#@;';6唵:XM K(a38OYn(k*$<.U,jbf ֡THB+yzw5L/WBu{309QS!A^IBH!UQtO:t''kZWqRv=-‰|)aCáK 5lݵe'x= oI6$A5mЬQI,-QF `滝/^dS驸160TEZolJw')=)kr堣:c76 wSN9>6$$6yd׆n%zrHaҕۻl*H  4!W і:a@mJ?<تTj4C<V\+wwxx>њ>{:躴)H֯F;ð^F.:ݛ^FMKݔv JR ͵[b`-%9] >j]Q V@@zI;m.#Nu†T`v7o-/SEe!ZRw gh莨D&vք^i`$'q%k>L$ i-HFcVYUv"{Ng88Pȫ*G` bU BRr=#5 ܓ eH h @tglNkį|ODMOgEzfᨘe%OD%|@&Jic]q!K;F{|$ciM{pH$= S \9z8a>`;nϡrI[!SwM8wej<ͮSޥc>_I0mz(o |5[UX5 dB9 :G ֺjBR6KdZ*=LUe􃿐3 zRoUl׋$ڤjIIO< VJ7q"bEVR!Vz j)^Rmc ǐ2Ԅ4$SO":۷,YCCl'-8oҒ:z j(PRr@G"=5)\Z6I%BĻBMJ!9gxH$ht<`{g% R5c&ֵyj+w.yVC1z!]tO^2%8J$'#>au@a"+q jS+mE! j* Jg(4tn}yg'W,so!2F\8?;2t4۠Gri@O`}A'+ܜWYS@8*;( dt~jwu3u;mvԤ +5EkӃqJL LBJV:ĤYrTѢc[,LE H{'+51mKicP8Lӗ.}#VƶZ1iq@` W]Q).YlR%U Q<40PQI6.u2 ?џ-8!iyĠ)#>Uւw1-pK`͒{<@y{_LP$nJQȃh @T{b\ɔ1,HeK$s1ʼ:8ĺjGq~3gMh%NFi^Oc>nU\ͻ"(Ȇ3YnY,L\c֥*@W˅ʛ+t%)Rh $3zRj+KCnL(n=w3$# r4>cZI] vL蚔C9lKO┽שCʓW%n-)HR{~%$Ŭ_u,/ VSyXy-Md/YvSM,,'rb_! wrMKp'YڋR ơŖFW{^=k3Xi-)Rϡ *R خ~ѕL³dgVv T|׺rZĒ| wPzRio(A *5"6?zvbLzsȱb;' /+NQ<1\5Z?6½;lozUtO6 kVW-'ZeīʫZVH]QrU!y)\+ nHI( =C n*I vբLF @( J|OD>_-DXD4!r䌌竃$R\Jp pAƇ>%n&%*o.!e93WT_?eӎ2:Pq )Y< |!fLu A G#/x@#be:]!&N )V2{y"ˊ̸Ršu%MRJys2(׮2tu_x7 5fjp*)m#)ަgG̙v$0)}JTY)AP) 9TeDeO#-!Hq$b*(I&l%8uRHm)$j |SbRq2u7NʐI VJT5KroY>GQ~Bu 6jRi$TԠpmN7MjmKj$I$'A6o\|1~LY_ܵnVUBrwcP=MNjEvEnrݙ#Aqŕg!yU2#uZqVCESG®T8KS~ dydO(ɖu,r)HJFsSd}D~F>;׋ eojTY $y]5-'RdG5{\u7#31.~gGcoP:mxM7/jw$dH~G,5&2u 캐n+I`ѷz7JAкJͧ-amj`m!){RU4 (2 Pyh' x^Ӷh"'s\!8*o~0A;lya3zR%DLXsJR d qlyв1KZ +#88n(6Sźmz.LUnFN2?(yŞ y<)Trn: !.RJPH*I)iG/ObQɷ/ Orxȃ-nĶ-C,8ڋ8P9ڳaPwHyF52g$H#IfcyP+K)*(q+dBi(78$>N9?msUZ >ʚ[e29!W:*H5UosK)$zA"8jQhGͳ7r2<_6+bjJTG1"N3[ 44lg)Imvӆ0#9%jʳ歒yK|p&߮Lt]E;:9)@tglI7DZAЮ-bI[ ]ڹ$]siB  PՍ,?6Vv+ig*Z9AXp G] 5]RCl0Kԉ ,u)/8h` /C}DnK' _TɊۭ&+.uRf?KtP-{m';0ېJR%Ҡ])?:gXQ" #6X6JT,)[@ Y(ߢ^߷kW YbzZp֑v5C_ j-X;֙md) L4$R:O?g>kԜ!&l=nmۣZ]H!nOwYq`z>$坝YL +Vr;0PϢ<[֮iy ^ Ӭ9͖=$eՐ@$gv/S[C\e[iA0;ꋩ9HH4+VV;mFv"[;)'Aq{n1=„b_T{jP| n'xDL_~Lz+Sj)^q.:BY)21:ÖU[i[m=Aw@@ea%.P@j|.O[gq1q|`9%,–IڗR;hC{Ҹr4IQ$ƘԄRwYMW}}|Bi#ܭkclYqoތ Bw(@7]zDžۤ"ɆV R9:Ǔ ,սNz.S}$Ii[Dij|aHFFgbsm I8}+Leϸ%iHIi9qGY'7juySR=XW&bKm#*3HߥJX5X\GCŇnΈQBp`+.JVm]fF4J£Y %}#Z8!Njn لh+(J6.NSu!!uDZ<U@x'J:15n%]Z u\⦜ 1vKISUkX dvnQHj{G0(tw6ąu`0PV-h桕NFgxNeD/pj.3ӌa<N00]d Ӧ諥)Axq;NIV4m]\KM҈˒l$eAw r;sTc^bYWM̜WnF' 3\@@@@Ct"\n½!/A/uml ^n<ؑ@<8Y~D7WTbx)Ip8>@@@@ _)_T[֜*8<] B<2 rѻ,纀ˠ Z.U: +ӷekC\asp1r8v___gmbIax 'oZ 'VO*$mzܕI;W,5 MFZSgd"k[(1Ip87cmn[At5dfŎ*BQI9Y'ƍF.'˃m%{jGRmKݴmϸW`֤+^߭H}3SN&:Tue Dsk#*Zۖn&k=s.c5(jAyJ$%c) MtͤLݛuCr2RݞJ( (dUfTHtKhXv`ƚ셩‚t+'bx 52#EۢRRއ_wv;2L@k O*t.OFҜc`z@Yjr߶n%6H խĭ#!X9 wyq@7}yP]Ddn9o:b3{`qJ[*NqPz_ۉ_W:W6XcWT_BI]\=]ɅYmSp2+Xf3#dvW;ѝi\ZѺ2vȅh}ː/n<$$QmS5_m l~|PpKT; 7#k+-*2JFyUEޤfbpdǘqAp#R:+R( >DN2$FRT;J)!Hg'j룣L笳EԒ[l\JAX߷`'=5cV ̀ImF8* :)8W[]T`RVw%#a=%yՒl鄬I qm"$w)Oy9|.}:b-Tؐ#,󭨭neY744y3QPoiTۊ9q=wZڼ}MjPԉBHyC$wF= ³,@@@@@@@@@@<1pމ-8Rh ``P3ru_-d udB9vԂ=vR΀ L:WvW'բ͟V'θc`筚{VDC̗q魢je8蘾θw Z#2Riz9s!i?T3`2qJM1/ZXT௻ヌiԕⶣ@~&ˋ)RTM|yI= 2D0p؀i$)RMRrJ^en3r^r 2gEjP.T>mG]K) n'81S9+O-3FK R6R8@<2g{j't+#ȃi9N>𤤶!- s$W6%IC0+[qiFj. =!6ITsJg}B/sRi1/\3r%URwܖ<0EIzO*FܴWRIS QЛ ܼ d׭J7nե LiiIFT%HO!mh#e9ԲSxz< BԹSd|VgK쓝`j@@@@@@@@@@O^mq>R-`845t*Zb $eZKgs{sWyeD?>zG3yY8>>3W/g/hwϠq,%zC+p4}|;g=ƏO~!f?儯S߈s>p4}|;g=6ƏO~!b_9B{? cGާÿ}/g/hwϠ{E%zC(z Y4}|;g=2ƏO~!E=De|]ᆘqT v/w}Jp} O߫rs>ҞJ=ą׀0Wu3z=G}~D41 omȫ+LbRFchẈW,1mEC!@ ڤ\CʹgjSja([F44mc.x4iX550PW; ḶrvoY:2~^*%0hC@$'1\P0J2Y E^QxEu_UvNN=Ÿ'nu>984.uYj=r2sTz)2䅩%9)$HPN+[9qǮjwRTHNG9o * FST28`ˌޥQLꔨ+$(g嫪i;܉7/!`J2jN `u e;*es]HU ,IgVHB)I+uGu>h/)|%~/ ڞx~P>!?!`M}R\c!!%Cl[e*^CmŲwawHNl6Os܁ij̴&WS@ҰeBF뮦L%gz`g%(X}s_f{q힙GCHbϽgjB}9>lT^uLgރA,>!XEhv۝r/h4QǸ4C۝r/h4NN=Í-,oXKC8ڻ߫R*ꋾ}IB<#m>!grs>ԟ,A߿NCgJBjqVd!#*RuϠιHNN=nu>9846HEgU@9U98<.vi`Ռv(Լ]dBDZ\>#Trq'-۝r/h4NN=Í-æ2z/RTJG,s_f98Ɩι}Gާ'Ɩ1K-GR_N~u OE"vhPO߫3}IB?}O?Y~Ϡ{i' ?wӐC5W?>ks\LGLt+ AO` m,Cύ:)t\E;LĐRl,%De'H\yfi+JȌR!5<DZtDdDl3"}- ͱj|tĖc1אd%@Yq%%"NRI&*x#ion7cӻ5*U,=N^Zdfn!ԅ)1uE.{\uOkUkUkUu/ZMKLA\x!. cj:èE28fɈZ6gVN8iI&;z]S_\c:ީY CHJqDg#*9zkM; q!!{Dq3Ru;V;]8{ՆfcU(JozO~f^)ONS🮜1=x?]8c1Epz6բSXJXe-8Yړp+,cӢzTFqی|)9+] AJ\oi ˴Oʲ BzNVcr-֑M07SkNvV[Yy"D6\#LnE'aHYFA=,r&R#ue{eG]9|—'f^)ONS🮜1Dպ@s_OUhiNZ:MӬ,,#Hp^ClPYR00;Us;(JKjRնAc̀yA'}u!ƤoiFlbq l)؊%s1u Ռ!.CKr jJ% Y<%Dr#\(HeueLYweD]`G"OAVFBsUјV boW-mn8RR, @ ڬ#+wюuJR[p5en8%Rh$gi=7qK̷.2زt?$^0J:).ך7/:9n2xz9-z2-z2-z2-z2-EkwV0Q4ȅך埞(#^hy(Eg\QRR{Ji\^.L^.Lq4& QBNINnIp1G@\E6~%="9TloAryS>@c\#'yR?Lq rR:AqD9B +VTb--n<5e7ws+ךrp~~ze^jQs{M o Jz;.Z2ROyިISWLq !",^78XV3 u2?!=Mj* -y9K!oISWLqIR?S21v[`"*S$*TlRR^9J=yߟEך\p1eԔPrSR?TdfCġ+8aM$vy\q"ԀyܔGx cLq vSɜB 7v vyR/~ddIR?S212\fn:ݼ}'jb?j'M_S212-[5 zmi:CךLL^.y{\-mrCJ*+dmopidy-0.17.0/docs/_static/mpd-client-mpdroid.jpg000066400000000000000000001041001224420023200216330ustar00rootroot00000000000000JFIF`ExifII*1&i.Picasa0220                [  !1"AQR#2a 3BcqTU$&SbfrC%5DdtB!1AQa"q2RS#Bb3r$4 ?U@(P @(P @(P @(2W >V\UnxD1F5"J(y{<*'Ѹ"Inܓ^THg( 9jƬۛ$8nR4u],I$9>gYbln"hgR]KFVP >\ޫI%#, s FVp3QT%Qf8poy)zgV"z/ǦH#"6du>*JQR?2Ҕ^SYO˙2f;k4q"[Gw3M4Im [jΉ'1,@YR%o<|5P.'-1rꗳ5/5 wՑHTY"9x䍕Ք f9Ss_fѭ 5MtiEY/ @(P @(P @(P @(  &$Eou:[dpF[  l*h.m3ZqKdg74U\H>{u$ӦI23m)!KA sDΡILi0A NF-F8kz04 @`EP\% y2b:$*D\" -aml*3~Pz?@=~3?{g*CTއ?c~Pz?@=~3?{g*CTއ?c~Pz?@=~3?{g*CTއ?c~Pz?@{II %C)OqW7(3?g 8SUL {gzCz\O3?PPNRK vN-Z[k5k;>E20;F#iҝGEV%Q^dXK7́S)6kGZi4ŀׇeqr7R?ǏxL{;E4Cʖ9h!#&dt)b82,zP @(P @(P @(P @(Uf' 1>'oA%#r_2wdq1bEYe4F mE;T`\ZJ5sL(Y1l +]cNeVy -XI$JżP!G$s!ܪƵ5Vr}6NtխJ]K=RuV+{ 2al40{GwK$Si5l I@֫ekSx/W6rχ|5䵞;)Ut %XIgVkPndV)!a Vr-/5vEq$j3]/nL(BD6r :;xQΑ>$ur`YE)m Nf{=H2 P @(P @(P @(N^OcZ x&S#UHTRKT5"6W ~:Lٯ܉%].h$[kWPK[)mcz.zfx9GhpY|"N. FK abFλ0H:>"Ӕj{_Teâӯ/`qZnM|3+H7c^}VϧZ5vi$z^e\Y>(l%v6H.v;[OkB}z8Liu=$>\@isyw6RIv} eq gW Y1K䧻Iu!,!;&emP.{aYܱ.<6kdH#DzK N] nSOԭ9Cw ܺucueKU460%J˨./+N^&J$=dM]L]?ZBX: -# $\À/c܇¯iˠ={۫i-6կDrGyIcI'~zRMmu܋'I~\70~w$b027 v>xq#fWY.'"wBA9DemA;b^ZcT ' ,j^iOĉ'-,HTX+02P @(P @(*,c*Ybݍܩdv3wc'2q-֧J$|_Y)Ryس3;13fcf%OҀP @(Pt"'*2<M`9(N:پA606FR7FY=AGA5j%ڀP @(P @(P @((dC€Ad}F}eҵS*3-0[8;+nc_O YᵡO9'yJKKo*A|M ɵ`ϾR)J"dşA{慍WJjr^$q~dYngYBaI$]Dv`mfd`] Mwl^ 4<;,7o׺C 'H9`p=AeTK/e{RwJP$NzS"Ovt TiO/|q4߀>~M[tdq.L>_ӿkrTyV0s/INϫF*lDqXK70 %bn\s[^mSkvj7'#|Wv\*)\jQg]g{-k$2IIdnxzm}t+&E)RxnﴖU.)֌ZΧMg ۂϞ"%6 oښ&pp Aq|jk:Ԧ(J:<>k1AKq, +ӞiQP>*ޤ^֜aM_PZq}7~šZq}7~š8׌?K<_`qxͨEV+fZo?GmB|l2ֲcgR| ZԡKܲdGT9N$Ps o=?%g3CW.?mƧy דўA㧏Z?BK/P}\ůKV?{ Y`%< ^EAxV-Ky2S-)jBP @(fCl+&/ӢM8bk)V)g#'tcbs-\$fQEia {g/V";X9g`n%J+)_ݵJT[_We£b"h8Oש%V^y]M덵Ul_[O(p vnD`. $]T]UqMrk/Gǽ»)e. iɺtRxfwl vg{$6ʍ2*>bVT_뱩txs>9Vkj:pbl*|K9f}G=ͪ׶mTHbq2eI\nf *׫ڧV5"u+m0lvxVQijf+}?3hc w!uJ+R9rr9{7|2ݺOJPҥ]0QhKMjh5ky E'v,TEq[Ma4kj\ŝ7^xkjVӢ{yWz(xM͇*%hlڭpؠIZB:c QǿJzk⌔UD4;y.Y|LH&@>9mx]OLY&.U(_A|u4ݡrN 8Xǎ魽]h-w!iT>v x.Ii0#JZqs%5j[ٴU5yxmC.x|AVW26l~6< >h8FOzRoϔo-ГmEm.i6H8?dOq n UG`AՋN7Eg]ֳӌzy6(+m9aqmpYUrw`gcm+kqN= 7 z@zz xtE'C,2AF }.)V/*G=Fe#e,eBlP\I>oŔtQU g@(P 6N4#ki{#qo4{ݙ#1'IG (:ms{^s1ir¨3Ӯq kyb+~Iu>c8N7r̗QGmcgT>c I q ?-l}$|rZ䱄6{-V/UTZr Mp 걆*]=OS֤.TėR7Uh4Rӫ 5-WfKshù7 w\ydh,z(Nu-c-nsnv7kFR㧖Ǘf'wLN-Sj,{RLqfvlu qXNԓ_JIm-H4<6ۄĴ;>om#FO=WڣYkoI<vfp_&%q)W5UE5K?q.ᝤ4-䰚׾Kr7S3JԢ8Q2:˜ `;$Iтa\O\Z'ZKTskqzFNrNq.L3:};TF,05ǨP>1T+syط}smaVߒu5O)&Ro}ܟ$ 3ƥub8gm[1YZ??gD;UE(>gfRM:Y6Љ4+\"c~M*2#.^c/Bέ|~4ey7Ir۝O\gJxսUtq!g[Ve)ȥ[IbRi%~xtU,Ɠ3۸#QJ-ph.Id*Vvkz:z*_ɕIGjBP @(|"I <,wI{ۂDpDnI$۴WiR~ӳ q,FR:Ҍwj v#zfjFRxȿ-aRk2~-o5k7C.-n }4PΝB K["ayum¨*\c<#%m<2@?PVg tR忤էSױ5jYݵ$}j[rE}HF$'bJ,ψ΍̵<>]w`w\RqR2yi}7ú[1HdG\nGF 2ʐPGOOZԸg7:=W!5R/~k4 JR&Dkq"G.+ت0g ^];;w'AiҞ^[[=;TwF9i8%k=<GlYrŲP%Q$?\@~7R*?kjr.?ht,kSEIa!Mu7"E@? Sn7HޜWSptOsNjS_{>SD,Q  e)#slAE)V7QkGbnUjXJFgyi9%I2HU vOOSNw-qNv^ Tިx?;&鮴˹m浹 2v<y0ȍ! Kd)KmhS)Imk^^^f6{ ¦{L_d+ޟ6bpQEc%ц -p_ߴQ9E乬K+ڒؒ{,šr"+NQ)!F_!)q~h5\3y׽XYGXSs4ib2wUxyLh(yh75T35q<cƓO>-r.x5dNm+K kP @(T?"&Жt{%!#&U=W ;\tovE{Eƭdî''=tAFNN2}>玆mxOr,[h*#HQ")n޵:B+8O8'QRn> )%mpT]5#|Vnm;<+##[0 N\m˖$Y[(r5"w,4Xv>K|<|1̕Zp ecidیoGOuHo aIj_K ';kr0ʙQFs慊,tP|Ydgp$S 7A;cosxr݌vߒuM*W iv8I}ʵ'ż4S{+mF8 YchG,|b1@' 2;=N4ۡR,yrp.ʵeN /NWO]m 粉,;4G'5 9e4G^eSIMGy硫Q䏖=qTwx Cex"ީ, RW`TNXϷV ԩ)wkΙ(?},{kE I#0p4 ] upլ;0esQ$r"K%q"i6fR9i W*znf#;sUx3ڹfM9c ./#!lʫdO9| Ӝew:yQsCWxDl܍|oGkTKmϪ+BhV n;@0:8m-t3Mx6KFb"Uw]|X̬¸iT7'tiɴ1;o/KM֒\xocviv*݌gj=G%nϮ/S֡iM뾲 *Mj5bYk^O&legFv_QԖVlq׸ 9эxֶ^X:Nso2-^LiYyݯO^T)na^Lsm`8ǧ,'Ok5܆u)G~Crlyd͸9:Bv 5׳Ϙ/Ki7^ UJ-ɤOQyLo]Eu@- lYTrw x`,Ҟj\ W-Jsu1yiqӴIR"km]i`^sihQѧQUG(G| #=\2&0脳 rD8mX*Q۾J2i o-KӘ4Ko֎׻o%xe|WT)&)KĎb9B3sҨʏ.F֖/(',%n_&̑>^^=A/{g`bIfcm|F8ZÍHSol>B}R&JsԒ𞕎MŴ\3KeH9ؖ?*<ՕI|X_>9癍NQY խȤ3s&Re%a{^(ȧ'c~3lv\X+e6ePd X-[ڪw7nc,]zRZ;%\x3@NW+V!طg;PYiZڿqB$*Lb0r7àHmNI&O Vvyݿf/%ktJ9dcV?ܶ eD_zƮw| 9.)Ό,N-hYRJQkuhҭK,g:$|N ASZڞet?#Zջ1繲7{]B,Ӣ [i*F | j.LҒk\?;tKW,9J?{}Zoյ xM$1"Ft/f#cnR5)rlo%$jy,$湯y2I-3Ƞ6ٔܶ25Z!c+4Dψ\ zUۯV6m'{nc)-L\R]ө'7-SotF u ٻK7kkI^]V̵}V[=-$dEw^OY-#5fUVzs xV<-C˨:dCB+2JBΐ.HĒ N+g'y=1.lq3O$'"Kz\Gq gw/+獡X4ɪMiJ*+8Mϛ_&\`|*{~&z ]vVqvsFآ$eVI[jHnoӚ_\8ӏҜW ~5\bZδwq%=ʵ<Y ;y* +Kq|k!*m!r0Yb2b9P>DSgM.I6Y/dߌR5W(P @(Pv&&m|oPDi '>f< bE)Hp+es(o V[r2tY`y& smGqɦ\ g8R%&$^.-4G#GpGFn' `8*UrK8b&MF<0V:XGf< Пxkӎ-jVbȁo)Ak +z5,p=,q V-ZR4->;vy1WsEf7MIPnG}$j\.!.ketY|_AkDWږq$}=X%I"T;`֎\Fv?J|}7 q|ְ^Z%&L܎22+2*@ zUUTMuy;С}^g%.m_ྒྷxϑ nR'B:褾5i$:vlȦuJRJMdq.#yNi>Z„6I]w]~DNj5VW6҅?$5Cae3̫KraUDH;p<.I eʘڼX D"FdS.XֽwQ@T- |ϧ*jE_#)y(X$3Ṙލw7݄+IgZ~ rLA!(RxP(P @H;fmXa,Ay9 hٕ@*4 FYѕvcd[NuW?.3P,~U׉+xY)1\Wp*!/n.Z|s0k'̵=RdžRXœ7mkx- Zwշ`ơdaӨJ}g!vc zXRKI>3;*#:$1bUT.ˆWk?W?yqR/Ut{9:=ul̜E]#|n9 p۫f9h;d mݗ*Ems-8ϊQ¼[omݤh&\ee 2:0(2 QLƌ8f,(,suTWXzwUJ,:=d^^/NN2TLs>k8#ijDTqIA5[b33֕Ӫ?SDϩXYS6127y7e _:$yF6rH ܪZ]iExy"T:,%ձ[yHec"KJ^hU;_:Qcփqq[ώ;OES<-ۯ/Ĉ۟}7B,I3w !m ĂjG8M;)Nqӕj+lsg'{_o{OH4->۳ce#]]Ճ+0`NpZqIk!,#k䏞t$~5#ڥO(2I[L9 dTTD} -U^=Eg}a&K[hW"̎IS 9#qZig JҜ#mE}G)r(] űଊ竬p='vҕ49ge}Vp,kekxbyeXHEV ;D9Gh;Q.Tm㙮m3gNDHk-IJ >/h8xvNx5h/]U5/U.Qfޭ+-8O5{̥y4I$e] ]3|u^]Sdb2=ϑ|?-g?oM&~A6 I91BC3a@wBӠ/z[}6ݯ(/T X Qg{׉=7Ex'R4N2oO=R鼃ޥQu Fj;tR$b=3=Ռ@(P BϹs%N|c9u,L~yK}*2xSۏ/fX=W^׻1i{Y_j7"L\ ԁm\muNES=/?4cZ'Y7lC-#{GUY1$ă43 1,>m<'!:7һpxOMrKh屺CJqr;Kf=@xCFM]U5.yFLh4zBab;=L6*\Ä o#T\( wk>Og~*Ծff}(Ē>TU_խwBqh,oV\^v[xobQW;ǖ"V#>r0u?5.&r.V ҩm7(N>Q[O /X ,o-9TK=z~hB /aK>IO Oiۘ n#!P3deC:~ xTnt Ls9䩪3kU6NX rV2Q r/"!ɚ̉ɐ) ^\+kU9J:RXifr<}PmU\G(PFhq yrz|??31"+qYYO)t8# ~PkpVW/MM PX(dWG$Ec35v@DT45U-+~W|%>L~C;g/#v q]k &ehfCp Ȭ u;+&%Kr&ݛ[*rPysX{3.[ݼHWR!u`G >FH0N:*VQyOݏq)UZr^y. $d8&+˜džUjxx$FS[nR*IFUωdOˏQIv6gx]=vib+Y-mA yc`w枀CVvSۜw"rA{aiŒ,=[ܓv2\k%*>I1\MOgS9ZPI[x%s)K&@8u=#5pNig,A w>=|?7n6;9z5N'Ji4꼍&Vj/6O ]Nj /;[K:5C86R([KW~gJ 9ӊP @(TlyA6Vmq=RcQS)l jGŒHۮm w\J\Vq43h-u{sFyP, RPOG3-S;Z<[|izVEߧSF}y[6^Z;4ټue=C#\T*YH'^VΤi5:|<>xW5]I Ή$b=~q`~\C]żFW7 45e!Nll?=mVm$]+N+N2o#^z4#ïK=ZZmxtKGq>|6ֱM7:32qˌ$g,\VUn%[>7;?c+[H'Mx7{0[(Ϛ>9 8sD5j0XG^zUMiOv6]KnYVI6(Jc 3r 3BҔb֯IsMNU)tyx5[k^ڨ#vdY^gd'P( Cп%nU!Xpl%7BJIO=OC@*P @(qN#rfqnh22@UOR~Ld:cFKN_V"ȱTe5C=J#St̼iڡ$n9~OV4y-VCÒ]M%-<Ʊȱ/6Fb;h Uiӄ*uI\ӡ^NNX&/e?U[^[Y6>T#8B̊qac>#8 +o,5'"IUE~}=O'/#G-$3Ҧ3ioͿ$U\Ƀev븻e&@8b)cS?jiiWNj*jz}T-UvxX;ģt`#BT.&nm[պ+Js-./B;~9FpFy HN0pq0#i{Vt¤6qZՊkK/ncԬJCKsYwqXe/f?kmxC(ҫ*msT=^8ohnJ겝)K\^ZrnPHrkyRAa}66w =0&L[Q`ޭ\DGo,;B>7̲e! .+ܤ6ѩ)I=qaT 3IBRەb]j]1 'UJCncazg Ѣa<{:'lm*sW+"z^|0c:n;R?!mJ5aKq, ̱$"!jKj۩Ӗ|0>8RJy5ci#+ʇ8%ëRe%^򓦭֧ݼnhew͑ATP @([F ]PT)SyɊk(ƌm)4^3:'Xs6F궈<F;9Җ{9A̚)V`wm #lx'Jڴ#t\ѯ6W2*'g/ʍ}#PVv!5xkzӔ֛[핻{ntʞQ9gn_tH] M%BQ:\U?-bXp{j`<-^&Ƶ9]њ3)ᵕY{"Ga, V@Xۙ@:ckn!'k)R,<)tBk~֓q/tWĖ=ݽГQy\Kvaۖ.Sz窏0V3+)(gPzRw9Bhԥ$9N^)%Z+d.]*4"Ye ;̛dc.3N!v<^/[1b9=v~-*V4%k$Y)}a9\3Zio\,HVDKRv+n7zpWՄ>}/Bt.h֣?{ZG!4V5*82XF6wvU*$džȔJsw4S@~ g~blpфT,9=<~8x7^-,KXxǎe%"1jW=sշ=yMHm9'5-pON!ɾ~ ɇ0sn$|*UCWҔW,,>>9,=Jrr 6~j>s$|մS))fjC*8j3(PɷgCl^ы*3᠕PFHUI:o+שZqץIz/o̟ m[-6#8:}=otwo5b)lryaen`/bqIť ')z(cLV1ә3J (IiO)x//8cVSAslyk#B͛_e`AICrv8*Вp)ǝ6{ⱶvQ5|篤K|RP @([*5C8Yޘ<''dj.su3V)N<&n0CP3в~qkQYn{JHӄ|>o r"T2M(ژ>qL|ONwqQpѤ&o mWSʪv=|q0RUjQK͍:St%oJ :8M zFI$V{f/,ћ3x\7_zgX; t&:|~䒌^_PX\0uv"#y`D&yxնUY18x; *-O|[O:潳ۻ9 YĶ7ߗL[:Yd IGHHwHq\#0B Jc$o??n|5}V(ʴ-mɭiĠ`$w4Q;I3sZFïG9$ug]mE.n)c?2M 7-g+SF.U\O&qsd,r1MwT|]Җ`3DoRPΘ D;'K-Ɲt}\̍*CQjhЧMRM0ĿiԫԔӒ[#y䈶Mnrp@8'>vW>s),󶭾d;SP sD<@8ުR TkZ 5&.Zf%{7y͕uGPF@_RgWa'*MJ.g-(3v.ޝuU}-$~iUȍ@(P -|@*@v3B9!>L4bO&Nӌ1WoBOwExni:uax{uhWj~';YQLS|Nϝ2W$Q``vV2ucׂ#1K 䓏XZ-7Icn{v&rLTWPӵN09?7uWwi^xӜ'𑡻g,W8^?,Zė20}Fd.YA)*ď7A׬)[G =]Y<5jTjrTzvg 9g2B1۹#bxp>5;ZJZna7>n]TMZZU#j.I$X,2I'g8=ުknۦ䔺$GRecdH;1 ~L9*tܗOO? 8j Ma7[;52i7Oϧ EdF"P:CD] ˷\N⥝. RTǦ[Xſb]Te?'>zq&,ks Pw̍&`I[.i^ͿEF撎5bS[jKld("BP @(q T`UQhTXxg_N"x\2S5e.`ᢔC:ψb+YRk'h捙ˊQÛ $dι鐙b9Ts%[Ea톹i)Jުkz-I'owZLN->)7Qbc 2pzwtRTE$nhԞM<#l||M{iYRK7d'!2GB#%Ԋ1\gwtM'%Rٿ~2H8(RTѱzmvm \p[U\R[Fj٬vkT;@2HC_ˊX:5&J;c=UV#8>$ZRP3 "-KjT5f>;q[;rDJ&\)@w@OWR`r<"7tN6ѫqw5̈́0:\FPl-D2ЇboL?v;敾qXk,`K-nVOg 60v&eSMo:cXR_3S;ە-r'.YXxXxi6u RTEBV*kRnjY!:ok-TXU|@zpo<͍nSmv%b.UFҬ %PG椗o(Ͷjb(nAGQQFЭgmٿP=Γm$K&PHcM,ЂwJ$W.k\Һn蟽[kx(]YX1o=hWmH HykN >Gf8ޛɊHK |yXlF+%5W{&w 00zx՞>^VWiWsW#ŨVqڽP @(1_@Z9P " 52@J; ?9GZRzO9L#jNSP`뺪SIG>?}G'Fy`UU@Ӈ>T MT @(P -|@(P  }dmmfy[[v[p. 9net!@>W{.ab?{{Ilbp*R]-̈IY@"qnfY̢9 36Ϯ<4z>%(׷J$ڣ.HTdvDV #G Ü;r冶 nI!]BHEъ mū%qjyh R4J[c70%qOZ١c` ` ` ` ` .9%4P @(q? @(P @q9@(PN[P@(P @E_@ZP @(P @(P @]8so@JhP @(?E}ktϾEu)P @(۩U?@q_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>_>Q*=(P @(P @(~ JU-bϦ$v,Ou/7dUW$EJ݁N\[Ŭ[QYy,HgTHϘƳK-zry4h`lm5R{PZKeexYLsc[x[/PX[Um6K&#@:[Ųp:^´,nnaEqs$糸5#'sy0d-`{f Fs}k}5;-=MsXg2+M0~ӵ֑cS}Gd&yhn@aKl-im⸻}JIfekYa(ô#)jzƎJYWI+\ Է 5\ni4nл$k9 }W!3GFN\P @(P @(P @(P a==XF67yI&iz*('/. 򜸃HKf.K-a8-ѻ0,+qh$pZ򿸸2JKk6$[e={GCt6]wHwI̊k}1fx(("}YFDin' \VUm ImG*F襉[kzr<1O(MխTR001<<N=Y:y_4tӯu ?zk;WՖGl~G=W_RϿdcuM4>O~TwKWՖi}0sprX*SMSG?Ueh}jr?5?ܩ?ʯ-TCTaOtUYk:Z܏ w?g*;_4>k8Sm{d55.pA]Rr̊Y v|:/ZY٠uj+|6'>~b5ѠEyD#3]ɘS9Z04hb drS^CHg$xw86✹|#k6G_#kd~~ڀlѯP o5j?mFm@6G_#kd~~ڀlѯP o5j?mFm@6G_#kd~~ڀlѯPP @(xd:pGFkd%%8Fqq|'T#0c3(h>}[ӎ5Tx=)w"UF助A@(P @(P @(P @(P @(P @(P @(P @(P @( @(Qw7&ɟ)P@3@s@(P @($\t{6y]ϓv[3۷o\>m&?29c-#=b pܵ(3sTZLdv@O.;!GS;KGy|h I@(yyQAϩ]I {a 9(̱6m#o5T;Ah uLڒkͬJZM{;"\[<2D*),7i>8MP @( dmr\Dg2FF dxfBJL\u =kaw>l.s\'s#[~w>@@.hy W;#DPCyڧrrn|Gxfθh 4vx[orvy\wng'r;:073܋x[nqq{@ju_^ČLp`+4ANG G_ BQO֎{_&Rk/hlK1=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥd?yGߟ=ܥx82ÀA8'7==PEI7I}?eotJrMuG@ʠ"324圕A cүFTߪn:R.HNصh$}R4F 2.r:U PN%R=Ӭp*u)ڥSm{2B{Mo]&IP @]u.) [ify  H[FҤg٢FUw6@..mt6}v5'eФާ €P @(n~_g@7Z7?N/} ־Ӌ_ftus:ٹq}k8΀nn~_g@7Z7?N/} ־Ӌ_ftus:ٹq}k8΀nn~_g@7Z7?N/} ־Ӌ_ftus:ٹq}k8΀nn~_g@7Z7?N/} ־Ӌ_ftus:ٹq}k8΀nn~_g@7Z7?N/} ־Ӌ e*"[6iHb>4HH,@!G @uM5He]DCa8e%]r2V`A SP @(=mBL2Gy lP @(%5=d[DHN8DD nn@=ݗ?0gUv_lퟘ}T}{/~aPwe>@=ݗ?0gUv_lퟘ}T}{/~aPwe>@=ݗ?0gUv_lퟘ}T}{/~aPwe>@=ݗ?0gUv_lퟘ}T}{/~aPwe>@PP @(**Yb9&Ԏ$i$v¢q7V]ee8ee8*A#P @(( )jcuD3l8bmxP>WT<9cl[fJD^N|(AP @($weڜȲç_$vwu( /nʀ{.፥N$I-.# $ 1@(P @(6{wmP/}%ϻe;/+o'PC pI -vod1j]YɳȒ;[_5Le6F31P @l&Yhv:LiVW+xw&m XXe`OL1wvɪjXN+ m"8Tr,fsVHwyH>h jZ厭5Ɨik}ab׉w`^%n|Q 4OP@(ZGj#``mg $?Ov‹: wi/Qd ^6zh V6qZiX;ٱb eʢ=' j-P @(5I`eGT;Hݣ֮2@VPt~ ˾dk IvF(ұ9̄MA@[x6K.Mʲ]\rHb8,{'q 4P @( {^ }&X_)afsLu $j4?O]4?O]uYi #h 0ePǐx=<P @(3qCiz_ߣ _nH "'.'.vڧ5F ⡣g0cb|{utP @(P @(P @(P @(P @(P mopidy-0.17.0/docs/_static/mpd-client-mpod.jpg000066400000000000000000001047271224420023200211530ustar00rootroot00000000000000JFIFHHC     C   @ Y !1Q"ARTd2SUabq #BV$34CcEr58efsuF9!1AQ"aq2BCRSTr ?=c [HB@)[ G.^hxJ?jL&bZlx,[-ٖQ7 d8$BxY nnt}bt]pK.S"c[Km8JVRG:'mf'x݉mg:33kvgde|vI{e k̺3kbSOn4\ ) J@N05gl췩b[ЧJt}?~m-f+A&4 Q*BX߾u?f+f:ͮkRg.uI=;Bxݰڭ=_1veao-1nv J:x U[6|Ŗ6yJfd;wJ&^;Y̺\ ȑ!ӢZm (@h ܱ[WYCtRdxSJX 6ȱ }M͸K]a 0Bԕ( ;5t/, gCw&*3jxjQw 4$x VgŴ[Me gŌqԶbKn()d G bm1]AQKm.iPEIJ&҉th(J&҉th(J&҉th(J&4z U(m([O9”_at(jAG IsKMf'hyP]2 gnڜ Mr-:e58HkENhx>Z՞D ;yk{e>Ma^/WkMǫ\f7xX1rl=F1r- w,JT,/z=qS/|ҕiԠ )cyf8,Zb[3,<()C; %>0OzKl#5jnjڒnR7ZVᦧ m&Q]^uKܘ&,I*h2qĒ7uZucih;)=]R#59ˍ)xn:NXf^q\N:neevCҔ*RBc˥F7NvbeiC9O+P0hԭj=S->mjz"gg}Ľ2J^yu,|zuJtb׬^՟>{ÙF%h;{n[)XU6+ҕ&&CwV!IZJ@#SANEό{47r'xu{pHLZwcה=`~Q6U<-d]4KiF^V#vf~n˶`6"[I'ΌN2P!J׈F<5xb'mCvݱ;7#) ȃ}I𓼠nHxV'Kn!+fsGf'9mu~4`~4>w@~4>w@~4>w@~4>w@~4>w@~4>wAq+RVFF.VnZ Z$$ۡ+y_}U&6LxLm}IOo'}>[3wWK-6K[R:u׷S&խb>?ψG׿s2| ^ a())h O@x ׃rtٿj}ggkG&Ǘ0sqv箧;?K_Ns~7}ڿL.n>:gsqv9?KF=9Ι\}7ݫt_Ns~7@jzs3oWӜ韥#}ڿL.n>:gsqv9?KF=9Λt s-r}\~7@jzs3oWӜ韥#}ڿL.n>:gsqv9?KF=9Ι\}7ݫt_Ns~7@jzs3oWӜ韥#}ڿL.n>:gsqv9?KF=9Ι\}7ݫt_Ns~7@jzs3oWӜ韥#}ڿL.n>:gsqv9?KF=9Ι\~jzs3:gsqv9jr.u[:$v{yR R(T@>n'!zWkgE_cnO=v~/! Ky nC :Zk5zM~^_@נtO6!-۬9Ҕ2)VV"ur{#m}bxI╦ DOY]|pYmm,yۼkUtC.gnvǝDvyz5&S%IJ҂NuIԝ5h+t⸪Q/Quv5Kd%DD Qs8ÒM4曤(u| Ew{c;<6Um_{yB kqh @gMb#.yEbېRBh'ĝH<^m]Z}͚pt>2R@;5OKtӐn0 Hc*%=v[ *Cj{CSAƂrfyqg(H'MJ;ȎFN;Q:ꠅmofvk`27yfשi[/8i5zM~^_@נt:k5ݛN}vl~~Ĭ z|]t'sh45뮜hi7"vqgd+8s$$޿Ƿ5m/ Ru2y*x~8QW[:1E22՚Dt[o7 xqW>bŷ BWVSdOn6?v5mC$p1wLf<ԇt:y>(O q^-W7Gs3-ЖY*Hp!CˡP|1 ۈ܇%hJzkNȅ ok ?jOZ@֠tP:2&ɹE]7&V>-^ l,)|H)%zuPM/ܶra["\,벿~[}/#T$ꂒA)¹TY3T-vd]0A.+Bx/t']yw/4+62˓w z5{q !s*%xp( vXsbp?A\RۨVkANu+k{zOFj)-0H$4\VRĚ ?+#a6H;BT yk.;-*FK%\]RxA6;E&#Pł͙ʋmKPqZx r?Kl2iƷE^PA#Mt#Aw n}nL!mv J7WP P"| EqbtP/)e.iջNy`U4i.Efy-K 4Z@֠tP:?jOZ4rS9yMZдtW8+sK;_nno;wlUn2vx`C;j׏4R"Ifm6*m E%q[*'UiI=S|[mm;q7M\Ƨ>쎮-ĿuEԸm|q)K={Z+'6ݟg.4bwoknC %+i"\}~-kb:_,'R&kR">̓p^q +&Rs٘[fZtƥr9GM -y?-NGX)m҂nDwH#.cY-/X6ĮƴuV@w@5ԓAv7lQvuIP`* )P[A`e7|(=+@_; OJP:zWҾw󿀠t|(';Fᓯba&yF$kS6Dymï/[{Dkq(JJs_>m[fdxd%TcdIN('h#[ nڝpOqJ=m"$t$ה8j gohvd6!6dB|$#E+@ox4:I[ ڬ3KmDD7Rt*h K.RI[umĒ!AKҾw󿀠t|(=+@_; OJP:zWҾw󿀠܎XR-T:J2[ǫOUN>Jn2/qۼ=ooNk*$Zʕ<1L51JL-|#VyLÒIJJTtMZ\:ݳqRhhV>53^fޞ>I,~DbSh/Qx~)66yɩҞQW2S{r(xpqzt֮4xOR>ط{zR$=]U4xPe/.Ұ1FbSh] mD*iZ|v&Ʈ&$X-QfB֠R|҃[O~5\RSꊖu*$N>Œ\)PA?@s~9 92]s-'/Nk^O ͞፷n[ \PLxqA9sƶ?`.dm b%H|AO-T K 8bN\a}Fkvf+Z*%qt[Q0IuHKaMy(<^ߏvͻL';2_uDҭ4daBl[o,#^m2RJHWAٹX )dKB' (׎t-]| Rd#T0#zPk3v )qWY7㺠V]uK $p$i‚+9?@s~9 *"n[A$[ƒ|ZXUshV,,5\BKASi'eON9##r|? ̴v4!շIf\e>6&آ#TGr)6曏:VX#$45aFם<}~zcgwIʵKkhp: SS9Un/|^6ǂNJ4 MZT8VzW$:Ty̯\s_@2+rMSǡMygKmDy _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʁ7_e}ʿ*@~*u6Wܫt _rʂ>;)Rmhۤ);ءmPty55v9gt=ןCcf'nhQvRuP}u.=_}vs7Kr5!R<W:{1ourRdSsHJwנ'ԫZ6l!j{MH5=4Yx MkY)RXyiy(Y(%Sh@QjI^v12 }֐p3%Dh4KV#~Z;u>oNɑ-mGOj .!~ flp_n288'yo3lsq,ȸ6ʋ _%*^| f{=LfuiKZ Rڒz5=4 Oij{MSh@5=4 Oij{MSh@5=4 Oij{MSh@q@Pmֆ_p{^12,#Q~n41>}MƠNwCJA]s1䗳-`VAb SJ]($39lݐcW"ĉ;h߰?FiЭuF| z'be1fnY0eĄAu lס)|6MrțeW+s>GÚ< ^;Bɱ/ۍYVi6ru$/I 夥sG>J 'zce " aK%TV&m*uH:%Ѷ"ʢ~u6!zoo'@d>J[UL[MGR;8h#^l#do jik|} |$ $:yh 'u$uEZ@P( @P( @P( K3b%6,ߢr'6R}JzTX6Zlg􅕕J n$뢵']FT-Onҕ(f^SORG堩3Ci,Gzڨnf3ZJt.5VAotl)%e]HHR5O"m73oSqFSpC hG8\'x/gy4lK1v\9*@R@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>@>A P(6 h\8#͙^,_k⺴8PwA/'mŋ;] 2=!љRJTJ7:A>2}"|] wT5J$ A;o&lKfMo7YArdkXզ)w4: _gRbYχ$$mN!+Ep) J g9q}`Ұ~L vlOF\iJ \R[HQ mnֳ `3ą&K#ԇVAւv)'5cW(6Ɛ7 #Ϻ+P^np :qhX~/C?Ai4JΉl yâHb]rj6al☏9,ʎSZ8Pc@P( @P( @P( @Poʼn6}x9d8t1͸\qБ BfXvo+2Xƒ˙J v0˷2T'bOe3 C+*);O0yx% ɭ=gR e<7:;׉:y(4>@P( @P( @P( @P(qf[e"TR#@4'P&cw[}5UT{|eIm#C䠷P(4[a>[%핬A_+`[dTEGPq` I R4ʤ(@% @P( @P( @P( @P(6 cSto'; fC/ +sQ%7>oȕij9{Ka$BAh,J#-VxM  2!+tyDK&!erF9ؘcvw(I~VqPF=C>?g\IPĂRwYVNcM8^eo滟#tA1KYvvXTC"`{'^QPYav-Xb-jZLRsUxq v(/r6Wdqg%IUm}v>n4[J!; uxnWӉO]:PBIA @P( @P( @P( weq^iP7e6Mz .sYyCʾQG_.rkfCW9̦=n<{z+dm *o=]uCR,8C#QAnEeGDIaOJl:sM~WAWwr;1r\g[r$-iV MuXeT2^(.sn+>n+>n+>n+>n+>n+>n+>n+>n+>n+>n+>n+>n+> Ǹ4{(G4{($8f2}v&/\r -'}V@V:~ uW헛dMŃ&AҠ'|ۄbaR$L-D$҂1۴]"rmr-ڻ5렗=iQ*S=Q).K:kTE5#uI:@O=r\~tnV<;|mqdhG+g$]F;rc|ur"I%7N䠰'|n' _m+2|kaٲcGSAE84H:'ո4{(G4{(G4{(G4{(G4{(G4{(G4{(G4{(G4{(G4{(G4{(9P(7j]Sȓco/r{d:BRB('9gm2zߘXp]ȓE%jQnxAfoY`Ľ1k+z&lbK{!ē@:{h'rxlc7Xy%䆳W.JR+Fh4Gg2Ża+c*͟bo1 e%.H ը17-_ݓ ϶c)|, C%ZFXASMՠ:6sd@'zîw E*mwWbޗꔔV:J95ML!M3+rӌo)!0ZktTuh*9#g7h1$͏;Y2aJK {KK hԑt,6; fy͡7k"KԦR4[n+E%z+CAKV?"Ȟ_;Y *:RA@P( @P( @AWD:=>4qz[~jMq{R;I'W[96|?XӢF|Wkަu|C,>">ǎ㖑qmC-Ӥ%9); C4AblzaebGr%;ϹDTfBd9!I#x >J٥u_JZm %K'Aelm=Ӓ۬maL6ځC^; ; 3*Jm[iQV@[1hQ(NgM0%L5=I0AVvAαX[" n.;* VuНS~α&/D[B[!NTw zHl[I $ ~f*~nz {&|{(Vtl!*m%a= ޏ ~I_x%R JN @P( @P( @P( j na 9ϓR4<45gkŢS qjyKHJHFPHQ2xɈ$44 '}tRxqƒ"-ϑ]-k&*[YuNV8~Am6ݯ[ Vx뻠N4J]E"]cl[Eu<\׫ ڭ[-;.A%j@u'^P^;YZTK.>$pAȮ]ɔ,HЕ$QA6r|/-ɻzj[H ׎pA|FA he1נJr<4 $- tRxq‚hy:;KN* (S* B$^:uj<,m*K37,)<ҝʤt":Xl*HkAց@P( @P( @P( gOQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ OQ mA8G&9,Tڝ5''*[Ǒ[N8)Zuu4mrr)hokvaKI y$7>MuMEF;f. swT%.<)#ExuPO ȒW:k&)*=¹O 2YQ AMG\ <cvÐuRP׊NnF-KI@hRFn)!K>DOÍmg.9;ݿ1IS:-.wHXIQzA>Fd04f6L:*M '-I?VCldnmW\$=whQ.6W*$t(5v1f9uoCaȂw\m҂;uWBtY/i=1 ~iZR@YZP% ǰyfdppW)Vy54I #pVv7L)ԅ;h=^r9Xwyn\헫k%iISRNsH:[3 Mq.Ԕu(iA6,ExfHgpJ^gv, 8wA_7v-ЯVDrœkx) RN Rt @P( @P( @ߛ{-w!L{gz:tFu{8HNHP=t#g6Kbm-J8K-)}*Bt'y#MGZjCrH Ivņfכ r,TyАYm!my']5?.ZSaݢαEeuA~$!-Ŷ0\ZWGfOǯ?<ļݗ̘K*}m{ԒOoK\ ڗ"׶I;hVd֜߹uqDNA^6 i]>㴔 Sv15´l-ZЍCA3}mAX&͢}nC-%PHuA6?Kn_/Ͻ0z"9+C4(*:zs,\'\1pIieL5IOybܶ1v !حуkH- *8u!iLZ^9dAel=+-"ΩF~QSZ" ^9vsW4^cbbo}3a<VFweOm6͆i{~EsCDW).$< NiAw?vI"ݎfP2 GT2"d-)_PK#re3kZ[,"QU}I D٭@P( @P( @P^1: hs \1XS?AAL1=B+HgW?<~/h(e/6HS!#Rxm|TǢLYLq^AJ{q2yw@!P?#J]bqerشJb8Դ!@4p-"G̿jӱgG%N-GwRsJJըqPp&+;aN{7]yzǁvmll3vzGG5Qצ9ɶi6ڞٴǚͱ#q0Gp$u4v(د7EѢGrV8y1%aO^QJ {|c@jIq^-(hT8\c?:%bebB?S|1&xb\y׋vL6jBҹa甕 S\]$U%"@HBqs@dAbl‹^n=iI! E%N%$: M^"liV͈P\4l \7h}=RPJt%MYζIaԭ`O 'OfsaG*q:rBPSH'e[k*qKK$-h* 3 hj&>E7"t$A⼎.ʹLU/+ ^Tdލ3V\ǭ[z\#.Dz,&3(qe-'OA/;ɧI7ۓK\e+R |hP7evqtd24j[6#P'BC8̩QJq;I`h:n'|n'|n'|n'|n'|n'|@Cɂm{KpRN/sEKʕlnqƐD Ц.:RR|`Mt- Bv$˘K:*Jụ?{gYQXmwfM񓸥(A@׀Ǹ'mkiV<#㋸PKtX.BxئݨC߰{kkWIw%zO2T FNAgܔ6;%$ʳnYz.K#)JKa{ 6HqC@P( @P(2>yAf{*rS{6P3pmBFJJTA?̮;pr ;hW;'N N*0ȃt֥w(K-IHZun*pi|0YJu *H$~ ?ke/Bڴ2kjT`WuCTxJ agn\XԎzznI!;{]UK 1ma/CRL96M7|H,ܮEmlWX].SQRc{P$k/(/7TXhaK]ևH,%R` }}d{gɑ{d0c74x%#0hOj}gL_}ql[6vyNVx4)=Xv??Esr;jfsKe@ˍ[A+N; ڑ:6mGq`NJ $Eam,Ց"g% 8 -_'aw嗴{3.%92ki7Gkp'O- P( @P( ͘'f0(/AdEQ 5_AQcy%0E}.Q^U ҂pٵ̕G>o%NVuNh+3=ܮi~RR֤nxx-)vN:؅nv[|UMh,~vK8D:qEJ J:ԥ 73vQmRm?(@;::l 01ԧTIp7붞Lfe 3'SC`o]⟮5@P( @P( @P( @P( ғgMCmHpmV|A-s‹!k0%:$HW(,FU^bnjiՄhAӀ#:څ:r*<ڐЄ6'N4e6^sUl"olj $ı2ZG"LTqt)SzuՍ[ ]cR˼COtӨkAr4G[1J % u]kpܵGs~lk0D>ayЮp4M(P( @P( @P( @P(~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~r.N y+[Uu8D| cJiŀӅ&;rK԰8 ۪I 06bmI1(2[XCu .LX9MeDzs{8H$i^;VRpybؘ=&[/%w.Ϳi]ު B8Nv6{d8phs/DKZ蓯u=Ph̆R@h< @P( @P( @P( |C*=?4vnvEeN:/uĐuOǍ{-g6m4}1l%AG6`C$ꮳA)mc@!y%V~sUX @e O_-?ڽlyz??8] }qŲ_.G}`>V &Сb+hsg-c 55NeQNԓǶ4r巙 jA,Y'JK 1NPl͈9ic 9lKTٹ敏RwT0@H^ n9FlcZnyp 8x'm|:է )$꒵{xyP( @P( @P( @P( ˃dY'fZZ-ԴAov=x'X6C|ouo䠣PLgfg.M-H&4}u\:kխZmx81-ӣl0h<mM8-% I)RT4 %Z أƑq˂đ˒RRGlx'SɴZ\9VQ]#sȋOYPGhfϲ{[nk" aI>p]h u렝w}Lp\XS+r:]ӯrn OfQړ*W+*N]AikgUlfl]:)RIӯւocuf#toqgs ct]}y luܟK"RWF@FT @1?Ō7*1my"0E ARI8utFŻ]gG8xKP< GTqQU_T}/?@*>xKP< GTqQU_!|DI4 "0x s>9?#g=AL΁fK`9/& g~t}3?:y9?2p3%m ,@-new}/?=e@@(_:xG2;>΁6Q t񲏠e|: \,Hn-nele@@(_:xG2;>΁6Q tw/?^R$xGT/؟΁NM b:;ɾdAK΁nK`8;$ϲ?:21#,s>-ڑBˎgthE?AA}t=8ʢ8 K[zlߍ~4ܯ8h?w|w|w|w|0b<(*{>y;>y;>y;>y\G;oƁ~4[w|Ƃߖ¼@@@9q;oƁ~4[wߍ;yiT=Nn/q۶'iU\Bͩ\B|P^rp|{M y7uH]2"ƺe(B]P}@ O8:|Ƃ?] $tQ?kAbi4>@>}Ӿ|>ELof|W.LneA+q)}ZO tr([0_) krŶxQ|MBOTӾ|| g_=c;h;>@>}Ӿ|/z n>@>}Ӿ|FNzwϟ`t>A-j$H7`Ϣ_Mz02AtیG`% Ǯ(=;ϰP:zwϟ`/|ON(8]v꿯=]d_bXqkGqrKMsԕ>V_(,&pu|b5P|\}k{fҗJo@]'| ONEDЗ+HI i(=ﮂO/x :c\\Wc"ZNCͮSBVTׯZO]rK#yv\Znq̸nA8_6\lQ2ɹ^KeĶԕ7>Z$~t Q;@G0[L<sVo+ɿ#\c?Tl|v-kίmw;LS; VI;P(*m)6{IS"u=hZHRT?adͰr^mP-H/dFe ww6tq4.ګ)7^TÄA;yu77up4 Q_PvPAkЭp2lD];q[3IBBRy* Tu4M2K׫l:#ͥ %EI)RHſ~ƒ<T uJBoxh9f?}8m'U%<t=#x(8#pyC%AY*1CBwi?mA8IL=MQxPmG}J4{æk4K8ڀ)RORq;pA?sw3 sw+#^SOCI?X=T}>QN*;>};>}-dξ[lGDu@^PK(Y)P:Pu^}yRI_[i5PTw|}gϠw|}J}J}J 8#{iw x+iHւAuNOiAH=+(+(.zZ[fŶӘ CN5#C _}gϠw| yQS)#Pϟ@KүXc*nmSɖšÊw׬{h-}+(+(0* 6HR({Ic,Rc7ҥZt l~F}% j<`wR 5Q(+f?`3]l˼hs҄s(@*N젷ퟖ ˥le;E`_r?"C}*:A4'S˴<9~KƈIQRzIO=I8yN 9h5*j^q @ӫ4I0xLaIS;uWZ/@mv\Kfq# PIӷAot p[AE/h=zwfL;3ǻ&16{:Lòi8RR5 =z~ b/ȧF['M{rB(lI1Bt'P|{%{tl08ݟ4rkEkJR4lP:m>6~ں1uj-ɔ Qx :<A {;њ\]"7A_ەջ!!A==Ƕ ȑY.C_3[iv96/Z<y:A]zɟ2XmmGm3whHJPPP|@A&cN^K!7h>[65ͻgޤY n9ն p @M5At?eIܻܝqeǛiQs 4$Pk.>݉_q Cvݏ/l4d6H\i技 tm;]˻lPS5SA8v:|˨q'U!A@u+gdw.[;zD&uTV4:j HւC'=:ǎ}QKyZ 5$:HPYls^Qs-N'#uJu¤z䠾& CG8o O%)R$+P|=A(zmh&. jȸeQ,nXM ݳ|{fvv7cu7vZ\LHPZu ;vK rmΈ*N$P<`SVp`ttKIo:gA7iyq=B4Ca;q;șM .2.0Җ Ӂh`[m,įt"-awii<4deUӏ;r 5*mJBdo(k<8^vm ͒me7BaW{uPɎQ[ҭqeaJZ؎O^ ?PYh]#@P^<  ٠tIsNI>٠tIs L'S2|s-]T7|P}t~CWϴ+q2|R|w ]λZn$>Ľ9'\fӒ}%϶h9'\fӒ}%϶h9'\fӒ}%϶h9'\fӒ}%϶h9'\fӒ}%϶h.l. ;s:in?SӟQa]v\Vh\ն] RJT quaZY31Z͡OL{o`W;nRn[:;VfLff"՚5[lIaFj+ɪCσtb.ӔJ*΂ ݟ]> .Vzmd6^vm+ djJSJFA6Kم;WkF.IZuCCtwH3͇hBlF\7ޚȭ5PM6ǙD6|PTG] mca) kA Ϯݟ]> 䥌Fb!*TY[e'Mf' m #E'R(o#(OAf:\z6of[fBPG5ꠉrƣg;>BM؅J0j~g@Ϯݟ]>v}tfӒͳwyp& 㐀\BG G$aA=/\\Nxd$t9Rh17v}t;[6hzkBZS7F<L'g<6^4!8%> ǀ]^vg7eW],%ʈ $yO]aNBGtHit$;렒uu. zͻkmHENRx& b]M>v9E\6yx%KmQFjC<[[3m7"_mtp/m#;wY×+ ]ZR-sYeÑu,O@|[EN[Wi11ᩥj [x㷤LKmNG_s4w:=z#VN֬m_76ߚ ?Xܴertu Uj{Mdȕh(#@|D|7hϴJ7s56Dz.|r^U8ކm -EJeM,N]UMG \s m7.m'hŞ""yݢbc1܇kOÅ+,̡2nN7$@)&9dž&&w;x }'eh˞bb9wfg5˕=˶̓T_-m1_LۄRgߛ{MV1'$q6 :YkSջr[K]4AP5ʛA~ze}VI2"JxS堘e\+; s䬭NJ'4u ?unoOo+y\VFSh@5=*l;k..-ajPM;h'ߑecOjn>Hm)I :kAțҝyFZ&\沈<\*Jz B{R|/Sh@lW%۵Ą.w6<;5:(+9Pd\ (JT5=Pajλ6( H A#Zy 0/9NTR7P7PJGUSh@5=4 Oij{MShA6G=ٽ~22!{ĸ$U]5>J c9įYeY7;mFIt{?e߲Zn̽¹510v3.6$)t68m<=U:^/,Xg{#lݬHƭ{Y]1\䇂iWOV뺯m˙ÿ,d|/ɾd7ۤqM!lxHۨ뺯mÿݱ0Yrr!<Ҝ)e;/NM5[^״xS #:V6RTB^p A歒Έk=S2uΎ) wGOg_ LgW:d]"n)TRBN(#4 imE[jҘoT”yBJt┕I.p.M:6{Kn+@8n=*6Lwc^.*-i{VȢFxv[`H7ʼn;IJNDկ/@P(';*/9d˫ֻBmuJKeA#D4 Ďìx޶o=s.;.[tHeCw{]<$#iAn- -RLun,CF~&hk%)7ׯMF@P(&2=jiz;oZ*j}D@:n#@|bz;$bLdJq1 EJ!!#]T8=}T*o7E(џ{T7Fjm-o.PZhUNOv .5\Q-m㭽odl2SJiהHI5( dlnW5%_O7vTK/d{BA 6 .L{}B{ej}Tr!J {Ĭ98b*cυ_CDr\s0zL;5;87uIzz Y!]3WjsGʯnegee+p)EvT%+Ϟ5t FR 3nb\ہV|YN/Ou+&l^%ZQQX]K-Gyz}nKM@}>h-c lwH6o!vOA~[3!NԴ1^@}^o;wx܆SWes[0g{joQ~\ Qۑsv/h9^}j el0.A% /^w=؞HGz"l}*o~[%#1EBj /c|XU-=\#K0,>ES,G g g-Vgu={}gO_ (mӖ$$(~ [c:|Tf!걀s3>zcIX"+ZLn|Y^K%tR7|ڋFs޲Si$ -7gg*,GKsjVVN|2vK`1/˪Tig|&>4}^pC7~vUx.nbǩ8_L%[97eՎVY`{c|GIQ>5j|ݓGMVeBMo tkl_Q{g3"*%HHHG8`R Z?Qy+ "YQN%KyL,_xr{륋Kc?M}/)~e7ZV1sn>[=`YtQ a I.9?}}H׽G!Ofo`KHn{e%_ñ{V'4v^ ֮`T궭0.A%]D}::/`# }n,(>no돣U lį-.S Gj[;e(^/mgćW??^g8psxѝ&׏1> DžV[|dٷtш kK{<>(z.ٟ9@ZY rsdc'Gpq2 6ΰGY̬Gzۈ`i;W=록^Qx6x;7A{thxs4].ovKn%9|&ɶǦX|f@rHUnH8꧟~@+?` ?-X{}`66666/8V@LhkuVh38`#0}`#0}`#0}`#0}`#0}`#` OU?ͪ8d^WoVu4<~pA+;~dF[2O5칓  wt˿Xu\y ?2ƿ 1q ^#ݣ<âϲWJj͵!R}JH9B<1a5erxoIfc>=kQG\J%2O۟ ꣿ eߒ.@-)\\6()ۆ>#rtRV}`H /+oY_f`eҹ0pH/.GDbٟ! .!>m*RL?%SNg[mTЂ^`Z$>o[N;4Cmj "4RZ*([H۱n;taf2ۜk-%_eǁ+u< 2G^}+YO!V^^ 9#FsRv*70WN6Tӹ>n!˼ONvOG8d~!__{VA_}LN=1LbaeF*ۦ۷$꧟~@+-2x4g<`s0} Cm Mnnoz~Oi>)=彐~[%ĞDӚ\#үux/m=QVtqRy`rM^?_J5uOGxсb:jmkRKjs{nLy4buxͩ}%7,= BynYsj0q;hrsPB|M_8֩U2%}quI iS(^4"(G)ޮGߴ^Ј&7KXZ-^Ǹ,vâ㠩~]O6L2X8 V!3z'vO*S/^T+.Mt͇k׬T;9wNtffˊmۥʖ(> [cA,{ܤѧznE?rMHqs&`:J|^0=88>ΈKNA{ -*+ @6]l5>K 03[xy7 v%`3*ps#M&m|䰝VW7iU2]e8.Cg{,XYg{=Xb-^UU!h۵+?nrFvm9M?R4GQ6* Yhbii .z)9W֯_۝9ϩҨ/xW_[s0JB$^ke>z MaxÓ{Kkzi@ڃwuǤDW7'E!1U٘T୆ps{^~g.ůp@6c_yIZK0=i٥IJQh@ ۦSo8SL*&3sy_߬|[+zx:ܾ*Qrh'ySoݸL<5Q5F [p =+Q`j*yk ܞkIB=zSԩqX^]8!cX6 Z QrT6Ý}&ծ&7q3(܂Qu򔈲_;z=8⛾DUK1_ȷz6*k3&׬T&5g-ϩΩT+vW*]S_jn+r3Al<hrӜ;R7 -~dJWɫ;ʡ)zZƫ' x},rwxub x7BdxŏYA?297|TYWDrN~W^er+m4v &ѢemݏfҮb~sQ棨~|on =;>plfG)FMzʱnQX#ɏ6GSaiWѡ ݪM/\R#eᕽN^Y o@5{nnTS9t[]WlR9s4ŇB@~կ8vK:+5$]3.-8 eۍ  ȕ=6Az9i_vWT\QN׏I1vߘ5#CL0@7[kd(+\Fv29YQzx&QcgMJ3rŠs-#WR|c29GQD9R#cY{.YGnn7xذiY` L"`3@6D z7_IJw⏰Gҧn?*>{)~vt͇u}N-}]𶓐;Fʟ $Rֻ )3>Ob1%1Qcݾ>P}5ⱈ^nh=k$?<#~¾3V[W_V)҆ToҼCX^VnJ)Q|a*)]Sͣvsb_*ӗE1䨤d_ۉ*˵q̲TVJPz7K!pߏkqˎv(JC*[T͏SۢR3G9ҸTpgdӍvGߘPv"`Ώqfc{-I <-A#^`hC?vQEbPN޽,Q0?œ{sq+K;=͎>J L5l}1r8MMq[H; Znafֻ=p|m8˴Ak |9..5bv>jZq9+]5ir*Ҡ.-,K)k}lvGE۬|A/'WrZy%ӵHY!r#Z]6]x"ao<(`H{OUܮˁm%+16Rkx1Qb<(ctx/oQlsX>QGc;GYQ~n8vr+]>^@dL7d*^FfK|yǫ(s70s/U\'lF.dl?AvOgM5^pc#Iv&j:z( [r(/'P߾uWV˰$S^CMy+U?>tNv(qIҳxՔ*P[ijH=㧿l$`'+&QѡCBޅұ H;Ec{M")韏[R{2.eF!Z,WpTAJ&Hc|AP>kxLҤzRڧ]T,/U7gVe ^ k\+3K5yZ 8=CنϨU]RvFA}q&w85GQACD`P03F+O(mp litT>iTܞ \BZȿ.9ҽo3#zi]]r*ӵ !( * . ڕA|ۋSN\âvLؙu s46bۍ:2PH@#mNؚ(} ;t֣;6toMz;ml5X0~ZDY)ESWW ;i6gXit 8F?k(r-K4R?[O,IFE睱:Zs%FP1p *@ޚv?0gM+)\Sv2mY]޺Q'P6SM"ǵ&@QxOMǓb9*HQKx5ڤ;.Orng刔@}7[J; pkW!]Pdf;7L> NGR?9h|6;DʩWY|%\֮_nMǮHO>])}dG-iL81q*=_b#g5zt{&D*;z~=sm^SXba[US yٮ{TF#( H{Ո՗ָ,WywT0u,ys(9}XK7!UF^ =ҊCXP:ʇzIPd/3$]}N-\Æ0~B=ܞz) o}O/"^9+\>kc|cohze GvR $ 7ÛaUcQZqdUo9aQpxҳ5oW^HZ8ǫms;mpErnqΟ*='鵣ݶЧ)Q{ܱ>d+kvp&rnqf?s+njM)mNק6@#/+=l^y(S @Sh-(88'^Pkx=M^bt})kJnZ~|Ep$npzT>ٟ`8nn7_jA!^q$JΡR翍 eՌ3ƨkRTyzS=[|lZzԿLyh7̃b# gٽrRQ힣+uDdhatD>p|M0t5}{(1H!9t辆[! t߸ڡ PֲzOכq`X%Dzq_ҪOP,}Z?U(jhzlOe۟nы©3,] _eD0D\o{t{(p)@gHz +?J4CG pA?>h մO nn7+@I"fK13>` ۜ`#nn/ct-6gi~MI;9s5ۜ;F;ƥG);'T,ߒ}}2/ ̀} 7j.PϿ)S4>՚v<ݕZ[Zɇauxь50,-3EЂ nG onWTp ɆlS&u /6eG})he\5Gx0TsԈr_ٮZ),/-e| &lGȆls!^~6k (4jڄ5yБy#$B1)~@{Vbe4PN*JTB):"M24E0Vޯ[v[SonX'3JWTCsDJ8%gkʐV5l7+&54`E^,AW=Ǐ+^ʂV6|>B9 Fٲ3DoOČقW2ӜVPFz*잶GkR/ǫf)ݨ/qeߓ_I\݈mXԮe܎ @kdoªl/tljfǩ6'ۨïOv$9Q-?[竦H Q"@'Inbx8F`,ϻ_?yÏC|MXz;|sJy-aux^Q Va=4pmg iJ%ME|)~^^CGb=KgF痤!y,VD%CVʴ5Eeu"U09vXBm*N ce$~>H|v}>Y:P*5[ԁ([vmN FGk?RW v#BZ>Nӭ?VeYߓl1nP>R>&jGA?::{8e'npon-P-veiz>D3`3(~YO4Löz{.+з9Kg<+Gy7^ʖnCVVy.+FӴ[ëa<&T}\qǴdl}y{3(hxQLn[=y.ˢgPLӱL o`¸ԏnk7 r ߡ2!stܖm~*u~oTny{-9tA3R G%hj)̀ReMJ $Zps{w;)ߖ25$\ZZ v;gE|MC_+`$v~5^ct?V\ 㰇ߚUR;ʼ0Uo=}:V3H([v oEjn@b PϘۯD-,G9-d#_KPCvU#[v]tY9~Zc^\K)J2t)=_zK,ĩ8Pz6+~Q7$潹eoP [AudRg >/sJmc1ܞHۜ=Õm;<>nXӿFۃgW?$4K*}fn0- vn[xɇ+Ąv;١]M =soË0p$0w'tskAh8_ÜU7B=c: Ʊ!ť'au4[cQ״g/vʗXjYaH3ѐMF؜l#I)xUC9(rB\; Pu({QsOo{Q])zܞ,Y,BL6<]y10jIpRT>I6"ҪL94YJ>w&ƓټPY/o՞f0|לc )ٷwiМj֮f>|@o-;w{*M+ J=jij]Mq@ ?#56jiq G3=]^dNA=lgeۉ=[ڭ]kz7cPc7+da,yV*@̛W4}6qmbfuշ9Kg{<M7J. Ӷ|jo;/Gp}.^G_J/Z9-]~8j(9zE)y3aG0HqW&5C0I?V%9F(n{sM׻V=w!jr|H¥.)k5KLCz,^j*z:娒l=ڴsI?/%=Th*;v[|}5z.(_|M}sDcX=]WZ.u/ixYW?tUU+*jڹ@NJKT~GuI׸3$^x+ /?]^PR˘b=?L}s(fСz4g8夢$nZT-ŕh, -yon/K: ̮B2F7>T= J(9ЎQc+݂0ռ0J^SfCS}xU>Ջš` RoW?2t:]sߡΙoŧ^Wvy| ;Xvc&[@*驔O6ݨK,=mYˑ{!*!cn)Qrߺ< 6 Ϩ-[܎ l˲ԧ9 8S> C`3lnnNwwos~-W&E6~e(3pը)]qcEK}O+RєW,g[vx43Oo-S2=m*| ,⹼{{S$|ѹbmQ!X:4Y-[t؁Mwz7˂KNV.^`{TNwu7?59:˩qZjRFTZ>!k%xl7+r$ʯ.9?2$SaeS~VNpm&!Mqq W'7{_}SfԸ;vo; %UyWdۥt۽L3=[`x߲jci!~~Tal~snn_yoHrMT= JLS&G!}d~A~V?nn7zDýX۳C6 >0̷ W)_av ДnnO۾r--˶ic=~7{+<~>S),.upk] SI^֞YvXՀ *4 Nsc`R0wZ6g؈7^H4}#17˂# 9ɕ\8Vk;DF2,{~8WK2=QE[\ GM{T=a!Qͅf 50#_y Sܮɾ?,zAm0p/Q-ΰxɇzIwyL׸'/@/T~qi:d_4"F)][cFl$^64g=7znjdճ8pZc7=}lz'%tMnnoEɳrLp[5Ul+W]SZvE@J:C5Pi((1VN(tAqEɬdqx=2>Dؙ&7W;%b bI>̦(=_9.) 7r}6ZVQ _11( }h~se[EC}Y9R(`f{ڲU/o=W/ǝzyzWtu=Wdק8.F~Ku1 nn7ë<  k^)>>sOY:17K;4MEG~="Fxon?!Ӷٰ c;|RjPά'ŸayQ.T^#&2wƁr0Rڒu@HA[>>ƞUvtۯ},a )4+' &[,J WU d]"LbX"K[oQrion LHJO5XxNݣĴ[9IT2{Qk#nn"TQdI#pN$-u[~5-eNQ Ңͩ6M3t:X7w WF6ʇb*hNRv;ۺmeNXnn73w?,;7V@翌V&U؈_9r$հ,hY9[ }tc㑃YMɹZQ+ѐ) 9*x&b7]SӪ5GkNj#(9NZnxp/|K dWC#K ͯ$>dM7e /Ekx&/@)kqܶJUv50ʅQ0NYZҷ-+hꛖ_6$͹s+w">=Yb:JGA%+ `O_xUZ? 6{uJu|!nUD`kZZajqXxLDŌj=^\](+@ϻߢ.'#R&Rn>x>@|?~h7V5& j eӜ`Zl],7}9~9c#TB86gic㑃YM\>Je\cg.hE!+jpxU7V\GM-#}~^lKܞMn?Ew0m9T _K|jޡ^@NK{7vy(pON[xL+*PIlMER)'~CWrV+F{!_R=t nn_vnppZ& R ^GL2jUGkTܾ-z!gDR9QX_[Y]ɋi9nn7} k>.JGA)ɶòOҢbzJTۓ(d`8ml{DMKy1;GeVZehjYki~v(O˞bZ`nK̗I@XBIIW$==ZrWl|o9?ceCӢ8Ǻ',Bnon~]6C=}Zڏ ~ps`BiLSY>/@)Ns}$5>o&&Cfmvr$j!4?'v 6g[xeWjAJ{֣"iVo5)^g} Mhyn,uj˒ִߺ|Gx Vf)5w(j օ|C5 .ek*zxM%Nj4>Xs7p*t/e:g@=_I}*H; {QBI K֖뀓@ };vH c,-~Dm.s]qyMշnϫ 0~LS&ֵ,5 Qi(i5-R{>}J_nW !"E^k7GO6: aoO!mcb4~ 0 ^򦷜8 eN٧^5 lʎCWi2AIlxgYr"ϯR{+Hx1+GkHt뿖KTZc!+ nn7d٤alM/;` ll7}9VXByTJIwۣosv[!&*}bOlsiQIƟ=8=FzLB:=Y'7\EPt6V~^@WyEVrVl? ;+v1|{\}.ԅ׻-z'Qp`cx#֗$GDzAoo/=on/p(ء" =Cvte@z*jZ_]@K=Monꍫ_Pcdmnn7} zT^kt-{Qʡ$]n[_=5VVps;=>,ǥs_e Q kVQS,X&mR^9Dgo}Snp4:hg\ oKy'rnZVY7=:(z sǏץ].J}Wr+휕z=u!nD;^ϔ%K*cLKY:UWI%_j![nR4~re=&_n;d\*5{nr9YɟW; BHWqo>WM&t[_O.K4K!r Ml.l~n 0'~o0d97Pϐ:{$v+'PNSx8ul(x]k,Xnn?f$:&d=%$4])Xr HT;m`V7}Br]ncY77P `AIlMHͫaI<9֑g㥗7\?PN[LWᒜbܤYvx3+ƷI6Lq3@U%>`6G0wۜpsqܗcwJ儘 ܎}t)g}~5i>~m_]ӡ}G)^mѱ@\gvV,笝nGQf~׹RɦX]'=Q^Ew@9KvϨv5>VWJ].eo^9Dg3^QG%]|Ex=OkR=/!k<"恛ۭOr9d z8Gɔq՗wIiQSnAFm8Pq%|'>/T1F:%] IZ#Pב)U{~ӷ%(U[#QPqv]!vOq~(=ϮjyدۏٶʦD%aWT5sL9dsK~oe&e}k\7ժ{Nf IZR_̆߄㥸o%9p|$[eZ/W]%el| 0\a9yxկqf\ /;>>{\9O{N`J)Z"\T)ۜmʑM4-NHhѴ 1]K%Nε^mx=@e Q)d힬pݜgb[Z]O9 S!lF+rod8:Ǐ]v\Ybiekb:>%yNWQyl$ WSm*H6s޴G`}V{W7 50~o[[d]ޓeg*9-l|7GmURIt V(i鹍Ir@OV׿9 go_˞FS)'*_^9RH#e? g6&DWl.9^7 ,{R)XI)`Wyy/&Z)'ps;}RhYwxyQÄ2(d,SvPzeYُE G.g$Aҡ eSF^fps{Ɔ+{7{^W4#g4֎"-|nnXRo& eo, +> 7?ʷHB>]<S#miU-Feur-[l^V3`,p 0*+3R5(QW,+srYY!V(hiT5ˤ+=nn;=]RYyb]em4( Fi ΐ#"I@ex (L8v[KA{n?4J)!XpE]N2ާT=O B6ZZf2]lD[a}Y9ΜZ5%̶^nnE3~R'UT=F_FC3c"5, מnoyW_um%9]n*ps;fx ey`D1E}7Z԰󪏴맗R[6U|om|p?.5I%mSi,}@~6}!,ʣMp W)&Y&7]7 o9tcWفo7\j~E[+pA:xtpm*\sVH;LTNScȻRvҕ-D$LO1T0NRNROE9r8]~$SvE /K4|鞵Rx>tb(M':w2??~G ̸zGPe{0('E7 /#+bs/*'ޢT-Y-֌]N/(%e/lOki 0>7xnޖUl~c3LvKz|ΑI׽!_ eݬOہtIZq [ OHs5oq'NuF7S|’ }ʣxa_EOW{|UHizz[0Vl74G$mFQkHykê7?꣨n*'*b=o4v;vg&VLedܶ|' PP '5}e^9zO@on,ps՞t-N4Nrc,iz9ܚRG|ٽ;Aoq0& 0X3,8`#0}`#0}`#Zci ۜjM)ԩ؎"ͻ}8rs;@vOv 7jSN37 q >Ÿyd#sr腴`Q{ ?(RBC8nty&2{r[xOrB6/Gez'y4,5q)nnDV۳ >M\&GŢgr ^FכUSi((1; % kUx-S ^FǥczJgE 7b 5([v=_ouvB?6~ R8ӖHjop]vr|=Fst(;^o4o vlll7-Rm6pE49ލY9=]/Ϩ#s;_@p&/Y؏n/(̅?@ Vߓi cQPv7_UV>_3e 2je%on\؄!JÐW6ʱ+(7׸pܧpb+۳ -3!,nnϰNݣ"#nnL3 چTHɉ 5vp!leg>Yly-x!k =mُ 5"OG7 fX j l9LnnMoۏK)e_91]{%N˵^vu JK]bo~vkc|WxLqZq5 <,t`.V݁~y}O`x=8GjWךp9}7t^GgM! }qv ͑̚Bo|>t[_M::+DZtks n$vF;., lts;3p*9-l|i&=/Kv imťOOM$깍K7 /א7S)'*_^9ʷ i gߛ۽+[П잶8cF=w$էQr˳RIYoxhTf޺g /LL؈_L:GwZK#auxa. h ˹#'| /[n@OB\wEᰋ-V :ܞbfϢP.0û٤aF7# E8=DHQ0i,obߛ[ׯN(jvx3aghYpxF`F`FY?%31W|[jxz{Ȟ דhlؽPɾ7go7juV:_TbcZ Oz%Mx|;H_ <_7>Qwgieps;lũ iuSPFI}onE򩱊 `#  }{2ܐyHT¥mѷ7Û;CȂ 6UI/> {`66666/oI>@q;q;@ pxF`F`F`F`F`Fq;l>>>>> ]^`IENDB`mopidy-0.17.0/docs/_static/mpd-client-sonata.png000066400000000000000000001356001224420023200214770ustar00rootroot00000000000000PNG  IHDR sRGB pHYsď-;tIME6 ИttEXtCommentCreated with GIMPW IDATx]w]E?gI6AH^AT,HQ(4Q+RD CA@)Az^BIH/o_9?clnΛ{g/D!BOD"D1r"D9B6Ffh"Dg漉V##~G4BMлf5~Y}Y¡o?p+ÓnjU}|?u;Ч7!yI٥s_|ݽOf+rӳFag)Vο+zaUL&SQQQ|>A#DE,QEA9sf&8ᰃs^{SC8|Xm[~C9p1s̙3g}4{ɚS/sF?8 tUg~\vɏ !(#"}mwl /,eM“rU8wQ!dEn{{}?ό.-θ??IIdr%snfۏs׿=}zؾg{L\*^cs)F.]?ȣ~㉇Ey|}q!NgϻcCWm{yk.:."|xއ" 2rP~խiF܃ms薣e~ņyځwp[~Mįg/7}Z+_}/{f/ڃhZڳv۸lOse?teOhkeD%g]we}VaX+.γ{\ut_) W uK#_3ɧ{1>K{txuFN'c~Ԁ+xOѢۿ>WӪ<~~-W]%P;.M'"wc.y׉CB_Ћ/~lWNkn۰fX‰}H/ʧۗDYʩ i>īP8Xd] \3C\\1xqM*c#}=I_|̻2|Ƌ<)O^O|>~~M?Cjf[0ĉ =D&W7]yAwȱ^?au'3@") (bL V=s~Zc03]k!v K/j͡|y,O\G']TC9?ʼn8򪓧qooۺBk<-4ccO1a '*\X=~I."lf#Q0] (8@g~ߺmyYlQTblGSXlc3\z]no>`k)3,`+;JsY?ؘN)q {9 㚻4V#=_hm=Gu- #.Ϟ87rwvd Ʊ|^A_u>o>A#DaX;ևrn>D9)r$D pӥ1cB^v#)t=ʤOO:c߼UO(7p2 T7aV߾U!Txn1n|b?=*FKw.:lfLdݘ#~|p/.{ݢ\_?*8 `^QmJxwoݪŕ_vN!k2wλRUU;an_~]>A#DW{uw<\+ e^8,9[眳x3zkN\ | 's-wUO{+lUwp=W{Y_a o^{#?Ow~ u<糮uF_ \5yǾ7~ո~vV-+̙wtu?S^1K}ܔ\WaГϻ?nP*xS;h48ۆuq|' 0Zª!3g,9U#J'z}?ԚNj=ۼV(AOҡQ8^LnBAifpfzf8`&Mz>l}WQQLmpxm DCS+pyY|}at!B 彥ҋKGi;@i*U2vls\~ /M{eq}u"D11r`B5H&1V$Bja]H/^E!BQ"D9B" nh!"DI'"DiA"D1r"D9B"F!B#G!¦ q K=D|Aee "b`2DL@Dl`fCDD`f"CLDD"&&60D @LLa`g%&5cZk"VF3#,P "Q!–aDP_[ikF!&===\Rj cƌFjM RJ"`~=N1 6V"1 cH 20hf& "bP;W( 8kl!7fD&6Sk/A6 F֥Y ]@!&/3aD$˅FR.2FZcIkJ+)ښ̬FbF3@jm4QJeD!ZLJ۹ZSYPp!&ȴ!MR:af2F a.ц5!b&6Fe01*P@Q #X2P 0 @X QYH鸎D R1 PX,%GabdKk1 E&@b`c`` I^f !Z+)&cafkMF t 0*y4cAr9?j\4 b㸎( &t6L 1H0TƄQm"lj6ښEI&6RQ%EDfCd Z D   2hQ )0!? 2lۭ9!ր@q(8 @dV* ("˖-!‡cn3Z[/dFA@KD.L @D,Fa1ȄxC*emR*8cOeVw~z,`RLd=>a4 /C-º ~oCOK&sQf ZCQ:@Hƈ dWN3ZJ`FDcȐI29#fkPHv~vΎ! t@}6'0;##RR;;seƞ9o-X@l_=i!mOSrܖ;~诎/s6W . /+++M,mΫT06^w>F^ 梆 h~&UUW~Ig L:{o*Yʆaرu C8l {SR"aC`RJbB84XUzDTwKgwf~3Μ2yG}ݶXŗ^0Z{yM#|ꩍGuRDhla3X F4l_X(5 `L88FeW^u9X[o;m1uZ|)K/^wܪ_vu-y/Svi\zgL'J-=Ic<~}WA瞭o~_r ([CuYxG~cR~/K\9͚ƮdɒYo1s3wW_^t3<e& X#b(ul Sc,,TcR!a̤|?XuuG|1Z)#t6ߺ|E}]mCm:B~ ~v\ D(@FhQD=o%q6{kmii~Gy\ry"?e8|qûE,`AznLuMUW^}A۟~橒0u-淎#_ .谻C 41<{ܽXň~w<3y;~wƋm?7jnWsX^.9{n;2 79}c֧0{߽{'}>2FoW45Ⱥޠ';İPf| !x<2RA(Q(K./Vpž@B! MMMerK:뺮b !"tR)em0Df`͌R aW^{ .Wɦsv]>}pرzz:0(qF3r?3Z6r-j>y(F.3ߓj F| kfdsg</jll |^ʆ{6u{WN8a`Y\[yVz;OzCm77}zʿ>fNf|);6>2r/b摕MMM ֠J300r#Xܰyk0 "2F5m"!8?@+}uwWBbC*Q.3 嫶f˚JWPC1z #"dR2d Wz(ؐ&T$"Zy.?Nur 3 )Kx"AJ \]K%R2lπR:Dvxܽ&Pˈ@/}=[nDY]]}Օy'ND%@JsGֽHOݲx_7zZgkݬ/pJ|9)'xUߩ-{e wnˏ̬l}R?)1*sWQI>F4C($_YD&TDdB:%2pʶT*Z+!@Ah])Jfjb[ycYR$2d4¬8*@Ds[Zn8Jʪ/}ޔgD7$5\ Eƶup ,(,zE<[ > =+NZGV%9эt˶BӰYCcaPj΄b檤Qk[Xs؍6X92Z+6a6WXpi.WRa&AuU1{6+Wrd` h(@ u'hUȄh!pxLsjkk汓'N1De?JDtqk 1\w[& 3mw_{ 4c>zӥ=ܒ~ǃ}uYțИ0#yq7_}tכ=z!/ HAV bv|UcUlygkˇuFhf  [0 U*7ѦXtI`kR8cC@ LdKgB䑅J lv?flB$VBCLy'DDՌ)>~y_[S &MS~zQG}c]v+>\w"!'[4\3Dz^:pQ>lwSC~>wNf_ IDAT ]]?gӧ8Y߷@.'zݯ9-{~]~w<\wc借ʇf5sy {~rBFNq܎qe Wdv[t0 H!D,Z " =Jjy;gbt]0Iύ lZd[Ծn l.9icBJ_ʲe.=ܫg[m5ŗ^vI'njUWr teB ,=7r=,www7Λ;8ttkc{~ȰO յW9lۘ?w֎ $˒&N.KmͶ|~E~ -O8M)w7EH6T\^y!Hd؟#l8=8aڌBD$( c8՗3v  aH-V5fKYю,Q糷9?Ͱ(/+?渣+kl3f駝O?ὧv}w|'Ǐ@uUڦz?uԮ˧Fʊ!b؂ óK9F^^^L&O9~vD/|GGSC\12o*J6LxK_ c( 3F&f"F@C7k;)rB[Q`"Ң} nI𵢈 !D|<8ֆ%jRx1lV%ш8>@!9 8\:' 4."~k-y/)euu,1B{jhhP(jkk8(jkJnÂy| h?{?+Vx< /^%?K#M֕Uӷ1ol6C)?r)ddVJS1v3abe[c] Me,dUJGDT+Zdl6Z  Jb-]7P]XWSW_#WututL&aV&(]a\B@&2qoj/}O?Ag"('9Ygx΍|=v}":QIJ%euƆ_~q#Bm`c4@CBe,X |^խ !MRfZc؀#$JLѕ` ؤ*-HP (3vāݿϵx.\ޛ60ؘ"B|eS6|q[׾(mM0ijAD9)Wj%F:j+0 hJHda 7>3R!Q:B0 [ 6sܚ}soz#3n/\bf 0R:,ESqEtfdcg H$l Ï[(~+oՐ.Y!fk*Sl1&J)$ B: 2zA@k-e4b,]61QWM<#H*X6S:2D61MJd3# G7|sXh"l|٨=2X԰1ڬhM\:ә K +=V!1b~-)%h P@.HLƐ@@d3eQ#ڲvLolxY]S@Xr$m"R:(fvm|R%#DHffbF&d=}2t\pH+T CMDa`a`yik?PJ'pyYAbK2xDD$%6D a#mƎ-B͜K~ Q_e:!&!ZZ}P)?I){qRicƝBHBGb2 ZwuøqRTEEʕfϞ)j"R )jQGӄXbbNhqF"JZk azzs. 'N=a\׵^u=GJ?tscL(([r_~Us̩bTomR @BF6r61F.G@)Mљ En̓K@& L *նh,K)F) ’ݻt @lQ Lb1yNjbIωAWOÏ<Jn1cƐ28B DG W I:"&!¦g#R @H7Wh= Q62]]=;1Mu5@LB 3"q;?`+b.Vū*UeIsE2+K&*ʒdL._/8q 'O2rd9(q1HCJ)ER)JGS[71+epvc&TBJ m ,8.j;n+iU 9 Y3#BsjkZF\wUF1.vt~cǴ=uyeUuee% CqP)e҄ƑR2b"lb}K6&yxg9m4v=jR"l""BlAOo/(A( HfҚR+%#gD&ݗOeAU#(+ >8R2 u[M6g)'Κ֊+;:OVSS=bDSSZeKL?y{z 00`1jmB"A!Dd!GaSW-r\Ow.&& 5U]Y+ֆ2UmM uhbر|Er~6ZܱG2k[dlE5 M_: M -l_zwho_KuvtھґJ)Zi)2("lr6@F^|Q BzkK@Q8B35 $uI&<"uŖ[Nd+0\jUyyy~U+>[tglM:}z2|AiB p]H<lV#D"Dl^;::uDa7Ɩ=/nK!(ֵdGbUE+cBfRt]ݽahdy2^U(/3v>Dsի|]mMxl3k/wttӡưI)],*\!iBd (9Bj B60Vwv,5a4C$Z WVbtP!9fLS&NXUUYQY&С~و0iҤѣ3e+{SB:Qkeb-P AD"D؄=Ykn9W8Blx?1ؔLj66uqk] hՙ?o~hujjTd2iƏol7뺕 ,lkkoi#>y ӹ2V`` (P0Fp򧃑:(>UߌO"Dfd3R+B~:E ZR ]nj>{f2ٶUsahֶIZt+++ŋE "\/vuukcֱXX A@DesYvSR;}?O ܺ1#:{|:g!BCU- wl5x8N63S11XYY2}jl9/+~:˙;m?ytZHQ]S=݅ aXQQs"رc\oo*O03hHHZ3)*.[w-׿«v'EYEa}Oӥf#%4ĖDZߘ]!0 QNyErt,Y6iDGzVLvll+KM.S컽sYDI{rw8Pdoﶶ+͑;aFmlSU2rIX0)m)jWb1%1!$p%(p=[޺rٲ坝]2jd&kl5:Ww{駟['UV\/\f&|W!O @02JR( $a ?z./2~`/|>кv/{y!߿覛osھ¾Y|0lyU읿g~˾o~|Oo݄f!BYXX)Ux>SD00}}i"jm]ye(HK.(5zdMm%3wvv/\4))S'a!˖%#G6N#G 0HD c S6u.*ZFF|;PLe GdU6. =15IUs- RZl ;>'B`B1 )j̙3K.RӦM[Ϗ{yɮGM-5.>=W11,EW<p<Pgu̷&T~UT4 l,@6AXX+`t&,]Լ瞻74zzz)S&I `ugEo3oūۻ@JL2R*)e" 8X\' D &<bڴi~eeu]`*++ch9O\oytu~/ʉ}p˺Ϻ^ӝe<ط:+q!uo/O1L7}W9"mၔD ,ZJe"rXDF/e⦦ƭڢP)7Z 1'[3c.s9s ujj&L'h[Ammm&cP@)0ո (!8DjӦM;S>+bx>SO=uP:ķ.zw>_O[vĸa[?ȽqQO6N(V~sqmw?|g *Q0i6{~F\O"D 70{wq+ꫯ2)Dŀ=2OmQ%&BdD!]WhaO/t8Rf'@s]Z붘:%&tIU soo_[[-<󽩬6l lx)%2@@-]v~ZSs׿Ԝ9sځt!BCI'"BaÑ* m\q\EB"`fc 9FB6SMe/o;w^mmMSsCRwޞƬƎmijhھyl&ITU2qUUUooԩ;zzS}/8yѺVu0 !F00u >03:R0CZrD"DT e p6̄ԟ(jVϚ5oeʪʊ -\'Dy1/VV(++K&Ksn 4˖Ο7\׭Z|RIJOJ k[q]O)e41@FD(NKʇ~?񏈎#D2uF^pֺ͊ m6zbgGDpGԊ\6\Lcd>ttFswOud<=,lT__3v9s̙nSSSKKK===#|@2)uVP@)ZJa# Jb`&RZ w,)_r%ʋ!Bץ~23D@fDו: X,СM]o !-) !r: h/Y/AAhCD>P2+O=Q__?ztʕ+njљMIѡB0G )fB0HJ`=iӦtMXaYr.E"D2#an#<X @$b aXV#@0!l].f0} lt3&~\.WSS5bD+VrABGJRQ Aa#:J)Dd6Z1Ah6:^!~"$lA @ 2@ a\Z+1dtB!ֺP(9%HēfBؼB ]PU Awvuah[|R+f•1aFD!6!B&?dϰmfW=uwaQ+]dK[zQEڻ  JQT***(Pz;vWJXzْ#w]\tgw239s&$q1ipbJPQ BAR@y 8 @\@AT*NW`  pEq۷\NfN7N1cBP)(BA@)4 Ag(!_~p ZDGx:y^^}mv^[b۳"*4-#1%Pҷ 啥Y#$1"zq-L{hˌ@ (<9~*0Q;9k 7,p_r#tdSʀG{__;Gԓx.7`YumAV 2HyphÆ^/?9{Ÿ~]O+"_[!{攧^[vوY~y$^]iM O?wNsJb\Wbcc<^` pߢbL"(rVSJbHtoG[twP> -=F8Qϴ  BI h4:9'BѨBmG222t:B!@  Bt: a8N 9y#NqJ`<> (3R " p 0@@FAPr&[f4v/y 3Ş >5{hN3nڽuͨPMOKyRQHL磊Hl9rke]'%V/u" FS(*FIM \ c59q F`ɏi }t^%}``ݡ#11G2(U=ۏM~b^83G-$P)(A@i4*@BACVVL/RUcF*#F##((《R0KNKL;ArdA  ǯp A(p AQ)ceuI:={[TYmyuuysoܹvD/MV7s&e.6%L-PT2*8xq,&gC3T~J7!3s^>3tVm}4Y>6^4v pA ':d֠.\jòM~pAׁk7^>|kwF'Ǽn;ZDžzwttDDFAsw2! u/$`ǏI,fJy% TAT? #(a8 4cJ@h4*ǧh(x>B'_P  R(A |i:A J o!oM qlCr_FC'r m.|9Gtn( sIVq|'_@AVU!Rrb{E!,Aɱ0N'W"Br-8(@T0 N @ee@FT*ռP(dm p+Q*ǎʫ:/ gn36icϟ?报wڝ@ (9%) L0Dc8A88(E~%p 2Ȍ8a(% .w[ d P Rԩh0 Eȸ4PP 56124|llllll =@ -@} Ғɷ *2@"C Td@E@  @ P!@"C *2@E@ HsLC%%%IIIN;t蠠9))SNNdee˦lfPۍT*H z-ŋߵim/^ܹ3(2~Fߵim&#d1##DSS @ZNoUT[[ԔTTTdgg]tQNinn]yqJj;i;wk>"ŝ17,!!˗/8h4&qqg&Nmx<2ԆZdl m;wTVVtڵ;wޢGܻw F0[7S om%M+Wd;;'O6rUl jIWLJ=zT4K.˖-MBrF[=qіS䜜슊=z{F#hj:FEQw>ɮS~qik9\occcTb # $ޣ5GE9z4Zbӌzdpa0rCfrN?zט_~zj2}=z++(O:wɥ\md:fz[ocxAC mQE8\EQaT*b0L&3[*x< Æ 1E.ϸb=EKu|uКA6W0@p@ T˳Tѡȶ _Yy?iF80-I\aӧ1s8k9[d|ys-. Ox|r򊜑SPP@>)cTi_*@SS+##] U@W7 q*ǹŕ11b_!}|OܝUYxϵW"d.Vfqٺ| Вstt$7)c8@pL1A8 $Yt)QLX/,"d3FE^z[T~HPۙt E4[va~] /Bi<`Ҽ|ȵ-oOɛ/f92ʺgBuG9+;o_ߜA kkkooo1E[1eEz]yt'J6NV3_T{vdSQ.vR!.=x;pq9Eeu(Lۛ`B.[iѢrbN 3®'ʵ]dPT-jLzɧOvY%ǿa^y~a-ZC'lÉ;]ccc BYY۷oQRR"G^ '5;(³׬4 iBNQ_-Bx=6k^&W36NTdtl\GE~vqq&BAeѳ+C%Z>wKT= -fͫ 3n>'O]5#}ֺb<{ ЫWM6Z[[.;J].[NSGDE;1DTm9~u;]瞩ϫ!~Fn[{~p L%Q Q}۸GKyw}gȺcO ^g}bePx&,`mAmmEpRL&BArM<O 7jMf4JMs-}J]& 3|"=CVųkQ96#eeeuzYYY# &khhN^q(Lm^;Vd4u. f<3kwz$xm],*[?Tdr@D3u&UR* r Z*zf5>O]E FԞN$Mv7eg yxOYq+YYQA咊rm'M HYYYY{ihi(w~%5J&qU %ƿxyy9R%%% `V4fww0H% Ï\75+ Z; <]J߃>J?7Ƈ}%Z.+Ώ:qiݱ2&.Gqyj񴵵.\Jvt}p\^C/h'~ϜQ(sU(AD?8g=.D8BP'W*3ެ7 ks)nl===淼\^6~!ϗX1rg?3e\P dkue}Ab0 )****g/ǫlߖbw<#zkڞŠX4wΛ\:v|vGFXҞ \S [6"E7Nw={(.eH{Ԇ>32bP#tuJٹRIM"" r~ l酾eddT-/71Kyy9J37DqUUs &V| 6_4gț{Õ{ /ɺzt:[nqrlafҍiɯNC>q*59ׯ_¦9U@ |5;Ϯ;#͗  9(FLnQKusHvKc#A88?V"p AriL]딱PP;ACV*ԓ޽ð4mmEj1F%MNӣ./y~%&61ܬ;lRj_9QΕ? btZcӉ&5v]`ӡ5σ\%R?R22leNԪJKKutt}$+3SEU5:6j%7'MQV: SSS'b('@ԩvdv[&v׮]+zùO>Ϟ=x>}llli\GGH,5eʔqփT1ىmo݊i;i"*))IIM9w.9Eےc()))**jii\.и1Ɲ;8%hypAD] YM!Uz%B^Zԟ' בV^-$%%ݡCK]EǧM*'"ƢN:tXFFw3335ro3&}_bRl"ۘ t4^h1ȟ[*-RdATo򱁾aȶfkL'((=i!E;t淌K@ *2@OZH|6 x]1D9W+W@A HC$?IS}?# @ 4eF÷h4۷%G-!Dj?^YYYUUU) SG.++@ F g- @wÇ,-ZBBB޼yB\@~8ܹsN{"C H  /^DFF֓}g<>>19s̙30E^9u%n2W5Tk'BU@d~U"H߹4)Ҵ䷤q{/;:;;_^mv6#-,1p0+䄷Q#':NI4s~{T|7tx1#G[y,RY4WŅu`5zꖿWZ+Ko4- #'Yk6" UܞƏ9}6^IqdvofG37[}2UtGL0 IDAT`sM.G9-'R2<#g:Щ˫Wrrr,--Ç~N]ɹrp4mmmYYGΞ'@Z P!*2@"C 6G׮][P!*2@GO<@ iPQQi^Eԩ2HCAAASUZ @ P!@"C QdB6lƖT=n SNݩ]ir ܢW\l:kqv ƻ]I)n:o%p/$y]|ՄO@Y@9յL 1=50nJqt4 yې"btEBׄ?^p4M%<]2eߒ683iBL}Zʳ|-@ZgnF.uz+Z0Y4布Mͽi)}6ȇR#(tHl,F gO051wea`M4v>]U뾢gYw@ziiyo-Tf~}ՀZjF݇|Lc5fc!7 _ώԵ\y Vr4 CMU ݡ-KaPMMǐIgKO@,DМ;V &u~hGћu E-ѷEvm)SzQG˭cJK?i\ԣOu_s.Բ:ivur+=2Y'Lv[7n񔬜W#߬ong5@Ut .Wl߷?cfs;-qM38tw)9io{::?>eGz:]gC mo] ʢijKOY~'ŗ2TQ䀛g\wGxj;i[O{ee{ISgD؜ʲĆ݃\=bC,pɁkɜ W'R#PRiD3j}oCu֐ɪ:mLN,Ħ%=;;q@g&BaE=CQCBg]t-PwJ/|>lj.grه\1^NQdY矒pb7V\>ޕ(23+ οXzcCotɺ?B~%s~{{հE襴zdjD3j2cG]555UUU ~b%6-Y؁:l6[U*4X!6G5՗MTB%Y954| /5DeAϙ*۱(ҬgdIanLڃ30]EJPK S5 <}46ucn_Ocv\I.ymYnYM̞L̼cHlE}x95 px6B Q׵5:9é2*)赔o_^,tOgXZ*E\[/4_iE\nQ9d=pB&lP}כ/V֔ª”-ַ]Mf\.@QTN&lFn37I+p^ֿgYeشee L" f7T8jX8>#!㖾zp꼗ی"G8XOC]^ uigk|:_yn{ŶZ=,\95¾!rIǼv22FnZ&,<uRM̞K+cO[CoceشVSinoA `̈{QpUBa]5w]`{6r9}/^ɱ .>{H3!)((r劥%5--M[[[YYYgϞWOO&{vUTf)"?i۬zC @ZoB|Z7P6n_H3!/T ՜fB =F@ TdMB@ HRRR MNMMMYYY@Aۜݻ  wFF*#C H3"JJ3-#C Hk*2@E@ TdM &^B7o8i-|=e2`u<1»'rS'wǭj(wG-M/j&l8;Q~i_rt_f\O/GRjt"|mdd&/܏I+I2u[U^D Wç/L ȦbIumm}$0omȕ yeey e̫KOj'Ă[j7ONl3'( Bgvb{!q2WT]ם5BDrD߃'0RSAFFA=PPߴK[tm/K~Z n GwjLAUSSEs^aRxW+x~ .X\S$q}L2/lY+K iJCf05s:"r$5&T]YsZgddl:5mFg(:DO˯kH|g/3 ΗN:Wƅd8n'׃'__ 3f9/c/raˤ> S2FQlɯE kJ:2W@f·r#Vg0|ᯅⴥO:.D}<  Uw, HP](+υ(q,(cy=*k -wX"`|- 3u-UefO&RYPUxY=LfIW&1#Lgu{#t}Sm2%OZl=j5aF o::~k(C!SE7r8P4H{ô ^(&-\T2X_UX8?OFiz<@2\IfiJ @PYNm2}m@ TpU7 RbHJqAHLL]Cƍ&&&ҼGE5Dʽ6>%S$r JKs(KSD9`We\~_xS\Sh~kG2χ6e"Qy%И='./Pg|تx}fR(䕽$?{|fP7e&A ]]P)ՊԏJyXT*BЙ]!Y7toe\nٷ:jҌ`40N2)tТ=`;-N+nƢ."?W eQ&*A)mvLփE^XSuY״ wSr n]tOG. S"{l.-Vϒ[4f͘SᄀNī'}X0q@BN[a+;O+%?Swh3e\$fXMF.[R S6;fQ.K?oqt̊fKd!(SYIZo~S@rEy|rq\"~\Q]3XdSpraUUarӜNNdb1FȱO!c;Pne5laVLp^Ƈ+"=ZLs|~Em,-C wOyyO;<ȣ(r,#+kӕm|]wYzj˫HAn/;4KE׮`Fgtݵ h~SYG[w[3O)CgY̎tz\gsp짅)>Qqۭ$yJ]R͍Tuzeϼ}x%zupY*Ͱm>EL5Rb_2yxgYi @gXUʲhj}IT;: 2al8"b'"5ᕅn7'Ll@ ?I% OYvqZlﺿq@ZcBfVwWm:j4X4"#}$iB a @ @ M8raa!t 12@E@ Td@E@ iۊL¦rwYtw8H~fٿВ?muTjEt6Uh9Տwhk66JJp97}G/'0>ݎ2a`8 kSxFgxδ<}T%K.HԵLS{YOKKuH*E59拄 W8Ns8氉p8KF㳬683=Lʎb~=Ąݶ̱{2ZS:2>'R+$jɧnF.upHxWˤTs9fPjq";].u Aa F=n\xS4v>]UUT)6nΊBPXOKK{k?nݵՌYilj/N0T:i2~m̢ub3l|$.Y_ǘK|:dНi N=_PPg,ҞL-{}?CfW^''#㐕_Z֍p<%+75f..%3'wTG3'{E*"Z3^Wr*-U~SZ6ۆV|o̓/Xu" gmhʒ>foQV~Ni2{gVd@ݭo͢K;]P˪vsek|?t޴r[dO\m]olmH%[tCp. t0ܿXCtfO/ ђ\&m2\ioa8AKm&ZVOk/1RǧvSq otɺ?B~%s~{{հE襴@Ig1chlUNdiTP/CUJcsQqq1Q1@( ݚέ]'pa(畿I;']J+{2Ky7Hfal ]D-fe=%KRwcW=ܒ{t) 40%]G]myX7&vQM4fj5~/v:ne_55N^{%_e|m]p41zVא,M_ܥə O{YfH"=j^"{$.8":(`(bovx1eb#-J>g0}&s;47a\džG޴xնݷVUoj2ŬO pee [ ݜZ7BPV [uh0F]'Ql\ZL4e".(5l Ox@0 f7TaP וKȯOXrxVdTbw,c= .w=V:z-H\7_g֥afne1 :hipƆL{sHe'ctBrrkUӇjiT@ԙu}KhpJ7aؙJMlɽ<̤}{ayMjc/(ubmq{y/kl>a;g\8'l(e/ݨno6gFL]OaR,i8ʡlz45 fĵTT r.adM+Zcu҄wlSUQ;+s3N~9p*?]N$(nрQOE7惡}?į%[$"I*񟮧¤Й݇^lE8k̕,U}ub?闃=(u_35&,= A 22 Aj?]:k{YU}}Q*^bPY; =fJ/*Jac|TN;w'N K5Ff(t,NIW$ˁ+2*+U/,h.M}iƆ0NګY2./)V_y45#F lK>x?<,8Ak߄ ˰VfܾQkǁ-͠nL (q׏JyXT*BЙ].փ+ΪNhQaeRBm,ٺ gr {w^[W,݌E]TE~*ˢH VA 2LYl뵳Fg,šj^R5-ԼG[ 2q[%y\ȸm8 Tq+G(3{_r0pӥY^ +a88_KMKغIF8!jrJϥi/;CCB "Jjo˸XUI.,'ȋ˖ª”s3Yo Onj]a6Y6wrJTVrR IDAT{uT=4l%dZkz\X9-umds{OEb%!ǒWd2J?xi:m˔| e|bz\*Jʭ6l;q@gVSʫ|AǑ=G,rK2j ju3Ȋt%n[+yMJ|]g֭*F#zmA!gЭef l/2"핕x3~rnk=e,ٱN]oP͊zʜ>-㹰SWQP4\^1\J3kfѻvȠ"e+X7yO=\KEj,nt'=mUu]{^_R)nW/:(7'ޙ#yM4'?7Үom=Wt}lvwǣzs1?zZv( {OqV+1i,uѸ-3s.{t)q_lyO֍Ʒڜي{T<̈V[}NJyݶ#E 1kG0FEݑxL`۽'wVg &A2f+[ 05RSUЭOIAR,bWOKC]b rʍٳ?2:=d+9Fu ohOc 5 #B/l6[EUk?˃Z3 Vٜ I=TuH |e98mPљsw*ϵE6Fw2oϑ3r$_<[g6v;CsM%o_w|ʫgM`7*}ɔ!<6aڇ}Nn<9~KoNZvAfʇ~_0d0Ecb@ڙ׻퓝⟐lÁ.w-hKVӓ_j̀@Z-+kVǽ7qZCW^@Jf%w,Z-hyo©XF)r3Bnt&Ȩ`l{_sz輲zl6 s3MG:Jx[lP4: 1N_q 7>=׌I㗿hgPz UK-W ӵsWv R9pVD)yb׵/3)/[M+CqhvJnr4TQAJAA^4i4zJ" cn=8AF\ITT0pPl5|ާ^h-Ւ\{w=UUНIЙk=(B8NFE׫}%h\W)j"Z>m€F|!M*,K$U D:>@\5bj]]* LOZHeҙofˉ?^|Z.(\P uE%7+XĪc~YڧTQ=xd*^v W|hrKw/!1c+:^d~ލs S_m;Ay+L}v5@˩Vbj {j=& ͵ ?빉f((z (rCqwKq@tQl\:Yů]JtC'i49 3Uns뜍K_W][ 1{=sݏE71 4n!bFm̤]9dttVk_x.)q9/OzOР3 V6 >.xwqmjs HkUkۂqie\[i|rym@1[5HMM{ǾSŝ-A.>qƹT;z;|l2wǍU}Ν^?D_KCIV^׶H;{g\I HD **('X r*{E=Eņ;H~渐?Nɬdy7O~=M^R\j(!iHױq?);kQж0k"`UW@;qDdsL`h@0G Dd "2ef?w {g,Fmem;}S\G$ ])"7c'RGM?p61L7 Qf2rڿ|TYuT]u`żxh+W#o0'iw #ez:D,ZT_]tj˻p9+C\.˭*J^?dnYUKUVɘ;YU?m2<'IGTLÅ'[O+lHe엧eu|0'-,AkIs4.,ٜ&M(x.jڈ`/0TJl2}_EgB\VyI$A嬦aͺb[)joSϟDYOf";̸>$ontogtґpGo9m'g]&"{9J*5%{ttd/tHR7o$9K%Ewͥh /1=3qFYYM屴]5}2FCʓ5m8{?c~h?yQULGL}\}Uͨ>磳ty}o},};3ztUCd7T=XVGҏr}W| 2#d,|PW1>a \Tl]aT9' ]ejpOY }S\ c|]ai5"V>j4R8ke + !ĮOQux$6yx}I {5TV (,{"-cщ|~oiϸ^;}u0C "NJ 'DpX,/m~&6?f3O]5e3B(?aI{C{F:nB1BpO+s łv]PU6vBM9"QNs;7ȶ7"]epzv}=~:Y SmAHnShZCn^"G*V~-<o[a]ԕŵO8MNOc%g&hu gKmӑi])g4:f9EFgmwdG𸔵;CPZQ޳ Ӛ,KJ !Ėش^ʓ7zZdT8l}i Bn}bF ya0/Own~o7˪}rrYU{bqw}C;}`$/'嘿=trNIf:ZSBy-f.ȫjh _:S4D}W;R !OC2ЅrD?6m`0F5G=H!6">+|6=߬H'K6J 4BOk;Iӗ Ftտf5Y+'+++>e !JH޽0'ł ?lϞ=%ə_VV<9ryy9 d0G "Dd"rv;|7N[k"rCkW랺ʪ#&ߨ뎞v=ҚIo93rWWw7]7T_d8ⲢָRS& Fϝ6ZKMYS jLD4aVW_ ^o!iTmQʲ$Xp<}d-#Zs`,,0r @ו}kg\7=TxIF׳t/&. }xkiۻLD>YyF.%H>%S92U}4]uM~w>޳|p/EQ)J_@qY!^6ZjjZ6^!_ɹjx_5e%iS3ZF)6̱vQnٌZ|jN?vj?RG`7r Qݢ  7Eҟ5H&5fe"mۢ>唊9'CVKZ~nO%"Jj\"f:7q/o܆/9*[Aݻ;m;8o執<'99*Odl¬w^3 #a o Rw9Q W?uo;%IJd?йzM o(9ײ{tpq-gj ,mx&-RmDY*Iv/Ҧ#iߞ}VBhCLzȑzt2sdgz^NY,,'=J^]$ZCuV{s#^\> #gE'#aü TXep#W}za1yl5%^xŕ]&";Szֻ=3l&nTؔYV_y@w]{TJm+|y$ 4g2n׵,Q%;mm|*b1>dZk!J^t8BWCN+`է #a!}Ro`T..:Z].oc2At?,T39{i>~Ò˷,U]&"iZ>2#ҍe+& U$)L5)mjfM玟36߯V~=qS?Wߦ sa/" ܁l{TzaFo^6q4)#[c0'NzS&bB,sfIiYqaiq 's{jg]B,rtn o&7=FFs4uIG9χvCK{=e5]7"k"o~lUGԱ8E_,vlk!T.~!;ԙa愔"%%bKXlffNMTA80o:)YiBMї}˄0O*n>5nK Qek䨵.Gy?|DљMZ*:FG 㪽̣ѽ #FK{jN.뮦oܜahĝv 3Ҳn_Wq;My{wZJs2SS6mEl@y q˨)79&(mP3sz8#!wD5 IE5jmkb]ZZ\U=bX'41BC]&"گt*Gs%*K ۆ9rUNC2D,Ս ywpD~|(`إ B^/^C!cn!eIa9&U!T<+)tB쪼gQ 44KǦ*-e3s>=G?ϳUnSSS3Dɸu/mj"Gd KPf1}(5/ 0XY.Xi+YVx%:OWVw.5jaڲg{h!V3*Qp8%b}8U5ӒFjYlG}6!#G4IDAT; ߪ&ɏg${FI$<~\.YWӑ\Xٮ+q6O/~wNu_jmL_:-9RXpY 9\n5㤍 9f#fh uDFxAj0_^bl6>>JY^߾s,4y#yå x7JydסCͦ\.yVC]:OG$vg?q󨬎i8MuT Nw˶6qVyBs!oƇNڽbČ6!~cyڮPPXwĪ%3Yn:a/*"۝pF;O]ȹ}.`r8U_#li^1x^C.)L\g?YkJ(WH4G_EdC'l\x+yOrC'?Q!QBzƞ]9eLO2:nǎjiJBJ3Ǎw ɫlRxcdƖyL]0' K|~ږ+.lvODw@{FngYV-@D` " r92@DDd Ed:. oCghaGS ;'E=f뮩m޶?F,2Ԍoz4]knPjԜ@ 0p5.=ԴzfkΏZ1yrVAfrVM(++u]ZȖ׼ȢG̽:k/ꜰ ̃j岯x`7릇/)GK\a7NOCUQElw5 ׎0SRRg:Bj@ z^Z{vlx;!ĩ_u97IqtSVӱPrӷ{75^{+^cF[мB.jLBwٯ o Zi>Kݐ>޾ˁ2Dmw 0TV4 r-4"}_ff?}ٲ3 y tt>ߵ"uma^.'$ʚz*lXJ̰ JLD~ܐd,6I?҆~zm0yW֙>XOz};bW*r9StBŌ3 /R2 eiwz'yN~׽|c')NBJ]4)m c-QPPhIYA߮%tzqKtxxϽVT5,-ȟqnhf:oj<';w-vכ D>UCߥ(i-ՖδVRsx! Ɂa*bʔ.1;FD5鿳 6B՞7DS9Çv1 8ͷ JJqqY jbmҎzNtI <Kܒ'}z%`BBrBjk՟S3ʤQڳ)i7f~Qs&B(h=:ok-[W;ml3 =_t1c2[rcMOm~-8']Z]1/[eTݍEXc-";>U}u|41_Iո #5ߪi5Es~w3Ld>d D.Pw+ȒX>0~F R7׹w_Sc2mKx/ܝYP{ww@ {'yNcT/s֎t(5rZvLrxr?o?=55nKՕM9mnkUFlS",ӖEi/!m4ih"*Zs7S-C1ΫgƬaZ'$*QSor|5Es f4)mխ#%sds@.zWk]]:sk%@kDd "2@DG`h@0G Dd "2@DS"2ķ3:Y Vd0V>U6]$Tv5{)$e&̾]ӱWZ2ZE}m#\۠+|Ue82jc훷st$hh;ޔln^ws,ߡSgZT!6!$b.}rc+e/AO+G_Vʻ/N 7̉(ᩯַ:U>5Dz%PUÕ$ZhHyrFLbssφq؉ v+KL |QVx@A%ZvC#euq$([G^zŗ*>2xAfV>8` ' ]<&aèhJA2,K`ssoΞ=wޔ09$ͱ_҆ um%̟zjS\.w,52*;ay6厖#^+cr6%ez T=Lx!P$l)Ռ*1B%:tsni B^y[8D%\.m:X 'W<֗ߢ es\mNtew8^ZRAˁjzpX>vy}ORRRx'&&$5%%%111gϞhnn.Yg'z1M`e#ː,hyݕ,K+B+;r0mٳ=SBOS(8dz> iI#,z[(j+}dݏλۢ&]o֋bsVHD5ߺ@Y4ּi:2Y}7|)BD MG3RfXKr̺M]VSh,AlוOQ ;VܱMb2a2\6iw-9RXpY 9\.GcqFAZ3B3ֆ:"m:B/cXl6p8%n׷K# <,}z-iD$%BYҏ ~M RoHneq O1sSe١CCpA'ӭU sY(jKDWݙOƇ<*cq>=mSl4*'C.ݲMbܳB`xvlk>1a~_4DF?T7VA(C`XON +4=d1ḻ׾!fp1k >094CCg}!d&5%}`$/"2U OԡHrj~{.'G!zCffi(zr!y= Ycψ/ڃ9eLO2:nǎjiJB "r @Զ[b{ ~4 cyAwP tؔ=)`0Yuw֡ۇK/~vta{'W Ǟw-*aO0n-W]hN#yDDI&6Ū`(Nׯ_4;7oFY[[:z;wg?~ܹsB222OHJejSٚaZ=|A`^ңKq?#UנYHQxF޽{ּ?nOlk|vH˻g >;p>YoTz6tg=S=Eu)42qS/TG'B7ݻűCJtkkkți"MX|b/B3B>Y]J@Hm1~a[_1w3*&;afrj2Id>JF 񴰲cEU!.J),O/w{g5Fd,KwwNyСC˯/ˌ=q:6V 7O-V8p"y҈ `ߦlm`<ε>!ǖ3Y)CO}]YYsWuX"K9N mA}meǟ+}G{ϔb~ 3U_'m?f)ydESf#.X$Bϧv<(3ICW8}잼'(٦BAD^!O/E7_x1C~],Vó^^ۿ@ŽTrzǣwxUŕYEum'w-ˮm%ظnp H.pKۿ +Y DW{, ܩ"LhQ ~?Й+Z(z<)l. tc4.!$)T[R\VŐiLtn=/` /gu`!m.QPZ=;lşCW 3/VaբQ~kjJR %!FIJoQ*Xj,MT L=K`Bf$E}d[/M"q=Z'Fc?Z>E\` }93b}5?lB:DM->~ ,VHj|;2䧹j4O7a |AuccuZJBMYCOF1u"N(%3*YRX[rqh?Ryí,~l$EsD"U7v^0Gn66>!4OБl.4b^z_[^cWҋ*\n}uKd18A Iwmoq1e\8P:b!g"/ ?$Ũ sՒZ$tBc?3ʩZhguhR"hd(醎#: O8%o+\#-'ʩ8iF-%ɩ oОVvq&lELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)KmC   %# , #&')*)-0-(0%()(C   ((((((((((((((((((((((((((((((((((((((((((((((((((("M !1A"Qa2qB#R3br$4CSc%&'DTs2!1"AQa2q#3Bb ?V s0;P(@QE$(1@QEQE Z(4REQE&hvQ@QE (;PRPQ@RQ@QE$SIA) IFhLM IIBGfC@4P J)z u+%l?H-m.e%bn8XIgbOMѸ9[mRͰϿ޳Ѻe:Ri3H 2ЊSZ@LE!"Z (&3K;(Y})EUŠ(E%- ((i(KIK@%QBJ(HQI@-}h Pz((ZJ( ((P(ZLfu[z/0= GSP i+sUO32s[~MrED?(ܟBzcY&4=nRo~[?nWM5 ǧvLv_@4}&A}Jđui :8Yfzv{Fk6+{\ŻlHةe;4O!4EFj,h;P GzJ; :gR*F2> kIX5W4%+,Y]yo  d8z,K1'֠_ZGrf2sk'(ڞאtvO9“j]NT𮞀QŔRaZM@˓3`g8Q1V[T4y< ngҟ/ƳnY}jkl--&[j|\{,RGbY)Si`qQ^_moy#c@R>j[;3ٺoB}ڦ2:!K= fzg4dAFc#.;6?B -/ 1&=n{pb 0橵."ZO@Ha.Kp-I>}AITrثM X;C>0Iacѵ}Zܘ2JERXa@ܞ [# nm5Lol͑'a{G5|xW=*U'!b?^u !d>$gfP27 GYI u9dt!*R׬4i?!Um-||\Eʖ6z1Zk[nYFz0Fj]iK5r&ź>XizFdɲ7. t>c%6Beqm|nDIӡL{g?UO~+j\? vZQ _Ľ?~SlRoصP3drj,*8%_O&/,.`Dߡ:ՖI}\,Q`+$-TK^VR;_rr#rzVL帀ewm{aG<{ռqܫ˳#2qQgkInxY9̨LMrҭk{XbO0RxǙlʬ KH=J$i@ʣ`€k:/pq1DI!?R@kBp+HkK1_Z" jV7gnTQOo}&g =׍p9';>-vtWȅI >ڨ tk >go&4L]aUNoM9U:,]XM9tn%^vC |xklCXA31Aה~t[jn8RQ6 =67os蔕;8`v*Fagieۙ0yd Xzt= Ex&f5%SQ%uهS|khݗsޟZy;S\@BC*=´(#̟]~9t9X]z4e,|\9a'5ȎƾK/,$=QeLfګhY2=֥ = }!ߺGL282k<][sGQ:t+ I``(8}kU Ӱo`f ڻ;2%Ɛ\M%Υ28X"#*%\d_2dzfxP${Sk7O[EsJF'f?@ M!cĚg{ >q{,>lr3e5KM=ڢOByʣ;'>59.4xp'ʧv_T%2nn?=+UM: MP"mrmSqOoc=D5% dIj9ӭy9Tnm–deB#?0E]#:9Z2r\Jos {r-xh`1֭WK>v-g^xX4s!fS+JAe4Jn.+6Leċ̿QUYxf5>"̻jJҞmy#$ޙ?pwJ  g~"k)ra3/:LFNΰ][\G#B63`b`>iz,66iPG~ަ|5\_\G)3U_گDY[UP42F18g٠`pnfVj\I7~A(mQei+Aˬ)t@Y$'P=WpM$LW!kNP0(Tջ~5_Ǘ2[f,z(ͯ3=܍1v&xjtmT`Eպ̚Pi`֜ qv ;:f55(56@Jp4EEm@Qڒ\IERR℅Q@QEP ڒQ@ }#q9NǨ>`'~'j|7*Gz7\ѧjjK;L7 ydUǛv=գ|8:5tI3ьEr%G};ulT0z7;\ʹ1$loOZ V~C(9L3+ 3Ngm*An~rt{.pOؚ/7%I9ހ''U@oғ wc]M 7v5hVAg/!ʎc1vwWl9uvk tu(٤"Ys^} GHT<,9ضXI˖)5#@/8Ci]4|ʸ<'l}<'ӭ紌GjD)dgqX=o{Qy}HQJ|T b<Ƕb֐bsz-mk+-ĭ5cX;dgާkVw6p֎Tשϭ`L\vxfW y}߶*Jnij;lGzd-M/I!N7i8VKŮv,6T+IqRv'iƑs޵us̮l{Vr-NvFvk)X pk }KC=Az8ʟn\oRlnĻӮ·AƳkʷJ+8nፃ mXi)ܘ;z2.+.jCSh-A'?o|{Ҳ D qE^R߇aJ@o*<>lgۥdX_kw>w%܀|Q IbdlVM_dbd1Xss!KBԿji^G!`#aF uR1lԐ(F<`jhgCs_ܵ|+]SF{Z3M^H9ǚ+w*պEUE*}=F2q֤+xAE\ OמkqD4ia?ڽ"N6-5!* 8Q\-#S6Ji57FoL5#mvgC(p@u(43R@zui8Tz^$_ 4R+; 6Щ7ה2;asK6^H i);J^+mڢ!e.R|_E'Yg6Qi}ZMW7EᲬc|ӧͳ+ybY/mR [HI6'GQLV [sQCZ0=E=(vwsޟjKJ}idXQ4 ev rMz~im}iOssg|c6N9``^272r 9&'&677̱Jo Iٔ ~\޳Flr㑁+-9YhHc 1X "ZFDA%dܑަ2~W+)ű͵{=m/_5f2\$r Nv=jh4;]jOk1Ca7 @ڥZ'm;N =^^a(O_R^}Ew4 Ez4PTDN# hmZbG,y6^c7jH #k65b~aqڙ^YDI%8ɸљb1_خ@u nĖh_ӽpODThFٽ#=5Rߙ'xnb9OQwS*fcqCF'p&NcTeoS4MbB7:|2H$zٗiw,p3&խgLut2p]kr3r)ڙmȋS(-Rs#G**Gb;}  2\kQxs9,OղA=Jhd +tB N޵ǭct{~4qX Z\v5c\"=IV\*=>6m hPacr v3֔Ԑ؅' 2@d~Bw?r=i y9l qںXlڬ` qbp|ƴRL;Ӆ\@IKR@)({i¤ )E +C!i(R E-bb(E{RRR(I@-%EA!IKH;4 zjzݯ6+'_A;8*3̪Jު?RVi3GW*.h0M-P)uk2$ H2} f彳/%`>^dS؎ߥ^3ѐܚ0 [n:}cď jc-.⺅eA#֛qrc&\[d3۰W?0_z{ T׍vc]XztgvWkż/юTi丹hNe ټcS48H ;2흐G݂-1kI\R5`VP cmzE$lއa/ lT]3$RqIAu,w'#[zn[i$rYTbvqqRb@z¼hmcw^eI^ry<3F嶵5YmL?̘;aԏT5%u+-{i_>WsXznjD#[iUFg9 dHa$ʧ|-:lJ.`k4'fXZu4nadA.ˀ be:MkOX3Y㕀φ 9[s\oZ=3NVu;>hZ3'( rXɟ+6Klpv?K34 9Ulҵ}>Z@Ffe'~GC>0EfZE0[#+J!uLJ+E%>;' Τڮkj Nc'~}1Z=G/ ǢیqozX_=2)NG:)']lⵓǚɑ%LU`-fb᭹IZL[bOМ:[ܵx!#  ךėQ\鱬keq>U'aۭ7[[uMل9Y zAVJF8{*˃5\ܑ^DFa[$sEu$SKi(IG`Ƨq&}kMCQ.ehNd[dqJxVÈlnUT@[ѐg=ҍ= ftȯ F2I)yl9?aZ2SVyx2Ess,ܬ;籩-1Gh,5|;{7h!ֳMm5n_mוL`M*o͏SU;'+յŅ]%-ȥYOj%,mg?1V_`jYC: -6%]Yqұukb;O3GN{g𶙠D$q9hWi7w`3^i:Jӭ2'4`J0ޟjY`7.@L#\r:.UG#+#ĀjT N8К9e 1ܒڲy+h K_%R+Գ판O3Ru]>UQ/$Ђ U_`o]£`*BᤳVA4'0{Q[ΏC{T帕Nk0`r?R>-^MeOp$ Mj&FA8bzgZ]k-UNe<+oְȢs PIؖMϵe5thC{p6䀎P}ߧ#HOkX nmNqް7|Oa> i8jߵa*g^~U$0 TVr16<>M-md "C8+׾z-HdYny%Ԣ)jki- j,v*pe>PNp_U,BV@`9؟l8PGkm6scaⳒcqFȬ2q_x8s¿< *w34qHbҴ6]}Iܬ%rbfaGKc~Zu':,N^qVS"Si9Pd߭tZ5ydI!V[3¸ n7 =j˅8tמRYʬCߗWZ72JuapKiA*N`ҧLE*/*dxQh =7=M6Ry %ABȮ~a`* mǖ1@2:I3I .ZjYFX?^ ;D4堏nl+?W6,['`rF^%to]nQofkR I.ە'O=K) V/'..K'd Smv!Q:Eޙ=l]DpQI}=5-$H G3f"q_<5`a pyr6*2˶'}RVN<ש5Z\H!(^|ʋʊ(^IĊAQ:/}>|%\1ZEuo IJFI Z?^%Tp,;|&$|M3ƓVoffO޺!z'TQea ;jY;3I]ɔda3j})CP>l*6YP:{ӱ\dXգ??eYAYg^\3D솨9uI/~+-'ʱ(5-\dEgV%FSMVf^VRJY7=+cA r>ں,eBG99^fʌyڝ80w$=*@ŠֹWlm#.㠩 jM#Xxd?[P Yu>i%>v5E%Hy=nd6OފTO7wx֒sḑ EwVvXiPAnXz}SW,҃5MY^QIޗ5QEKIGZ( )(@R4.w)XH^7VSG֗vs7LG}CM7KH$u_]:7:SSyG6EBp q\:Z$a/oDxJ97Ñ3һiLNչ*+(Ŗ?//2[Vk/fIK. _RJgf-ܮprro]?jl'խ4Ls0<#^izzJH5oC\|_Kcgh;²C2rzgHHGmPyJXTz1䊬&x& RBpn>xM;<[I)*O)^l=s*ɒY768蚍͙ sj )-7!~By[Kfv278%!9Hmm?vXr7DK$CbUw?ԤX N}zkp?37&,F9b\A;֨?xZt ܸ\ʧswqGMڿudbsɴ wIndt8`}Ez_>6A'utFÒ{K wWTy="WM//k=~޾0yUty ɖ=Ѯx"Xlm^Kgɀ@,T zkp|,6$݊TUUϓ;~j=휖z#P.21pɷMW3ZքIi!46$`qAh˕rogiAg#E3xF Kv%S,j glR0+*hՋ!2sYM*GyT)ArVѠ'?l!H" *.ٔdt"yy9~U3]Q Rֺ*h"#!Qכ}}0r:0$X(*>f5ּlӱ( ./& h3+U5RٵK.=bVkA!u)vGH:asO|Ʈ Q iv:<6Wuq<ח Iup7%7cPq\t]D˕򀣠zZCiSfHS4QV I@P)jRRN +*o*J#n6ۇ.fXn겕G%-}TOF@T @$#Ǜ\@E MQ<'M%%%*t;&t9#85翄|A eƷr-.!Wg+Q GRkx22؃yfX}@(q? g9dOxIy0%b1}j^OlX+k}E1VNX.:_6ڍ;TffU Ng&ָÇZPe\B2R[#lczJܟٚu VYSv* d+dKk7CxE20*1'1zUm:O{p.oHt%#EYuķz}۴+H"q[F~zMH9Lp;52mEtأ8|q3Vo5^6\Ky' PIR|{k$240 gyAT#Z`1U{~iEy AZinn nNlvU H> + 1e9Fn5[Akbߛj&U'vz K"#ge-#7@rW9ai:8zᅋw@mayh&*% 6h06Z eɫ4ilu/.\l߉ һ)'nl:cjrO*㭶n uƛ t6tO(8؜`]鿳H jG LyrsJhF&(>jwG )mc?FXN.i/DWZNPІ$u_L9!Ug[ qchkhl;%İD"KYrJUCkJq|OFBtùNEq|X>vȐ 8woT3&FvjorE$edF!7f5Ï$K] mYL$o&I6=۔{z)!iVvO'޻[<Hv$OXKNq,g}iqKFܲ2L!;ܝY,FykK5j9vI<ѱ_)5\YẚKK޲PW&8F<*P(.xEu2*NS c~;H;PAN7?nnCVa[+ S2$a=jj|J"+ڬ4b6{%ۻ{i"[HC>ydC6e#ws,6m­sh6Z,yz'ޱ9줎'Yx~Au%\8"+ ڵk}&cm" R ^kum-,n[zJͧ7!30<z;=Kx=:á\v=gJw\\FUR|'>oҲƨsTUU',?޴<'s˔2A-ӫ+(du `ޱp%EgIb9mW.J@K0!Y[8]sY6chr c5w+qn!skр|c |`9#g\syIVrYy`⮙HςJNхUbT(+NSmöRMQ/'b:l \7 KyĖuL4{G1TlC75/Mh-(mL*k<n=WڮxO2φYfi$ #_S?y U$ ;-.4 .q) f\ NEJjJkg=?J/4Su,i$GA8y0H=hYqEgq,wQ%r0###FAo44665$)FlC 3ޢ'oF?($gڥII:/A޻ApMnlVLœ5ЁڹgzUo=:[ HCf)9]wWS*w ai#:V891H9$FT)$+B;qdjd,Q&?-x~ ^9grަ#z^ #dLuͤ@z;ͪ-d'pSzTT}BLK2^(CQ-4~%{m;ǢsUWW6d{ b8!Q#cJEe\GVx]E')G]a+dVSYeU<\s!zqzvz., j9y@Cf~ө#ωG_PDMuv#!F'S2Ϗiz\ߖH!8b1zUmq|_\Mss!Gi&b|nO]glb SЍR5e(D d;`{k[!N1?*Az[޻-!Nq(P/n!h$sƦ9T #G8S-5VN.T!bQsދNRǷ8W!DJ0?Nՙ-R) )a$8o%N@PY⍦ˍkWcqMlbGeԐX{SjW ` 4 Di f?aZٔeR}D>fLGdRN4HUeoZMƦf ̖U?ģdimvria+4lL?!0=LVw^,& ƍ̃zoTySsur#Ne9`#(^'ɂz0ޱr"j,ʁFyrr9X U$鷋ozfK /Uozr9չҲ望m"V[`| 9 7'.$r%|"Ʀ9ʕoQVVJ-N+BF+u}vF! MBM二@}/*c' g/#59J SYn:-[Hܜ ^A{GxpKpj3֎xE\M?o6{OAZWtp#.Q0́ 1Yx뎻RCHy ؒv_APmF}Hݷfc~Q<_Uܕc!uWF ojYy,e7$q[l#*FSXF|Zay%捶=Gi/I$[k(ta<%IgɌ186HX'{G`r(v;VkmFUwWqwDZ*gg3ur;\3 m޲vS,unp&7ST20lP}++s^xh s^zNZHQDRs18~mn&B@خFt5dΧ,2GiO OW*t9KAiIIy[HץQ֩Z|f\3?1NsM!FlwDekGZ,, 㱾?slSP|Nw>r:1=Y]dE30BmGN˧\E4V03+xcg&7 df@\>!Ħl$i-4Bf[x(t"ZqTvp;XXWǐrd}Fw55]Kki%I",s'm Dd- 5YprHnrz3lԸ[w/+B%C!yX?5Mx~R ̜?1gpk.b hI>Q"{8aV4`l:Y-vwb<[ƹQ$4lG/U%ցm%^< $x7듏6{^4 }Z5R%2\B$|͚֟% vKS2"+IqxfSJ6ךJ_#WFG䳆[̜U[) bD?d+ج,ڋP"(ϪB?J*PtJgPKI>GJ3X4;/S3Hc9@OZYWEQ܁W`Ls!P@>oRC:F벊bWedS0 R❊P?J KIE\Z\y/'q[IߴSk,y@$¼gM/ď=yWS[F;a}Ox{<:ΤhEA%XZq߆\E)`#16<`6ïՄ]b+%>g_ p1㶝ͤ R8,O5Ǘ؛hq^q gu2FRvf6ڰI/8俕,+'$>U$FhÃʧ?Z-{Is݇#%uVƍOp?a\mdq̈[#a\弶 )~Ld>%r}<##y_ajT?1HQܫlm/fG'r;SK9&PYa)2IV~݋X1M^i;A>дq.$Q9&z? i֗2jG l9=0H@6e<&;Oѯuӭn1Տ@=+cuF(.$OO?[[FԴ,(a+qbROqI8uI$ ywH旐ܸ޶q En2G;4NN,%lQf]gwڲͬV3 Zrulo}i_'lapE9c)lz翽tQQh匤ۆPYE{۾k;SH".y.(:-+WUi3켤b)Aڭ8JDOk4KH*ċc7bNN%m׵=o-BIPT`;GPj+K8c`d1Y1]{<RyM0 ݖ0F=M㼒!PH<܉sT=!/.I_+&F:3w[FKkCMw^QcRlm=OmSTc7!{y&@aB{ !s0P,O9#s3H1-!NSDoʌ`)bq0SkIIۚNrLKg4CDJn@Oa$HlԵ6eAFP2|aW K$fӠxx>l 5W]MJ VS-Ttw9Y+y$ %2ƫrExM?{YZbK\|Y$Z][˩[ i⃘qf?jhO4 Rk " 6`:\/AM&r*fЛږ4Sf|mfv.lfԬ( WKqkϸBk@^D%}tQE֍1 ,1/gcoSMc+Kg$=y::O7hES~ Z[wAW6U6 HFMr&LOVΎD=wUi*3\֥$~sҲj >E8.N 4UE"[jE Luqj)w]qkmdPOlv#)$W6<A޷*<ץ9= z,k H%D% U.ClHlxx^|Y]^-N{ tC^ =7"챏'.ih*E^`Oŏ}_Rn1^y\x7m9@Un޽m ^6 )=7E5bjhbXQ =Z6*:6SkЋm(( 1KF)Ek;Q@tz3M *LjU`dԣQ.HBܴn>V{Wpt0Bb$M?aW GS寮KGl+6rU߮yYͪk&K'ڹEZ#k7ۥ #1?*f7D'SЂ6tIuwqp v ߷j˭K.X\caqZnwrvh zk다q$\|0ER#|U.mw:;|=3Oaa_i!wr>ɦ%gB3}=i6oosij2HJs|GSV-JrHYRtd.2$czMi,t'Kec Y&Syn#Zm-OKmyWDdoA1SR\soǥ E ,n^73y$tQXj&-"$PB8נ_ pՍ #ys+&p1=;Oj❭ȷs㔾>X؛;{V4ԓ[hʃ۾$_$RQ 1KH$= ;Jn:VK% YV3#G88mSL\bgui<|")F+?Im<<$1˕\WGvSAOcp~Yd}v:neM:&(甎b8ϢzUݥb|K|VDKw?^zEՕ\:l FPXu85gN,i+vVmk<rP\H8qWO}6nJ,;#աYonmUCFcmEzs}B4SCQ[^oIc;dlI9]4Dӟ}5f3:9Ǘ̬6zV~OKL@qѽW[$nRU~^`[ m> 3&uy[י=SQLɚ3KhtKV,<9UO#$aa`H&jia}"9 ) N$HscgoXc͐P+]eaiXǨNB2I3.R3_4f;fEmF!BU>»_Ρe iQ;0h~f\6$ Үl;:Rԥk}"&11 ?wēh%q=g"?>\;}6q {h.ٖ# $\8AjsZէiݍE%s;2Ǖ@=sZtlY19 iWԚuۿ,1=Ɋ7@6:jggcMf gsu _'ۥjr/m ۤVN ,(8SQ_}ÓæqmԚ܋i)`ʊy}{U{3УBE: "aV`eee= rCPJ*ў>hkEy^W <,,ۇ_C+ 3,.cHP][Fl+'eonۭrOǵ|xx]3 0(_k-WzlQV7\\I,3̸xuVpqڲ 6ozC;Lud,*`S n`khl"\,A{0j}J^].=4nny#wf5UWDTs޼/HQ1f5꿇1&9[j]=q\clPu su2c `o-W MI`IH|IFy{z^ nA"X =+\dXy4XB `tN{Q?$cԓkKaio n`*r-qNP6h+bU)8Pb^PyiP^mI#xx0P!;#zxgq4qZ;r<;nG+IhW [bVX>,xe?s䋋s@mF# #EVY|%$15˪E$LTHHP9K6=n"wZYSp㙉89UAl+D!qbm:XH}z4ϘD\n>+E/!aO]n)8ۨLR4m i༌9 9Mx9Ija,'mq/}Nw$cAFb|߮:TF.!;),qU$ 4R{WXV2@o`ڧ)$tTb\zYNIAUD +)-r򫑿Ljtlݬ TS!Y֞mo@䛨W?"0 ^^_*^&e߿ȇ,oֿs3j?n#fx o^n1YYyXF9/εc0uYݐ';A0z2@vgmc<ׯʦߨ4vfHC2HUQՏeS_!`ofE* @.gy5k)$r!>3S$̹ N0Hڐ>6@_!m'*BVHQVI:Eq5y ,t.>kڜ ֐_nĠKl1tc ;|T[UۉUe#Ffd^b[o@k\\_=ݴEÛ~*~| ͳM5Gt» Т^tNgtpc*}gx)=Us\dU<+-)1V,Ia}"3#P\`RASa錆^Gdt򲲐sA+R6=ҽ֒wmױ$<*cUsOe'm*$VH΂'w'H;f M<& Vbf"RdcZ2+z6{H{Yc!qo;bX9F՞T޼;;-LhnG}jSvbQ.{4lduv,u6/1Eu\2xvh /+ WUZerO'Pq=@>˝WQym}3Iٮu8AԚh-m,6 1d."xa߫ ~KdP ?{F;נvNUyA-rw5۾vrlc:TKgrJ pw 7Q CW -_KQ٧Gêp-3M[p9]a;qiEcBnVb6e>G޷eY0$8ǘtxšODmm iI؆͞GmƏOӵD[5<3ku23sգ?}ned$84Of%tZG v[MaW;*uc+hdm&P̧zn90^)ǜ"9;k z}OCO0I/ȋ'dRC,};K&@ Du+C9>\sI*{Ҽ |~sSZ-3F_:xY{/ޮt‹u0ErS?튺p@Ucz$olb\`tP0_gg`(ER4ideaE=jbAvZ`PNJYf4Ɲژ !;tm[MK~ugq ,5լkՐVV\v 煳oj9mȹ\xC}xׇ7~BpXMya,8?fQ4lr-*V' rNAU~ 0*`<ʾc P3i2sodUd8߿[pT94I]Nx2yO_͎.,o9~5ݪ@H'+Ct3-ܮ$s^^nelTۜ'j]E[`T=F/Ոjz$mfҮ#~UtNEDwQI1ڢJRFb9!I-MRWzOͨXV6RFb @^,'M;g-' d zZ}KEP[Zuu<=zGpݧio?,w1\c&2{U=޼+h"s)ݭe2ggc*"0=2>j}%*T8cQK0{z~ygp<ѭʷ2o,ot>J7x8GQI?u YuZ#c@}v=7CXy>'W^o5)rm2Ic_tǨ2-\-4lSTrCg;k8N~n2J$31EnG]dS-muF"ݹlTI-2G͕UqlyWMRDviTHxjNMlyKfU%687_?VE)\xKq3A3/'=0b~ؒ}9#V80X9$~[xcS7sx=BAm#XiʾMviip܈=$c@ wv$Zp֊_ypjO$k5XaTz+ӈ23ګfƣq'ސd`tG9s}@e*;Fs^iy j;gn5~AqJv% / ӵַ$0N;֪WӁeA"oq_.,OmIbb?|Jʞ{fKu#[Z4Aت߭qGٍn\HNLj(8AIq<S=k&W!Yz0|W. NH[%a7V_6K3 ^K$3G "bԱV'YU\S *5L3cc} H0޾--Zs $Q-d nFRAE='e\; \^q}v\/?"غ$H#oUk}NT$RcĚT=T;I +-5pp2,V[d>w'?LEҵJf";.7Rd؊-鷶?¥*B'.pdGrcV6֖e5ń#̓33x7(bC*.yaW03<\(iL#B{Ȭ! v7c!ݏ.i ui+k 2&4^\A /~.I#ɲ!9i˟ʻ{,̞+~B9S}Mr7A OUc<ۍ(6{Iӵ$`Xrw6 yCrfB˧іWU*wU 47U5ع R t#$Q"اU.]gŊ5 e2+<98ICU?\Uqa ῷXE4 BQY=ㆎ69d<(;cWpMg,Us Ψr?`)|mOԹ%B}GU1,';[=h5 3-j8 ?#vϡ?C^Yܬ=o"ڹ|%gGV.Em&/:xOҼ ;Glzץ[I%r I@vn֢.nw?}?Ck<O/WzяuSIP \s#`a *FЉ%^GsCuneF,9Nv}*M.Rǖ ..`,_CRkMZvr6ix;:~`9tеF>#ytdq܆_k'4cE<Hb=5\h"17̮a>(Kj;9.k li$`0O"aI,zo_ruU o#obj*V|f[U}NOڴksKt_ ;;yT*{@C<4GiTFxyR6#9bIb8%GQI2]q !-# m'l hw[(^睗3=q Bx-x#eYb*Fűɡ. Tkg6ef,?aQnjTZVZ;|Fé3:GXb2-9¯.>iO·V,>#`QpT;uThk/v/s w۩ڤ42FUepPw4ݛZ>E6H$6Ow{*ee=3Vh{}io,Ohؼr\B20zQW*An"Hno,DF= wҭQXXdY"Cr}( 4HaڂdC+my}IǥjqyW ٹ;Y'ګ5\%GYn{k[kg$~9"XdY`Cee=>n20A# V˨gG=m4ݏ*rY!u'[8sƶ?Eyc?n2~HW}}¹7PF.p;|g޵VX+/wcgɖZm[OlF*?B* ZojTtG+xGJӑk.(HJxྔQ@4-8 P?JP*@8 ^b1J(KIK@-.)@JqIKސUwO7lJö+@GqDl޹ " |Ky]:\"լ ξI=B>2YEkɡ4rɍdUQCho+æ\İCB9JO#<I2s(GHsMKk"(Cs3?JkK{+|k7GYQdǻcO :NO:>6]q{%$']0^+@[83Nj=~j;ct=;Zͦ^-틴H81Dep;{Ьd[Oaӧ`NL+XjRX^OIxoO8T0n1#2Nޕo h3e[?^"Եk8bgm" ^~y yI_3; Un^vzChڝSE>Uߨf$Dz-)|v=Jj7A~ \ia*N ;dtU{7{wS,Y|d$s ^2gC^&@#<lsVw-opfLy@;߿h89dmoB k/xtrLwDaָ<ԕŝ^>y)*FEdUV(i_qoJ-̪YăbdQ;o4ĸt cg.-l-` 0#b>NO"񲻋/保G$^l} 1ʧ2_<6qOq}"$T"[㼬dYyc_J(,6'"A^qf\ImQ\vJpEvoN_G::>xT*J'n[jSm}, $s7?7ҫѸ`[;|?j4cMJuNYaTpq\iggY9cܓUm"J%Wie9"I$g ۯlTˁgV! *;ڢ@+Y`FyU33SX9|jl~ ]&x"Ef<“'ҡnȌV V\ R噆 ;t}J{-4UϠ# V\6 oVm86 UX 0/ebkkx伿XSP7a#s^G* %H>{}꿋ѬuKM-Ʉ|I~k~ q;ZyCFpXojGLqLM7YZ'Y&va}6}gTQ wBT p,4Sdoo I;kawtBѐHNq{ѥe2F10sj3EIB̹#cN]J؋;F.- Rzҷzm|M^kfLlş(=+u=2B\ym|RrɝԞr~Svsq_Kr.~&9˓[^68fu!H;G*JscА:ԭgUPa.-zt>fإH r):|6TWYAjQ#Cwx)3lѸzTr8q62gZf^SlK),ASGb7tr @";<|#͐Gzٲ5Nx1#zzU̟? /&ZIyW$|ަ;u*n|{9i\=E R`n [DӬX-W*WIϥ3T,L0i''>֎M%ISQ,O'z!ˎKUH!{g}*}\mVm*(-m¬BjI+%t Oj<-8 P J?P z\RP QQKP LS( Pi01E%\ib#4c4Nj1LeںMaJ#Q{T*;_J&C!'gEHrr5SE#Sj &|@'a+3c4[K䌲z^xaĭl(I>q|U%vܼ͟6sw} 7/YyRTj2@J›@ֺ"VnU8j<=Hy|v冡uúWv\:"g ־OLY #%dtcKvjm$ޢLZppjNWMl4yuMS5 8_`pS4ig>"Dl155 f&2rrZ +n!fşnf Lv`]hd_sմFUJLK8 C^u{Ε=][({u>}j5Iax svin@<cWm {>tJM];T#K(M = Z{ c%b?lgZo^ y5 W%Pl}YqחĖhS<^خx;:u8,HWKgņoYŞApBVz}r}K=3ⶤuFzVUO&2i5 $7p RtiMck]JWwcQ2tS ף'%[&r`>Ǯ+ô=_Uئ:~#2{ %wNN'ȪJurKia0%$([dlz(fxu!DQLdUC`3W)/\(KGj Q|CyYQuF&×wVZ;ag@[+qZ "h$aIFp L<+1.~AzNf }z*}RQW#5gvŔ >= ~ӑ#Gu{ՕN5ooj8(ވV c"qҧ1W*r/oIH%uUPHLWUZr=TT1NJx7 1N @fv6M@P @ERRC@-E6mE\) L}(SNOZCҟM&͗n \ڀ"pq{TZɐU+#NGjq6PLmuph.VO<<8a5^Ǎʍzb?0ҴAdaхf]$=N9}U cpPii(##cUC,G1vqD .z䟽K%OGfKim#bҙu1}뀵70A{ 1ATm| kl7ܤ~B?eä7b@[s{mnU^.r̿)51~̮5FKAqeƔ]FĂI>٩:?p^qFf8tx- yz[4[{n0#U'ִO^]ߵIkmsr9 m(#nsS!JZUx7Q\C Ny؊sCL7r̙ 89 r-B@:|=N `u#87]-r"`ˌMК/q/in,40B7*0989 y;sn7n.ҭmj%Da\1=j^2D3# mog>I\R^kVv^8iF:٣&>ל(%2{ȅ=~|kXzu;~G#ުF l/r!?ҧ[pp``gnNf=kf+(o$y_7FlM֢Dhc c Ǿ7zU"WـMN{mY$B/?010`UNji$TE`ܞz jxQ:`A!\^AYS }M>:Wz|hbv,v.}Ԛ"X>]?OHfqME϶ڳx~{n$&Ob@US̩%T"z" DڬkQ2M)7 3x211>xuk-()y%M[}jߑzp)"6X0U{YjzLDPd4R aioR\¸g BE޻ |Zqʼ9ـӰ~ۏI.z ;GSÕ%` _d\Eqtvoo #dyr26$tsj7<##d‹;oLNӤ$IPD1eSoB3YeKvI5;HPZIY "vmFڢiL6rn9??=Ar#q[i+[\G:g,O$7w=+X¬ZiO HB(%dA3i,Cr| =|dGLE=;y>gG Ԥ1vpX}1ThY37<6P.*qDqp /92 CoZΡpz\vi(77 cmIJ,[ϧ./!DsKo;*UW r1w"cdDh+:/nA-&k\ĩnݾlu;Ȭyn渖tlX1aZ],`sc?Z<~/z/ p~5ťKu#LRJabi<%YNZ%҃ 8;h`id%<a[{A zΞmtCW*쪣 AVv~. ]eڶ8p62(1ڦG:QQGR1OD$btQO )qP)@P)@$1ZRsIҀ)hA@'z^hGJ(&ޒ\@zQAtIz(jiޔ"3'oUjͮye<ݢSҩÉۇ{`<5$:2]KցKM21ľ}?ntĆ{sFe1_aY>/KG4;NK.DG)`PRE|oTٞHڴLEwjʟ Gï=s)/x9&`Jt;Dx"&/9bfr$o֭m!{kXB D4}|AsPͩ8iZ`G%v4FBXRj1w =G\_L>2arP"c(GRfMvicHŒ::U5HxtmFe\b ܣrzFGfKwQR6 ܌@glV`ZZ2|ڲdaO':ARK)9hF{Y#Usѡf'+4W-bѲG~ U209cVl&v=[dpfhOZ|w2I7k%$Ւ?z@1JzZp\БzP|ڝjH7_YďiGo5o,pT[ْjK8-Jw˂\T-> :IYhV6nFCG!ᶯE} K%\]Gmp`pyD$sRUl#>uoizV5[k |#<kU@+YZԤ__ܤBN#t})YY\[[O<$6)tֽ*QAz9d*^iEp%R< J+(z>YTy+tHoZa rТ |<yHbBV;f-F3GjQ-08$c[.RΒ:+G!y,Q>) @&Ә>VR%C#!ꦡl_GK+yo&0=5GWT਴DexL.PAht};,cx1*.s& }^iÚZ\jpA:F8<PKe2dpid9LRHW9'J+UkFYKuĢ|c"EIHe [ o tԱKQۙ$뷩Un3 fXu;g_ iF e9?z2\טNݷ'jxēIOPC~13aQ/ *Мm]xF{gA F0ioilT{^UV "$cj@ uTRIcӒˎ@s O O JR\TN('oj6jR ()  (((P%)(=)JgzCGbR@CIҀu6twHzoPҟM}JF)zPMYlk֤+lL:V;gj"yoZIS*ф  CcGPTS}Gju 'vM߽e8Cm:'WHV2cRcA.qnIӵ%Ѵ\FG(1{לI#Cw%RE8c{՗Sf$%6Q3$(9P6=qjD*mr%P_ªڧKn'o2xrؕ9>j3P) fME:Jkq:-·v h]wy?MeG*l4)[IJdVf] F6Q|ԯqu?uH5V)(?ai'O!}?Ү}fa %c ~!>hClӿ].)MtwV{9C$M2ccA;:Nݫ-C@!H G\W>YHlb_uAZM)U/~NY@{hAD[;s]Zi^%[kS"u7VNۑ]8{sJpޱf0VO|bLr?ޤ+`F?>*+WM@¯U@a_&0请s1f~$յSm:dULI-"K{<XmbJI'Wph\{PS SR@&(;  J0(hi{Қ(-%@KGj)1-)1-:P Z(IZ(4iԽ)>ڒY߭@>rGzS@!KI\QFi 6@ 7ҚF i47ҠڹStRI=j;=s 댊!:hbHhm6UmX$\ }=ElzUh"FvB9fUsd2{>Psã5ޡxTV֝y4ԗGI.xI!ޤbw2xC }l y,c)m5ÏWF_Cu={RO Fs 6TjXdI5'Rgr*Ydq43C73E2p cyosEKO-\A0cGZ'jW7V|3\YAs*(Sq%TmYwuam(oOḚἉBId;CZil1]ޡh\Y"4-/f7h;j*&d.#+aD>Iu>cZQ4Sޟ s˖oZԴdM>IZ]2i1 U=UЌd%ylE̽r30 'oA{K1\Wp#-n/o|+u3z;(5$_%Þ:ݽM5%F9ǘu#nS[jwcPy-e#4Ӯ-E$j$d6L8/\Ckw=;*_Aތ-.b>hS;uC{&#]ivAs<:\7؁7 $$k1ywEzyTur.UZk7֗VF KbzܚqO.j[d-^]"mid*N6+kL"8EOڭ R6E,vŀ+º*Г)8 (1Gjy- FN"Pt/ހ 4P@(R@%-tJ1PREF=h4@!;QAP:RQ@4ARbGڀLoF=ih&CҔRc4;6 #$i(HӸLb"2ڹ0g_ARW]h$@=RO7<$$gu>㷽]HhW^c 9<T5kL$wuqY){Yvᷡ2VH $c.p=I}w6O!fPd6q0vSwFv5>ē N=+,Kxy"[wu( #BS[&5*㷹9K ЃX{~xs8I [,$9fqG"yg9v'*kŢi"BFycAܑdzםK}>.f@o<%n#Uk;Z UeyBtwM^_:}[PG&nE9>bzݩORr{yHBT#=j Eemd.B# tm]'I,Ҫ ٙ8Ҹ82$;tڭG<:mBQK~ ! !$bP^K$a9$eَrOڴf hb ;7У-r3(`ߧj'y4?$iщR.<6Qj'%Rmm{9& (!}kiFy2FXJ{ ؏aW01l{ssFY3:E X$jp=t6o+)WU mfzUq^=?Z@}hsGSGJNj %I"t@{P6 SH:(L+.݆k "Tg_aS\m\ oVL cc̺5C~ʹkHĒl"zV֫y.x+rV9:}1YQ,Jg0Cwsقݤ`.v^no;i7sJ=Q,H(_*uX^X쎑>$w2HF[ðXM?Y[٘Dѡ]A$sP:ю{  ҎBUdu`B'`Oj-m 3?Nr:}n}^;h+tXM8aUv$֍ٚjolԭ'd^YP탐ñguwQp jO46<`yrH | Yu3p7ӧ[%AC;dB1vcGSٛP򎙩[cզ?,ΈB0 v3zՄ6a\XAlZJ`UA =dQc 8RD1Һ"bPLB< 1K;$m:QPR:P tRbj]J4(jJZ(HPii: EޗJ(4 )S;E&?Zu!]oތQގ:Pi ))i(E(&QӭXP?%( QHih%(Hhi(GZ)(HQKHzRЀSABFSH֓Ʈl+ǭǥGaqR\v+ fL>=}ji:Y2s60c8Dtq%o t[u*UÆur>dTNP:}(HQ@(IK@QE}i)h4QE'րP) /'z iHڐE&7QmZ(4(ގbX hEZ)[RN€3M& ښh4i)AБ 4N=)zW6Z&jz&(N)^jAM TCDrc ҁk+C?JWDufE]1x F*⟊Zp"O)j IjpJ(@G 3ڜ:Q@hEtSOZAR$NbҊ%ڃ@KEOZC҄ii( (Jڃ֊(tњSIPER IJZm(Rmopidy-0.17.0/docs/_static/rompr.png000066400000000000000000010667211224420023200173270ustar00rootroot00000000000000PNG  IHDR-X!dAW pHYs  tIME   IDATx}w]y}3ܲw{ծ VP`N\<'NqsK^'~v\B`ˆ^DHPezΙqv/ `sϝ33gf~} ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( ( (~ ]P@P1>1ԓ ( (`)CA!]`%pa ( 8. i& l T@P,_{Zk3#1^_'"1 ?; /wcL8Nmq!NpOX`8,߱Xf6H6ƄOXH~h up5l26'^L q `2m-^:?)NT}@,!}DBu8u+f&pcHq gZ xS F?" H㛪<%ۯnA8޾FG0S#bee̙3{yH)"XEeef2$", 8J 6hm[BbS 80pfd`cњ,HaLN""D@1&1;lHRdDZk!>0[sy$g-0^ !֚_GDB~)U x 6\f~ڻuHtfk^Gu(7//P&FĜʆ32fDF;Neh54r]lYe 8v(B&Kq6D2tWk4`q;;QpȔikR޾d*W ɓc1BI!=߳- eDDMNU]`QIQ+ٶ?0gΜ{Z 8DQww7"`A"B0*%ɓؐ 2D p唖uvv !ذؕŝ]]vtt1A@ctR*,)aL ލ4Ssz:ɋk4G^ nʣh(;i@d&>'OjhJFgLommUs@X> uSwx0(Ri@Ėضm")/ Z뀂ipk ؖ=sn4hN4jt85Ft3gK;zcф[(r^ݤ:}J+S/-M鶤UZVZ-qgʤwFP5u%#'p*Ty]]D"wvvN6-J:t(nyI=3EѾX(#n cXBb`?- @0 [v@LHF!{cc&8&(hZġ5 2B5 3̌ ڠama$0|@BǷfD۲|/NKI)cs$f,R$QZHZ)!ehq1L<Kv:zHNz4Fb_ u##ٽˍA+di,7f&ڰ,4TQaֆIi Š0Jki :4'@eQ_~sBiT@(fȚ! !m -՚I R!1FP&Al hUC4 0!M?h : )$@E1 % Y:FaV Aʈ c04)N{3Yh(Ufk}at~ic]MDP Q&pu ``ho;{##U](PD W\*D_b`D`F4h޲Lp)@c il=&1aЊp< #@b14` k !]3& c[LODPY 6(4ll3O eMƆ^$[#"2lC!m4 ' N2DlQL8ޑğ< !h0akԸyjL ?Q1f/W+=*7g*hMEf:nD 1Rp4A3$Ip17ڻܙ1cP!DoaSC, (4$ g0S`!!@`Uo6HF ` !30'0(آg`x1$,F[8  w9ΌƩ ':>96Ñ;ڙx۪T?~0 8smlW~` lRR.3 AYyhZRBB5-4CbĀ>=Vp0ւBc΄1@``PY*mfL '<Cuv$) A`0aǚq]5LPp-~aHKP :fB /?cNcE_dR6`L@` r81`tg0*f F()X!B4QxP+ Ao B$V8N>3Ƅ lW/9W=]3%ڊIbbD0Ƥ),xP@ғ>Z:?_|<j…sY)&g6=U@Xv](3%_v=XYFD'7c?/bpqۖy7ߨ)ep=wNɁ2P- ^_p6owffE{{%b\}MF܃j(K @3}aֹd #1l IgBT:%-I30Q$k[R =F`Kc[(?`$F-;6{N?5><.@"k߷kǺ>mTy:gYshmxD훷܍bkf͚7'E?l.x7J++j+O7~{s39c޵+]|Qwl&wH׹VΚrM933'MK5ki+[ܳOFemY%/|76\tk80ǞlaRB[K]ѳ}OӔ_~e֪ƅM{ۗј1,dk_Yf9g#+ڸɍ$" axN h H=$,+~+D0ټkdž^^<+--*+Oʳ}3gfqwF㬆ӻzjjjiko4* w_*'?>:cƌ믿>˲|ߗR"#JKKz8*Bw˲C@x`m;L~ 86" lWҖ`LŲl6:.ҘYAM) aS93tGB~`;H,H*C-O^..QB:*JNH='#I ?RJcx.=-:BW#9Uʣ1 E/=ӱe/rNߑ(3U Pg:{Q[H%lBUmSX$BN' @l~2mMdPHpy+yv90_?'/D#g l0rA^~ȧ0? LdP5%]ubv o|BЄp&->\ž8Rmuf]w@ ȂHk}6޶ բ'ZlAR%!HfI:6"ɦ2YJF`V=b-ذ,LdL\F>.x3)v3]xp &!Bvߐ3Ci!$ 1 0px{G`DzD\nKBmgh2ɆJ1S"ȡ􆑐5TUK`Dp]F`A#@ C>WAD,ft\A/M\?e60$ȬfcBmij1A Gb%VhilhWRr$U0z B10-N ୰gH/7V6 ;BxJA5, }I=ṅ:'4 }ڲc"믿~뭷r+24˳wyggggYYٕW^'*c1Z@κ:޾}RF~u]>DC@ vFң+^xȉ_ҒbH0X VVR__OwĘ$Oz@7T0_M!jpImn^ꤪDiOdk}hۼogQ}GJgK W}r_)4---7tHh~H$~򓟄k8*w.-R^^^\\L& GB)f:::r1&*T577z_WsYq!e^z|*/U6u]vH+*ІA>p'[@1/3ͩ t.T##lU*dQ*;|"<|xсR9_yy|?g<}f}@ܱ3Q7lzPkaP@z;al޻quw"جY}MVZcǮ r7xMS=h/_}_p 7L\O,1sby衇oc̢EڼFDl6!D !vܹx/5H\eee! ^x5k֔۶ܼvګ:ow?L k%-su#' ?US\Y{?+߽xLNrt[~k}y_oo ~g_F5gͬLg+ȣ?V(%kK/⺱䢅j{vR dP{!CS&g{s3c}[kfFDS/+N1^xa6]jUƘ[q43ހޖ[m.z&ٵkh(f\r%yscO<S+M2e׮]DbѢE畗Ę D"RJ"rGщ LёL&L۷o1cM4uϞ6mڲeˊ'*'j1~HfBn$9o~<lݿ}ڔ)wΡAA(.ow}K7fp_gqY|=%,<یDLoꔩu5uϟo4OT_^Y* mQ)v 8kq>)AyrN6U=y8OKlמ={m;^8fCCC\xRR~V3}A +J%ᑬMn!{-<}f&ڎ^n˅+ꪍ7VWW/YQ)z7)eqq1Rx>wu-=Gl~(>Ncz.[z<1&/IaiHϱݚ.sn /Xm߆### A\ו;ʈ?Ch{ wJD"ADBLZ~c'dwM1uK8#X'(30o{*a ;MfIX13xi,uwwO<R^ڵ"-ԟNvh[ Q5\W,X)b=C6__bũi.G:b8?;dDoQgFƼa)04]<4?2(XIdJ&1{Z33XP\._i''{Pj :1h>Bq]YN[zo2FUP)eAc{HJGE;(ñciP'O cQ"+HD>}{۶"q|Ľq2Dņ Uc `4 5c$41">22{21t'0ID=Iei8tOnw[|J78ƩN<ޱGA 8f%F;("d@@ٰ dB6f ?m#>OgbcpǓ>]0~PBa,֡jXy;9Oi(NvԷ'9INPD:)wۍ 7;IpS)D4M:ێڦureÚ+'e${4#"b@(""0#-+ 1`!H&wALl@Pl9 !0' .DĆud:v|UdӼcO0Ҏ 'קZ C GaQD Tt!v4׈ h@THnؼu=11OCcLF)D&a,1GŔBBe1߱s앉*|G&;6vI #<" qSu܃|!1m\q+-_R<^mK P8`㶱w!N'Ithز,u( ‡HaJ-|6Ǽ(_)yq (FK,Dž?l&>KkP1x##>cβ Ai! x}~@JQ%f%Bky(=émŎ3Qt6n$51'8">߆vO&rJ]˸0ʪ{Dy̷hǩ-4'9y%Wz;yU.t?|D`&$GH 0<`0ؙ"d&a.%ŠbP 6 4B&X79-B:>{X!Ď;,d2&a ;t*m#HFTCCS)%ff$FF̙[nS` Tqplhhw2sGĈ]l4 DLq+;(XG40#9)m r`#)@3 )s)cP}KK:888gΜ0_0>2C?񛝎If̜y<RK^}yM@? iϙg>sǀl8rsS*O I{ɑF&fFڝ##\hDŮpS9Q 9H(Vc0c;!kfbIR@Vl  ԆQӺ҈ɑtc "NlBfEh>`c,FHeYA֏l!D:noo箌F y6*˽x}c%\BH}HFX~](|vWRmF@VΑ $ilA "Z&$ ܣX(q7{9x+p'Td$Q2l83M]\Ȭ2 ,oр6iea$&d43ku|p `VA a1q5\`fj(0FRsҘq}c4p ?UU)q-Xꫯ~ر{ҥs)++{KKb]mmwRrBY%\A@!|b <^{yH~U^.jժ-[$t:P߀51yӛh`pʴΟcΌ*;Ȏ4:^ҊX,[bI!m/vڵiӦ-7ڵc5U1miPBeGvv$KiU,!-l!JK4. Xb۶mׯ۾c֭\3:8O? WԓYj\wӿ_ YHJ+%k#{9 ш{mm۶m۶m6mڱc]{ʢ1e{~A$JΘfΜ&BҶhMMф=`ˎgn=^k]rݧP% ֈpw'e5=G2Ψ:tp\wdd`߾aq(ӴmIOxС)SQ֊k=uւ})LmysR,2xIfVі߿|U-Y3k[}Ŋǻں+e1!-#1;dJ[s֧~MEC" RX M Q9|~{vvv.X`iVV zdSCg.i|}]Z{1I_K ǒGNRd'ߡ2mhҹr:-[F={=i47Lim2ξmzRH2US[S[Z`K=+ (d9lВRZrقE2ueeRʶm!m!@ ĢCfFmJ812^)ٗH׽oi1"$!Qi-H۷o͚5!͟?k ͣ>|eڒ%Kbg(u[l~՝;Ϝ>k}nM{FңEnÇw^u;;|3_|qSSc(1FTdd'ꚥ3^}e#*q]#h~o̓[?[0I3vYJ-ܰaúu*++7o޴oޒDQQÔΧ_<{r흋n]lq+_=pg=W&ՠ^ڶm[mmw=}_~9nݲ~5LZ|{Ҳh޻~ ٱ`ˁ/J܅?[ںf5Ж+9–ׯ߿OܳgOqȒC##UBV /N>/yEŞ|ڜBf !;GG5{go94 Do*G[EgWTՔG#Ó+K2]7޷hO<o~lCΩ̚7m߱jr[ۡG},w1YRصD$ڲwtׯ}xG?9qʆ?ՓfqS`ϤzŪHuСC5u{vM>]J,߿RZ뚚PFDࢋ.뮻^xmJ?>er-]T5mo27}j`w"Ϙ?}Whϝ1_?|kٷ|n>pﯗ755aÅ_ry~{qԟ1;w\0oS,Ya`F4m̙R32X+7ڼiμ ?ΙhRx'ҾkmhMgUK1B9{т-E^~o:U;w67T> sfLo9x޿ou,;zkqGM>Ͽ 5*i(Jb[n(+ l`\ϿRoSO?SYWP7_nɓSݝu<䳳Ͽ$>˟v钏(C19 #.> =C[t2Τ׭[N&}K/[xÇ7nܸg믻.Nw(g^/+S7~j &d2gkKdSO47XQ]~|'/x&mMfÛFn3?ODtA֙˩Jsw?,Al1M8sߝwoQqy' !p?IxDD_|e^tR۱0hB-Ԩ0lT+fɤVxUW/>,KDNc̛~*v?:wڵgsO>U7mƫ2{»ult7=Ѿsλ{Xm[3g 7}m;v^q54+-;͸>(//߼y… _^~}$\pWO6-wv?PUU+jEaغq}OOqiɔw?FÒ/8===jkkm!Aٌ9J~U_Xot#!2-y#ٟΩZ2ݑ>i᪫3gK.+(R޹sFcE#ý S{Y۶oz?O>{ӏ=SODe`l`۶}W_wu .-N;SSSÏ<ԓee%SwQ\SU}i @$]0c[Xu\wqy6&lc!$, XP<7v;WW3DY۷:|;7md3'9aۻw3{n'i&Ecz… ͛gu R^<?PnܸM6H,|i瞉̙{z :skΗztGROkJtlXmGlDPMo~lO;Y ҩW]ޓLO~xx(L6mŊڰ 8M;/>]A< TG-n?ؗ>:jZ}PÌS)S&W.|SkjNeJ\?ݶ^ 7jfP;v3{"&\(_RʶZ"ڿ6M$ISEMueO%EƕŵR  ڶ*Oҋ[c̷B y,䄅XB IDAT{6mM:N0!3TAD$TS[w n#Ymn4{Ɣ[7'BPȹg?}?[^v]YYm۩T<_ݾ};+=@HZ3Tⶖq}nsPWA79p}}}ӏ1~|]w޳f͟oYBpuOUԮYbph(B6km=w{HԤ y'N<㎛1cFsssee%(i*N3x~{)++ۺukCC=C^w%M#/^D, O?r0,XpK.HRnzpK&6TPlƍ5u|p2k%رrNR>.RJIR)Fv`I$@̤u] ́"aYEEe<ϯ&/SUW؊%ee!@q&NoOK,uX7h-*jZv3RQZlO~L6G('"}_N@:Tc^3p]wLh:o~3yӧ?N8|]zD#yÌChBFlrKꕏv9cWn9i\D5relٲ뮻NAI`-߮1PlI5' zV4B+ _a\d3 < d!,Zwi7c^)۹ʄ T"Nph|u|'D,1\qRHHVn.AJ re2nYl}(' L{Aonjj.ωxL&FFđpR9<ӳߍM\*"Iwu3&nd:P1#a)pP{X΂O?X8鯫3ikg7w'36SLTݟwq1\Y/}ŧ F# 9xnk 'W V7v=?~m]a#JҎDvӦ=$DD;4^CuuuS[ Ӽm!в&nh(7<]$h/y &t싄h9U5Ij eӨ96m?)2nܸI,>'4 g}kWΛ99X$e 7&X"^2ʢ ω $;vui[u6Ə6'IH|/{*fLoښ 5qS]I+΄$LvB!EpG[:ÑHSkv@8by}\=]~섐bd+`'7ޕR띰 If\KV@R1k5tuO C4~ }[RA4] Us2v'Ԥ~k^|&6KlgKNԇr|u=}eՁ sbeqz =V#dJ!hܳEI7@$_XfnKvVs>0&Ԝ~)!'5C$\7ƥ5(x8 їQBes,!Cۋ9CdO5, ~(;I*J>*!Dm 0Ҋ)))3 ucm$ӌT:nRpuL&;DQ6R`%][Col6[xbn#lCF4u8F|Z͒ 9"dzgF~h~L PFNouuEUXz_.눃~F,/>rZ))6 !1 24MIi*jJǐ6@4B 9B(j)"epX:L"!CQ#DH 4/VF>r~_ӌa<`6#v}FD[@jįb0Ș`HID3dHkM<#sEOϫ>t^0db:1 :"Ҙ0{MW P*~LӗʃkŏF/z< #5jxkp̐ѽ?F|HVHG+rv$[[ Suc˲/_~뭷ٳ-RJ $)ϒ刨Y9[prsƹnɿu-^8. S[ 8}~w)YJVpȄX;;;  ;, JͲހxXuE#d24 QUܶmDKv4&Ei4JV7״MMMFx-`7d2 xMf›DUs (˜K)Ѩ3)p==f?zR:cXdo^#&sh1ZAPz3JwTef!t:] kB{RX±|V__AB}\(✧R)8\A0]A4h}AS0MlD2,PJ67a.B+ͼf'Mt̚5kĉm ڵ[fQ`E[%CkpǾ+8իVw YV!^WnRH=l{{{2ܽ{w{{{*jllҗt=tttٳgĉ555pK/= _tnBA};J(Bv^:rdrΛ|ͰCًu>"ȷhƏ\rRַ~aqвm@P$[>J(fž6cWk5g->eWˮ81Jw8ׯGɓ'۷ohhnԩJ={ bGGGyyeY;vbK.]rŋ,YrgG64:^tU1]^> #mF7q*PJN&??ʞTF16o޼{hM:c?^O~qk (%S 1` J)UQ^^YU*c[Ap]wΝ---MMM'|r!a}P(k׮6 0-͛{w\)4) G5+o+^E"fSH.AFJV¡75⵾>᜻eZ뫯n3Hw:s~_|طo!7L*&!b2b/7Z끁>~M"QV&b/~Ν{y 閖H$.H$cǎH$JԩS{{{۶m{'/QG(9q<#`5>-h=FķFJTp荙i9s̏}cf͊bcwu<`z^~_ #$]N9;kd2 *++_R18>~niɒ%[ZR{6IQs(J)d 5n5ɓ'3J$w_ggg*Fׯ}ZL&c 'ۛd}l6{޽{ӦMw!A&Gy8DD bF1XzZ0N!)Q, d%zCd^{-beH ;w\d曚+X1:iv7r-H$N?x衇C0v:P'zwPfϞ}UW}c۱ca)訆9&Lp]kٶm[uuIڅ(=… [[[/kgܸqh4 mݺ1`ӧ?_WO?,X}vW#]cus/ᙧŽl!i"🖿eóY>o} tFb͐ZO:n[|B Ga .q$; E-֌(i,@2=㵣4viHdV3f׿>44Tf2}%&3r7_ob۶m>3-ZDD=#xC}tM/b X---t KED555RJ!D.CD}z&y''M$5k֋/hh+V}…nݺaÆ˗O8Pu(`?Ϙ{n/Oq5?y]{z0_v>/<]Sݿ?kq\bΝ'U:nܸsݓ+&?0[nˡ xxpu eXF~S\#*$6CH^]C&s#}믷,KJiJR֮]~zXMMM{{{ee?q8w޿4%9s"Mч_vm&3gN8ظqNhnn.$̥~z$1/VXWWի٬'G?*0ie~WBmmmK,0aB&f_dMCDى~nVE2Nq@y#Yf]yƍ,+Jeeed2khh(//WJ#;kkke͝;wmmm\.uɲ8CnE3#hܝuk  ޾ԕW\n:/eh`y{w}N?jh?sAu'=~{);Gw;#) r`TY Tc5uY*IOd視1)emmqge2xg16ÎR)of%y#r0&SqOcx˲&Md*֞1, Öeٶ8R*˙~H888x]ִr]wc |Eǿڗ:݁p[fhsg 8^xaSSq֮]dꚛ'Mdājkk#ȴijjj X1ϛJd2 #̯-k5.'T&d͛w)vwwJB"o>yjӠnb>Qu+.4~gΜYUU5uԁ>}'M400f+**B$1DGz@,* x2Рd;b1Ad˲٬)jmmEĖ?_tҡ!uć?a# d ψ+($i!TVpDq"a5 K PyCr-"\Wli, Q䄦bI8$!6&e3O!\['Pe/hlT "~iJn Ϟ=#tuu}Xn]ŋ~)SdY(gϮ6m!/r4 IDATTWWVTTyX, &XgfBA+a0@T!7c R(2>YMMM"83WXsn\ ׿;fH0 x=g<_hQeeR*ͦSN81Ѷp8 H󼺺 60z{{w5gΜ]HZd+ %@E`bA#1@pD 2 ȸm&lSDlD$C9zDJ!B@] -չȕ/^oJ vDhLD%+ٻ25۶WXq駟yfZoii99]w].18e͛7oݺu\.?+,$u 0MƏﺮhn}?{W7ֿկ 3<Ӡv.BoO18TEk6%~}}}t08~{2|Лe\!8eYL& mV✏?~̙ . |͎Z)#A^\l2>!P!qq@!(gD5p PȉP# #@2 F$ cl!|0]ҫT^PC1OV)֗d&*xEf9{뭷"駟n(]]]{P%K{9YlgQYYihӆVYY9<̀P5ɤiFQzUBzкpdָn[lmvҎm(ڳ,X/66<<յ`C p4ZO6-ZsG$jN `@N{ܩwn٭{ip/G~#k䗽X>mV/cR1-:k[T6Z,&D}$aUgrd֊฀W`Pu 4+)vd$ʋ)DGl@@$~GO:X,6vv+R)S~6iҤӧAD~{kk ֮^/tqkYҥK|ɱR{۷Ŷbv߰˅h4F)e(ϟ8 ;7\;a-TQսy'w'oƺ7O(ܲm̙Ob)d! )ozN qK\\~ rȰͫr%d%{"!#4T^#,%&ƥY ׊वeYDsʗ/|~ܹ668GHmYjڵk D}QjG&e˖-[ G*j-B.+jdur T%|b&АL&3qgԉN:$0zcapB+W^r߅,{@`E?뻋~8xpWǾ>oݭZu_M}S яSm(}po@M0U+~}?n۱iX`A yٰ . 0;|wc DP2^읂CĐ8$^EVab\0#N%@r K|{Y%'MBiL͹SO=jm^\F@Dtl8G&⌿XtY#yC~ovvS[[FDggg"}=!:;:̙s`I+ /heWڷ%tu$sVee|s;JbZ~fz۹߿S־{ǎիךVs wdj{A++p$,ϲsSӣʜZ)=bU P¡읂C:X^TEkdW佞Z?'ܬYϟ?a„֝v?v/<60^ ^Ø!ZYzȲ}CǏ5y5s p(Nhh|bjb/n~^`<(Pmߺ1f>-`f2|tٲl6\rx܈e]6Zr\{۲Q PC9JEڻ]՞3ea% JVw "0@#I@RȀV%z#f켡MOeڵӂ+ Y)Z$/Mzl_c\.wu=c7o^dPuum۶m1'tGGue 㒈 f"J Y?d$q.Lq: J^*858g8TCdTs<CeH# G0w-UQH_F7N[ԖE*Qd2 & , BykaD)}#z[!5pm$]޸r+؂4iO}*͞z_oֆB>Gk (& Qh\1JaJ)C*jh[$ޚ'!؀PD OZ/*tզh]}@-,A8m#&ҥ׾d%{FP(>-Y}#eLXYoQ&".DggGf7:t:}xK.FKGsÉ璉͒*H)H 51F 5RA BRl!C 2z \$Pp ?GYrp}$\<0zQOY d$L@C#BʊcKOG1-GB##@hpYŊw(23doH2Z͘2[߸ANc4*8aLn;sw}?׿^좍P_aa~o+.0M"1i2v81TtO B4[!": ghGVZ~P%ʆ_}C%+; ! 'NJ@r#RɎ*PgdzӚt[Cf՚s 4pDd9JF0R `42@R# 'SFC7?H4c}W_zyTp#H@@-%tZ͖Yfw}H|% ̎v*LhI!?r<--dட-Ad@0Y a,QXZ(0h%r1D\+T@G2k( !VI ړ;HݧD3dkr4 /%+;)$"{]L30_3d#d*M|@߇P@"BMʬҜI 8H#qkYa%&y/j001?5wM;8<Mwc Eg-"Jѱyp8|W/]jӦSL=Q ΑC7oԧ:uQݸؗa{\K޳ޡl n6įt{G?i$bG#uw_j_WgNSFyoLk`2ѐg.c+e5J3]S(Y18I"mX`$ Rd/1SayqFAf u1e#c $Db1H3" IH?CDQdg$Q]|Xő> q`pp*++ ]bϞ=I'tYg!)Adɒ[:=Z&6O7csM67D874|ӟ|/-YSO빢;؉Y3<ܳٿ!͛{?oL +?|׸ݾk̳$2DL!h ,PV +PJ!ɴb17wIcRxH5;C(U4b2S9j9V"sd#2 4""Jd^ B۷o޿O;T*nG??s9G)93 橽]߭ӮM9&{~.;p߻O?w҇?o^K¦m7pqM~~㓭m>g֛?M!& cA(Fmmm8wa޽s !Qٶ|`ɒ%ϝ; ”j-s\x&}?ÑCX_T*e+QC*`BH_# U` Гo3KK H Pj P1vT(!1"HjF 0e&dUW¡CPg` o(cscAh,]0:8lO7U{~bs{聇ǰ;yzh[ux8u OwW="pSsӋ[7Iq9@dZi|"PGEbH&qxUeUo_@?3M_<ϻꪫ2X=/`˩9qHs\EE.;"35$]%@ h#HS!hԚn+RQ6,Gc<D,}85¨7*͸~GE[⑧ÓGOB:RBXa~ IB~҈3D$ i1mU_iP@aAP5Yth5*C -!t,rchPĴ&"bQ9-DW'@y?Ԩ'tG51Pߎ2ƚB  Q[L! BMfR !q`؃cL)uL`aaQo"R~*HDPc}Cqs4r!64w"߅ZQg&-HJ) r 8+sӓ( !@$!!8 Xzڻ fm؀ !rHL?nO`2zzt[U'}C цp`f7IRHgάX"ujojGLܫط$5f+++-Z/~'!F"/nZCCǎ NOS͘1#666_> qsbܹSLygo&۶q׽9l~dȢK^@Wu46qP `;Ne* `}iВA#풒X$OB{)**{D"i*)͌+;MGd檹n_?8[Cg̙p/)/h t*57h7|S[ZZ~۷/w4=7n曛O9Xֳgnkkiomہ@o,@$|MD/AFJkL&NJ1!  .`Y$J$INQunaUccmǏO&R&+ol*&M:zFw֖dpFiΝ2|вD=;M yRj GDIsO^{v\}?Yh*fISjښc,qݻwi+3^͛~JimpC:AiuݹsڶmfN 1V__N:o4i͛[dyj@)3@S?{߻cC=~T:,K)E.UWb<'%C.d*AcYv:)w1{_\dIii.\x=3-ugnx;^}MN4Gb/-7k$d6&h9}疷U6o"+X Zb9vء,7?џNßſSWbHTgsgTT=I)D\مZkH}-0Y;o%h\xKvoā6Bs#=`0xܵQOB )d!D̀i=HLWYm}qIUV&89P?) <s뺦iJ) 𛋧NZSSy^SSM"z7p2͉@P]\sG'Mska)' 0=0PdHmM HrR ky… ?я{ォV6mZQQ@aIqqUKy x)NڳHX^7-A/24+RйvOXJTTBȲ^x_y} NFJg2is)++LӹXmȅxuނb;-9!1 A7wEXpJ'ã.-ffN"sOt(dA.0=00ොCGU/K=Ѿ73e4>rBrut8a欺SҶ KKʕTR,)+۲9/5@H-"Ө,j9Z7gno@e2w`kn1KPK.5n3DM zt5!+-/șN/h48Ϣn4CU7q*Qyyyqqq*:ZSd.M&Z CNYuI%Kށ#F Zj"%`81$NԨ4o44 *++GFF|_jx_wށ>x⢢Yfm۶-(أFde͞=k6ZzZ ڊQfs*E&b >+:ŢWY4 +IwNRs>}={ q\#J-oyRUS)0783\SʌKX֚!K)HVDDz sa 8../ih7-m !S>g *G->c);-4WLg2iϓ9R`T>z@X!ȁxwS]0}&bJJJ …iδ" %mDLz$ 0,1Ԝ*0 f \'ု-j~d|Rjԩ*oZ[[GGG5NJ$W@txwǷFy?rS D!,$21Ԥ8c!䪜fUWW~j"[mjdv)[m5U1kU[32V-[.9kɬw$be3~5#xg$R>io4ݿ[[뺷rKGG'#N4$###>X*jllܳgϔ)SL= ~ Q.f$X`mזFb^JBP!j$Ҍ0D HLjfmΐc@ ]+V3gNoojkk.\iÇǜZfhYfx GLW"`"'Q:d.SdqqpcG[Z .MG'0SZK).]zxI_޺9*cKl%J]$5\i- Zh/@ Lf4h4zС'wyebqHw R$F7K?8:nuDmC#3e8g49D~?WQ.o-[6o|W<088y˜smu71.B dnq` \iYhpGYmMY"Rȴ\+3Ӵ@)Xb qdJ4! cR*ۿeYBW[wc' n҉30\B,0 H)NYld䴲 1~eVY4V_}fOOwF1B4Bkv3޼uK,@ 0 w#ޞ5 ! ڞL x# Bz,1t:YD">\;;\k׷hѢG}tUUU|[1 1B9ؚY5͏QڤTo. .w A1S1ɴ4&9C"@4cѹǥh42yei16_zȉ[1vls_Ʋ8hEJR<75Y #:nmjkI;y vYQkH$v[QQDžw+S`MMMKKڵko馗_~yJ)+w۳3f̸ #GL>[o]nȘD%0ƤDrjuApc;F[kV]q=P'T4g&eS[T%LC~9yNA8zhDJLEM˔RћC 4eE4CBԮiB'0XfDW18YCc.DDD1c"hB"`\7̓|ot٢g lxy9e zq{u /]:)9GցF 'u(jԉSv]sϬ Q=qJ^~exTxC9sZ)0+GtL #@{E,hi%P}lb\͑c iΑf@tZ$8_җ-[6w\!ĉ'֯{!C\$n8ѺG"e)0 Ӕn;\Xk'WX0%;tciy|ss'~LcSVP#q"կ~7ߪ7m޼Y1~%cֻ3~s9~饗&N@k9sgc&\ρZ=!MM DP2LkKY K) %Q/-551 u]m3g\/ P7h tquuiE y*Fw<$zYiSc򙐓$Ҟ*i .f9M6@473R+Wn߾ݶŸdUUi/*).v]ѣR`*m f:m\q뛚r-ӧo}^ "ҤEs>8 xANe^@ <2 %d${5 1e泼+oOHd A?2{DWbޓ,˺˃mۜs˲\&_w5i]N[:򗣏 {S-?g6X%?ݟCgOZ$\#P>CjM@54?5'E;^oNcbž2^g# 﫟y0r2$b'W@ r3P(,9h:rԇy'VjPJr4hx0Ȓ?A3~1&m#@ *uѰHkԊ1Q֓!"|[ZPGg;uw#2igՕU@m; r}\Y'Nx-mٲ~_ӐO(Beko!ae:"BNeZH3J#>y( _3Y BLmhZ{sW:xo|K2!rfHLg=6vP4Nùhsֶ}K.!2koo/***//u>OsW/t@.#Tް- O39%B^؂`a$C #gW~x:K9*9ݙq{`pp%4CNΌ35[;XG: O!dɒ\7Ik]XX$ Ư_U<4.'. *=DDi 1$^L|jϓ>]p1oR0B>%eum}΍gil@OO,X===Tʟ\$tfΌw~<8䷰| OrF#͵:d2>04o`֞uww' Mgjf!d3 ~ݝY*%U,9+sn}!L[Knܸѧ%VJi1Ǔ C ,ܿk@"&pЗg?٣Nrdx<׼#2|̚>3m@44ɗ^xQyYEg{Ǻ /'c @w# 뺁@Y_3P]]k2oon,PUybCF=3ϻpI/ (Wҹ.+^z uG?j`?o>OVէ2InȮw-5{9kϞ=`зdgwn7{g9cΌE+}V.]ƴ9_{7iD[=لLƏrv0L&SRRN}0@ `Eٮ'zmN1,g{y%B)9,/IȽI"1X? l`\wo쐃U8єa03'5 %N}M:- IDATZRTԳ/6Ι.|nY:**+Jt̙YvVAI<*UWt?s]7p;::8$X#nL08?_ x|s۟3jvy[j'-R[y_<`7w N8 OUF܈ 4"5o|A>{OpF_L)> @k@@$06#Na'a.ł(ȓ``b2Y7i)APg"D4>t>ͻ77h/NPOXBKHy%@_p;={AKĖƹ3!eT292 ܹulD$fNizm d`: (Rd43" ݬ"4ā)pL)d8F2$H eGhX2ch5h:`hp$Ɉ -\䠤 AsZS۔3֕ rAr3evȀT}<̌_|w+cH}}1.)^'ToI veW23fBX\TFE~jϿLgO0/_}$Evaf ЈJI!$/ƹԤ k5]=DTMЁ ^ '!I F&DF7Z+ &%ː5>CR 4ixo!v>Ji79ǣMM'ZT:7`#ޭUQ2R*JF";wR{vvzJe;}qpR*h0NҚaޏgTT{k_@ H/l$sOWp/s]c cºh2vI+y50D9όqsra.!EӘf4h/8"RҘ W4q :k&ZQPWDb}ᙻVmWRDZi"PJ3ZiDԀ2 PL:JJ!PJ;hE1'x^B{Id2D"DFcD?hBRzsR|-I m4q,>qZz8P]?ۼq]4:@KdPYYos0rlR8ku|o44O GimtBjHSNd;t2ҵ'yN&c;ױݴU^\׵F*r82l;%r]736ksRnG͟=S|`k;6߿Bw#/[^_YOvP.(5NL- @u?w\ }oy yjk_pʪzaSfe];|> VFar8aL#qOή~`i|h[m)-8;S$xHO2add\ B]uU8yr9. 3qhd R3f ,_s5?*4iӧΤ3^z)clԩ#JiR&T}X⽩A2W^<{{(ەؼys]]ys[|^8^ښښښڇm;vڱcǒ%*jkj|ÇslێbH$DD"M= da1qrWq;.Vwgh-.,<'A L6ӟ-[n8pp޼y###ӦM{ǎw_4wu7=5c̚J&?RY}3CiMS(3\ V JJ㚐F$N PKy h2_ֿOy^q4H?Ѽ_{^؜&v}o׮?mO@ vCx"9ڼ_7:rdֿFF @/pّc]}Ů;m~e8 rWokpʌt`W7~_ߏ]XT2iѸ|#ƏມakDR~wGo}?k>Oϙ~$ Ǣt* R3 dc"K)FbQ1h TF)P+R ax`T4F3xxx2\w8kcwULy׽p9Օջ9kmm9qٲe]G+=ϳ,H!%ӧO+(WU􏸮+= g̘IST2>J)u Sʖ ZJ|L: -2mzl$bVx h0D<g 9\}ΌD"aΝSLkjj \pA&6mڌY} s斗v UVV544tvt+%{x?cʹ|Y&4Bs.@ض!Oeey8|F 3;i@#'$D@ ,!%&)=)RKcJ>ħ>Bڕ'NwH:`e<)^ _ۿ]֢ ^V^^T$qVQ^uA'R2YVZ|;p=Oq _h?v1dLaeMWQіLdv&-7JTäe/+,(ƠaWWUs'?cρ;>Y2-) b"Lf4gl8L,0a8644ȯ;wN:Νa\z'O Lnhػwر¢ʪHCtww#c0=vH_h[[W\/_hqiiآGg/Z,mMè(jniٰa _H$===S4 vskk;K~U,PRZ 2@`, Upans)@mĢd3e@͚Z8hb 0[SS} 7xSO=uՕWzz{JJKxYYپ}9yf'Ycd%+Yj`#Ln"{@$ޜíAl&.]Ձd*DZNqNڧ~O`.ͯ2,bxbM]. Ae;"XH1~[7DE\uᣏ<}g?7\PQ-^c?P@ Ke\?/-o߹O#~w͗01L̰0/mx?0/Ѓ74WtBv~||vۮd2TIht^h0zT0,1N:a*=ZOMm=3ͻ_{&T($(W[[RRucגŗM8qVyRYyW,>%=[hw|Ɩ-[N2!/J# kPl ?RN0\jB+@Z@sKA:yJ6w6.NɌYs@fACRED#Z&"`,DHa،wSJ^h{ou(..V0N;ڎ?8 կ[oEl۶,듟}~g5޳zY|xBKA!Ȉ4#d- M^r֞'9gi[5B_?[bXNsadӊ@[6lXB*FĤf iI}>teuQ;XN]p=kxccd CXbƌKy2]- BE/ܻ ^xHӪE܎s?5wEߵtk2]{YR^D./+^uݪ) Ktd#/_Y`aK{ǑGh{遟o XI NYyŁX"љ_]*ظqT.*o.s }<$(ÎV<Ю(耔'{[Kݬi 4ͰL_w4*Y'=Ȇ`b/RöS?%Cp]%`ӑNcׯpaaZID|i>5< `c{<5g- ػ=?j=\c:t)A+.{@ ,kޠP%0dY6'+=O:SzNgK$7 !l$[qnAG/<[6lܮ4[_غq޻7O1#Nt:ۡd24ѾȰp@fVA*D"ms=c Lp|h$iBUR矵[rrn()y/kk[[Z/lK/ q]Y޼oχ>g ,|;w˛(110" Ld0 !O)Hm"ȊSN BzΔR#pܔŌcMǛ@ YVs3֠6cM 0NPs2ȹZjbɘMSRsaj9)Md2h2s4?W1B1p})\ g'Ss)?47Dw1KFʒlS) ?ϙ1tF!1GW!cuVmzPݛvVԮ~۶m]lq&VCC")Ƙ 3<:^k}̠$"XLuEDN/I<8}Nܒ"+,YluRD X@L9mセ8(&yc^@̙szz׻|?$&WM<} ,QVqS)&xE===xsl;?5"Ƌ,JeYYsRޞT1?q?xxmGw4+_ |螾s8q1\re6og|jߍb!u-ҨE1zp$T@⩐[f3)˲m p0uxg{BKaE*RnBXHrx`#rLS =&Dm57LҒ-KKR԰&wH\[@+ʁ3ZqJMJ Y,k]ʿK_G>~@Jƹ^ oD/uā;36~=PAPH.Xj-BaJD*=ԙƪ^\_=jҹs7~Cg5@yy@sAs`RZiF-!5 IDATAM.'+/-TzJZftc)Q9stٕ 2+R%%So߶p쎎mm'O>sfqy5WT>xo:x8>zhSI$Hp[Y׷@/t_7$P>aJrvGE̖ĸD"iS(,F6L#)M1 \%ג8ȥ˅'%Q1@k)G N%9vG阓-ӑC3! ,F! *bȄ#FǶ@ ,$mS% ."_uy˴BctFp 8F"/nVV_Uo(A_tYuGnft$ٍrwˋg";wIG Ѯ/oywPII  vm{RŽZg2L&5㘏3nYVOO뺣D ι$:߲+.EŶw0_QQֿBii>Nlٴ%m߾]M c9'M5(CWC8wVɡ=%HD PGJI)C[xR BjCkĔ H eQ%<:fLb1~Y3sd_ '߿_K:w(I#p΂ oY~`W2b/={RTP0)8TOOς ***oYa|L eYWXaī8ΫjsZi@ZKMε a gc0Ҧ4I)(2hh -NR---DK)p$`{HpёnZ |h[0>w7\7^|f8{^GnG_,.Ʉ᠀Rq:&hbUmOƖ WJgH]$A<"ynCA ˆV~zC7_)\Slwnx ە7Dc@?qs _9sXxd$8GD'9B nBr`,}xnDDJq8Ώ_k_{I,+eEQdvS2? 'J Q3=ql漍wd)TJEQdYUf>D{"St]8r1~cfМLRO2'3/Lp00 ?c88Ad[~/I3SQ\+dHH6r5/9mdBmJ)˲mۜs3gq l`qlq0 ͱqleTJkmY0LXB!DrG}5#q,ta¶i@#Ť]kq r(@$dq!&(F#\ 1Γ&0o,C@g \Nj8 RBۨ,qԠi C‚R6BP$DbȅGę 1cz˾#F쌥L/I@iTT8;g֌Lu S,XNnЪ+&GAn:\е\uwObu7] q\7}-9iRS:;V/^ "d[6Q sTnOX!Wzܹʪ WTV7Zڲum+MHS˰`딒ֵuk[ZZjpavǝeW.]RQQ~Il  ܦq=όKpÍVhl.؜5@ЙW~^1W-?UIdu,f!5lcH)]M Zww/87;d2TJEL7Ng2JLAG˲mF f\!Ye`2ia}2ǐtR'FL%_-b T8Q]7wq-c_}%e_׾3H'ա( B xdxc|+#RƂq`;wcFls'`!j0"+I\>foeˮ\0~r(|U(۵+)D{Wк>V\˟[gu\~ՕW^;ٳg8,ևB+7-M’4+)Sqԙ3MU|TzM?߳q˧N+hhqF9ȼ0 m>v[n^yusNwzxƜ<(Gϝ= ##G+ ByiHssu o߲eZG`6;",q)5JhsݿE|{ʤǎk5{w,*;z\/Rba׏\c&}q40:I:|T}eeYf776h4 n4!C0cFA"9iif`3PwWwEc/(ɽ8~[űK`梏+8%1%"DfL:l,Hy Ya>! I20 1i.f̪}l C~tEt8 )**`  ` G17c%H)A/NDNs\ޢ(r0 3BFky 3;N7G .Zxgϯu5iuvǏ}! Ғ}*ޑQ=akVy/]~Z74?XcZˈ.׀X ?=a `-};!șxW8'# `!SyˏhZM;x;rF!rΆ[п18jߎmb ~[bd~RY?yO~!;[_g'OZ+!<~đ&_Y_-A@7j/r|{6 r$YCofQ#1ӨcM)d!icLpedH(pd(!0 ؅8.VgΘsI@AM)^i  , iб'W=+B`p6 XŠI-[)+Ҝ +i!C,Rز\2BR@)!qJEe‘1*Bdxbi =!3rH8 Ɗ]zDEIUE<"m1b۶C>R獶km2{Lp$a'&gR=6YLB)U^^nP(HxXEjɟ\׵mqs<3c=A/{2s17!90 0\Ps-vm~l~^eG_WkoqV 9_>@ .)$&BE VUXܼ'羾gٮiSg(MM7_lケ8cw}쑇ULHd+2_*&M^,U| %V7cwf̨+j}|mFdze/sBL$3y1VYQ[WfbAhi9{I۲,a/MFD9 )*hqHR1"c86jfvJ_xa PFIz"'2s+S&AQ{< $b,@I#"8r@mԯl;I^eRQ1׵c(/t(W1lg|\^^?VmN.9(tH&9Z2VE(c@̤0dZwҶQC(-]VV&UVvH+) [(z$G]RҲ0?V2F-,ͳ\<aTf*;;;3hyll˙bm0,KJ( (!L 5b6Nc(`mv!L)` @II ctù쁃en>UGگY4͛/>}3O=N]Vf/j5ÎV(cYY3z̝[7:%̿b MM'͛ܳ ˺R%>oShK-s@"EB(ϤȘPj2n|7M1eo("L!RL)R3NZňĘ$XD0Br}F` h-cE+̲LIT(Ժ@xy,0tdSgΞEw.X$ ;k1kˏ冊R}yQaDNp)oROϡ\j;;[Zce\#U,{-aڒS'O':tiʔI/D/[#O}s}}}zho/iهvm5|gS7UxI'>?J?‰k::O˗_H;ԭ!hXV\t۶pt1|ȬQmf5u~Sk1%"%! 72%%jdq Ru]45E0 V ql;2*`(1Rf 7B2 54_>Í { _~_7o, B.DF&yDZ@-8s(p.rJY,Ca6Rą+D[!$D !MB؊|-chxiX 8P0:R^:ZK"0(a<<'ॕRh -"_߶" -YЊLZetO @Jk` 3ቹ9$` P1ԊL.h<.pGZEC/\<4m;Moz~ |G([J#0( lfE@ 81a%4ݽ֬-))5wѣ;o޼F|/ 3JMqپ}M%'?_r5 uvOXVl!?t`?ܧIPsG |7Gq?k%M2z;۶/|܅Ul?9G/lx2}TRIιˮ\񮮮 RJMXeP9rdӛo>}̙K*ّaKX,#{yX OmۜP)}IhZ{g,L:f H$ABq52KX)S anGG0u.SJUTT r9c B'7$F]j5< 34Fya2(i #4g8I%AEF-UJ؏]l١N>jժ4E} B|sϞ6v+Wh{Jou`II/?7cIm҉c;wl8wEv"eO:yPYIڵkG О{gs^mkj4^z:-!n.yc{g, )21ZQ6NRmv%R̉_}5+<;|s9ڱs]F?w͐ ضkOBȐ{m|)u8}T#DZ[X5wGN憳 m|dhdhhȲ,z79{Dʶؾ{@%PR1.%]&[97cteFA` yF={6IX%&@Z:A2%˯vjY9gYr\'"d< R/8ab{k[k[Kkk.pph8WTT0ƊGFF-P&c8P3P%`HCZC(csE{..a4xm-4㌌ͨ H312@ SZ1diE4 Bg tHCt9 B lf(z, 0q"0F Kք1#'F@E$)eix>.aBP qH+sP7@CFΰ ҼF @l+!" :7N_nWdh BL7_pޙSvl/]jc#e=MU奶taЩN:g7M_ho{c<͆dBౣGNl (*1i:I#u;e:cb2$ Aayւcٽ4εǎ;yiέЁ]wܕ/3_o=g^g{;!|cZ쮮8 Vz$,h2 Wa]~#犘GH[p3$HƖm#N K.=vXe)5jiPOBRkdfY7USIJT$s((~۷n_=ڟ;nXzmk磒W| |xbt:]QQDZƭ6 dt셵KZvբ ,\tA()L&jժ{rO=/I!\5d<|h̲nh̫MkmNs1s&cĂeۆp eY~C\я~t]---}]6l&X3f9~5pq3 s^ 3KƜxvŗt#ln$&0d<ޛKӥFa92S(- Èf ";E6ӚR!iA  T;VHXP1q1P1JI !RI Cr Kd̸9#1 8cudi0YB Ԧ94r Fl_6Qd\V]-\ڛO۩]*9ϣn<̛'O:#!`bK$cB;ҩևW JT*U^^d9̓p=(Ҩ 2Lyi;6I cWO8<4@'TUFH*t&#\85<UN, CU(J܄Sr$纷zۋzږ3U&'۵gϟEWln?06ceaT@FeUUU'OX-^8^1`}?gYN{9r`iyQWCd Bmx)l;rz~ [Z)jllLy_mذaS't"X]Y3UO祥׿^_oܹskk RtZJy7߿Rk 3KRʬbloVp#3ewgZ~Ք)SIyg63$؉ytZ;w.c02lD&g.( KLGjeɀ!OFDF%#sALQaxMjr.U*He&N(gs9Us#~.4!g??;vx.rGOkV,~*j #m}yս;77mUsҫ(u~LQȤ*F<;g{,:ȫ[L>znq›;v JTTXSVX+2DRSMv5"iHڴv(ȐcE y@!XBqҁb(NƊ!֨c9"5l)}1 X)YlӅ $Ri4cfoR+rժU~~phh϶g񤩧cu-{ 7|][K(T .pؓW<^ekosmkdxux]{wY@C!He-" m=z4#Mj߾ݝLIYt-/J^9t`6ݻooOO/P i\יi{닜H]}YŇF!ZSeeCC&T(76~ۇx5.x:>zp:]j(S\R(6mZm۶%jmif555B 6/35Kf|k_}~+ֵja0u5ÅΞ_s>;o+䫗^s߄7C 'zJ 8Sɧo-0Io{i*V fa;@H嬬1CԨ4͈&4nu8sYJmI qAȨnɬ"+ypt&HsAYsG04Q2"4KD@fL" ?^{ k88Q3qxN=gƫ ű ڣ^f0ؚ%̞=[FÇR|ʽ#['y^rxЉY3gd2ޞnzwttu950пzGO#!dZ`3G1}}/ƱX\TG;O7iH#2QEH,D+JA3Apa=)v=7HVbmmJ'OX;9uTGr\}'W^8C2淦XB-0ͧN+FAYsQ nRܨW)ۛtT*Czрm]rDf' =2 Rjԩm۶c}cjQ|h0!EA8mr9D7^,Fm`ZsLȰ+cbdJJQy3МI%_rjnVk%^{e휙 h~mZ[Κ;[kRxgo>p.Dsg[ ڎ0<h=`v3׮\9SY^!8۲yR^ٚqÍMҫV k͛Mav`(S<֭_=M?>4)[rӲן}:nQFML.CFM v@Sa,P 5"^ ZeӀLk!bf̈/#ѓcG!2FllmrD4:ɯѲ%jzwD a#35"їIl(9%~_[o'?WS#G _xO9vXY^1WʊJx.WP3fnٲmۖͳfϝRSp93fNG[k["}H\*H؂]'U]=x|Μyzc_W\Jj~ZIǙ̱T-Mb!"KoX=q9%e@Үs,@&$=oB+(7wڛnx㟂([7cv]L<@9jҩT!㦢̲9S -2fLyIIzqtGpι@ƙkY ,Lx=(2'a*7f5˷(+8XCCC}}yĩ'Obԟ\>/xS1``8q3W &8jl9/ "GZk-_{l1O9/l|5S|곟v |o&g,#ҶeˮlުUZZ<֦-K,t;+.[$NkgΜ~ElVmQٮwv}5 EmCóg̤R_{5   ۶6a>] 4 l BA/׬XV7%fip$ ~vN2 ֎(1 Q1窷{rBYLZ\g2QWO\9Yfݰ{%)A ml$Sm .&sUD,\4bLP;`+ ^lF0sl9@((( ,bZ6Dд! 4A, A6 5XcKH*q,㻅P*P9`t1RrXö%#NjqKgsعpmixƟ?_*8[.wlo|-S%eCml|K_x„CuVpUӉ5Ul?^VZ~ _>7Ŕ差U(/9m(otΦ'+.x##~/>ɐy~Rs?f|$1?3Lsk@ܪh)/d86XEqTҲIm a(XIsC(rmc"纮=06!q{3NfDa\r"Ɣqs`AdiEtI"@k'5IvJAv͍'G!Cq[֤Jů:}*S@1N .ܵ=,{>3l;_~b[CIT:O~6IϮ|qqH迺EHOt&?D̖`6"CP4h_ kV}b[ Qړh<3pON"-yD>mWFМb+΁mgĹTV0Iɵ ,GX:9㬨ȶ F"5ШI$㶉~(nj5)b @'#$M< 8ʩF-? QDhC̡ӌ@hJz3M+2Ķ6" w;۬]o„D-[lWv쪁 럶LF@B^C}6;tJKfLpi%/ k9_K(\v$I,-)O~TO;w;b38kl/z}҅ ]誽6 :k?3?ˎh4iO~tkSy EE%av8 8,Z|{qee ge,a$}Yy&700ພe us5i1hfF$E#LHI`2rPC9e(-KN+B"MU4ٽ_gۮ]^y3V)Čg#h[;)RH³sqfNۊE ػXĢ.$W[\8%Nn77ωȱVlW{+@}{;ef~`NbOϺX9;3-J0I)$^8LH(, C L@B 2 C'91gؗO5W>^1@˅A) 9K78ASDxFPAbpAUP8!R QI {nyeY6ԁj 2F\ljDD 1%n8AA$H$͢p)!\Pp䀀aQI@㍢АpԑH xAF:@KJP1 0I$ c(<(HA)% ephR#D!é?!Q.2sx3'(\EȤad|l$nXDPBtGjj(\I -HI\A)%Y!${  XHHeeeeeessS.J=.#TwbF $E300BGSJsm.޲>=6:Y[p tFɓLZ:#˗4uvG)ratbh飇ny/]}gO*.I$*/3Vg?KLJia]ͳ/v2t=ɮXԩSE qיG鉁t&1bH&ұښcǎ /2t'(o!u4q~ I3{}*zm۶M`zQ fj;0LWr]M]Sr 4-KxXaBl6֊|$b1?::ʜz󪇾& IDAT)>J[A}7XU'T<4_ܛij|U"FJmX8ZgwatM7X%z~ub(DmzY0Gdj׀xm@RZgK ]ttDB @A MYy11y/|%B(DžiH)j܊]z8(>ⶑTz! r'(<2@HAfmK2V0w޾wߞ `-7NOR0#gڽS0K Rn߾(roϥNJɓ')H5LdN/z큐CVcT:3O3]B/[٦Q(ү2%EJqǶmۈyyOf}BSrj#܉XP34r.ᄆl4TK.].8™P鬱 Us]h7?~ ?K[YS֜M\@D^ҪL#Fo?ЗmhD峤&L @N> 䲙Ԫxt2ER<L'4׃@)͎jbì{n X݇>z'IHjhp21s=o8~`T*J"??O%&KٽmP(s=ۗY3qYs9-1fR49v;qJ̠&Xo><{S=]ݷ.\% &8RB:Rx L)l=!2MIxBEԥM&W4XV]HIC&Q4Ft8B@'T9`"c49Rv|[c]y<1JIQz\ z$MJTlDt=($eFYIirb*ݏ. |R7t.Aud[# |@ }å%+-\|hZk>ur||;/ΫaL~+ʊ A !aZQH@JNQ$H~XK =T+l24I\'TqfQ TR2C1MJJJK&~썡DfYnЙ=T^~UYݲ?aO)~=]l>PTwg}jҦH0tu{o8}n[*+zO:|O_{o_;~0eJ[O_zӧ >ݯWdUUx,z{#`[YQW8-J[ds?ECBjnkuPϵӘ?:<_?P):00L&X~vK\NKJb/>yn7ͥL͗^/-3k775xR[|w²Zˎ^i?W/0+Eלs[]EjK(ij*m7|[>34ԟmvY_1mTG>] Gu PU]UZwYա:`:O/\42xOۚY] u̓ON`5unذ+w/hzxc`Bˬ=M-lI*{y l!-[X, 5Ω\.Xz bsOߥs._O677?v>ۏ[J*e(d Bu(S+I8‹p]umpnlpo\2ɓa(c@ G_>e/q]gpp#kkt}##ï"A]׃\!sc?E T!$fk3PXR %M߯8c nE~#7.ؖ +dvc,vW aK;/yORF(Pxb {v;kNW-Y0e~2 j%a<V $Sws[۟_4oj7^\z`s_-鎎Tn55זNC%N*@4O@uUI?wnǛ )DiE^UM믿nSe%+Vjv؇Zp]m]k_[ߴ韜pyeg-\]LK獏?~CO =,)buU5%c{Fقر{^sU2morϝ[(dcn{y׷XEd ~5<:8н[w]Z8wYrйW ,^4ȑ\kMӯhaB( Wdn E3'քzւ.(,^N#O<|*Yuҹ LzàD~`efRWPò%V[Ts~{٢UhW[giptG/[xZݘ8q"iK[Su?;$ X[[ֶ.fVՔڻŔ[oWزƪrciK#4Huu6͙uc '&+*KJJ֯_ORLGh$ii̵%O9M ӧ+**>! ӧO WVU%SHtcl: p&Ӊ68s&*CI,˲,+DHCUS!g7*\|_p%Cɴ.?R[xLQ}}+Du$F- iEM:%?6߻@E KveJCm^|<7̛B溅R}`έ[OAAJ¹,Y<6rdh,8hnZU-uֽG6¦k6I RچlxPXp>i \kOn矾7M$5wsn(X7v!8ss[f! (,S@ g[5i2:x2;qx2 {oTAMصkDggnѢElnʕ==W:xP(L&wؑbXieY&ub{x8={v"SAѯT(H?_JP$室dp6b*J)UU]zv/h(^:Pr]Hu<04I@̥A3*3!!Di&HhʷD2 H%WG}t횦1LHJnh!N=w5rK|r$ R"VTT0CfPk( 7v"(_C ("4SuA4 a=0<ێRp.6B({V:,)e)=J5) ]#h~ PBjs3V]$|݅߆(RzE֬Yկ~ݾ`JZ[3 gJDdLD{Pj !$.j= ʽCso_{C3 2Q˭`6,kou'':=VDFiʰ5%L)$(dG  Cg2R 2PfEY hPD({EI0L+Ұ\``]o zI #d  ۃP0(h22W3@ 25yvS@oOi1WpQ@)) AԹ"C㸆a !I^3*!(-WJh|rufn~UyssYM׌0Ш.e>zͽ 'nlhNܰ~CS[e'z-㣣͕'ZO{*jg6Wk:s_.3;Rɛ֯9z~h<:[[Z}U]sS7V-Z'Y))"D(JTAI}9o7[K}#'O (\eYK.#ѨHF"ђX,e뮻rO41 c/bf0梑XUU ADHɃeYtZ:%7_mȯ?|S%(pVpDU+OTxV}9 ZWf:[I.I%*$OS$H}E)1NNS[R ML=v  c넭,0B]7ɥbF_YPg*!E2]m)=T"%E@ l/i@$`{H 3uWgÔ *" # AF('s_v!%zH=w PG/u>8NNOّ b?%)S% H0@}-pht˭/?^lgJ$Si)N4VWBONFG Ӽ}fG^}.\]im K<CcӵϷDJc]g:; z'oݼ̙ ["b?ϾKu?u74o_x=H"1>24l]t֢$=h?}e=n믿y m{׾iUN5@Nq8ީ>L?T{ XfU~׆6/euCGAz{^D2̲sy\&ms⺶"c\l: /lme%%%U8. ≔,h>!VOgfU/8WC )"MF)3i?WǴaU_߰|ŊX (e󷔶 IDAT\h$*`LkkkC0)9 c:pW$\cXXT؄M߼y3gFWuj٩o)yS!D,󾐏B+1++%rJ8 AH~Li>{rnZ:Isdрykc8i (%8!g"(-%۶[Hk+!r"8h I D*ܠRD[?;* ,1]uS>U "I For@:}k'Oc2NvT#mkNw0T﫨>߱ ,* +A`j(R.adT$J0 6%:gĕ5U&qG7A S~RSD` C"A Ŀ.;Ozz¹>w CLF'g^j?4@$q`ߞBA^-O=OP=oۗ =qvȤW^|eG.XiCN%SֿhOy)t=GK: C5> 1HW@0{э7v_Lgݿxႎ3{,[Wc{v>1& BBJ Cd󞟗֘B (ʞ1uJ5MsŴր$|fT_KD - Gs/LRQ] .B<9S].T7('cv1L0]C tJc`i0]/:._|ڵƮ]֜TD7=[5%R~Gn&L)U+;ͭ/n\Je筼NmYTKPnۍݼ7 =}@UCln՚T&]4Fl,x|@N# | jȁT Z0pGz 4R~™|6եr˿XoW'X~m7's8!gt~YZ:ChqfE$RA8.(dLJn(l BlEh9DFC%H&WEH04 P p]RF4 H&ofsEع$4 %uO: HHIǓFƁ ML'O4 Q N&Pؽ{h"uuv_{0}ۺRa}o}Cc{P͍JKc?{Zp~M7UԾ畕-S@47Qf C/ S|ypFPB)EͦQ@G 76ǢKM)wڥQC{*ܷ'%@fBv!:hi?E}۶m{챏v^~㚋mUۇs> +W\uG\`}; !GrEr@8K[fb2d34!AhUuUEEE2{pYYYWW2s%d *)h/8SP(dZTuUw%VxR|o6Ϟ;4"v}mTHR{ wi~޹aQmQS"4"*tŁ3O]Xl/A5Ɛ&¡ ]:0VMHJMRGr@Y(.u. K_?fvSSc,+L/$%>؟LJ; >*Z2ww QronSrUڨ|… <۲e /pM7[}Qի޻<'LY@XѱsS{ 6aj⢶*&THPRV]2Χ@DШbdKL z(%NDaЋ)ri P@ʐ2o|@&DHH{ۅ5*)ݗz }2\2ԃQ&y[ HI N{8J8EhJ"MgUh빃g4h };sOy_WvE')䏪hY0(v U/R2FBBBƹ*ћhΏQI쉪PB_P^םԉsϏOLf. LҲT|"_tѣӭrhyYGAR3G Z4٤%%+3Db%gϜ .ݻw{fSXTAT#?s)%Z.{mK*YxǪE. Jʫٴ88gEGK ]/=sG9{-T194>›p#I˼aTU 66:\UF(tv ݫε_GfUWHjdž;;uˊƱ8_ܲpOXRVVF,=WUWm^ZZj0`]㓓sf7T \|Db]wF;|{ŗkU+3 `0(Ajjr/0tU.Շ)S!?(B44۶ B:npԩSHH,ˤ݉D|**xb"SJ9uy|a|G?Q[[i###LaH9?L*CuKեx)*TUWwem@ XZ?ʄHD1IQ6)]8 "G #@@:|0P x(: dhLAL dMI%jRr LH =Q0TF'{ZT[3{9@APC2-Xu/>'NzÏԽ o1=ϝ;sQJ@>7t !̱熮 9PsE͙3{޽]wcSO=/}7No߾=Dia֭n^f8N G?'t]1Jrui]c`0ԝW^!i/^4MࡃDӤcwPD!u=kHlf[[Op lqb hXV pMa@;˸HOcUPuJم Ɂ(PQP}_FTַQ60ѫ]ol+is 5v HbWOLRyđcL<`vKn! \#dweGB1*FG†\V .˗s\.{P _B$ۻ//9L9ᶴUs0G/22RT&4MH-v% b";w lKKM7J&cccfҙd*991H$DZ,Kiv>RYibŋVoڴihhh>V"I !JTQy>cLgDރS;%!ΌRJ(c=0u !,ӚPlZZ}7޻eo-Z≯|/o?mj֜ڴ؋O-]aʍ_?k^=vS/۾Ҵ{{{neK/|0Jx˵wmן[ o^|ǏߴyɑC>zmk9po342k7||raYg*RꚎ J%-:s44j:D7jj>;gΜ?} Q2 $AJN @ȘFSM)S. No{UR_dB((Pb.A\*ִII Q=A%fnФˋTN /&!%R|>ätKE6Y C %H9NQ9Ee}>uvƳf浑H:!1z\Ϟ=zh4ZYYjtFY6ʫz.:Ǐ+8iS{G:NF!-iWT% nM/Z2{O)q{ s[Z+B!H_~e,X@0/~~\֯}k_zoo.]<gO",R3?ݝ%9RS{=u;cd9 ,irٓ׮ؿod }aG}u~ N#IU3M} >IR*ojwyd'X^^2ıv3 m1-iҢtB]mNpXD0O#[@8y bvCB!OleQ(b%%vwlGwπDK׮wt' Y޷=9ZQYkBǟ}Ygl0KAx_WOp=\sҋ=vwoUy9 dR Po7@Z0̙SYYdƦl6W[[+:yԵ^[#d\lk/W_ҢJxF#AI/;66)DȜy^RZڳ6_3:41{v,;/6t[/^8cZ֚5kb%!0\EOή >¥K֭jݼ:*E{u]gkO @M")Q]E*qSe9۱;q&'8L<$qĎXV.YŒFJ%bb' q{;^a,ǑvpqqY]]tɲt:51>qM[…lيk˷lzidd?}?_g}}Wښ%K\CStώ;3|ҥt#?drʖGv7pz!~T[o>sX$sP~` u\?tvvg&#FY$@; R| Je ahT;$U[SkT,̙CyL*h"AR= ES*k@DNf}ЄW8 ."n*7B)mhTy'??2+$*uP] . =.Dm;;w_z饮/~6mZ~՛/m+/ZR;2ݩҞ'3T\{h̉p3֚j_q g9iVCaV3'aI5)܂W̳j_W󶹫MRV硳M.8Q-"?XtܱNrzA1== AL#p Yd s.HX7%Кblyն|PqIȐ"?}JkQ["y_[}"&aZ2 %)ͅo͌mw=444<6<8pnjμ{myj :rsc?~)=Kc[L&NčWt9t鲡ٳf  7͜fPK/-^4fR:Q? ~>~"*ʠF%nI.U`$195J~wH?H>$G=KI#w^{3 "-%c1p"B!,i{7<49ԧ- upy3l>CP{}7?TI3A l-`+m1E/}KBdrl+!8 ǎC`#,Mh,Wˌ˄tt,@@$}6(#@@a)pVECȱ] F, Z9NAEhdI@@65*C&cf )lp 1wuwuM;}y晊s3q[ᑡ㉑|>)%ڎc~GMsԱ{.`;ξoolKivl*fΚꫯ^yѪDf"ߵ{r-3 ]ryc)/MY?z䪫:{Lk\6RyܜMW._Ȳ}%gF RwTمovdb`p荖B>,Q(.ןjz޼==}}sfm}e{]]ڵ+m۱pD Ӓ?ٳfٷ7do{8ZI f W҆" <8zD,G'Zy\׉D"Hl@:ujW_}ylvxxwhh(B5P"1/ԹAxas2X<SUF8m|V00zgęxE+*FEeT7Vh#204Xb?zck!8K "EhZX6M"nAaC ~d.$b!J+瞗_~  iW.p(3DQwr>R*)%/m&r)(bFBL#$WEFH0d>WxT e7nܴY@k2N EΣ,G"+8vDPYVTv;IӤ}r-70dE+cB u.U!0+ĢQ)%haXl%"[2Uvb%];h2j(,I@9% !+93J2[˶miDSJ,իWZf= aY* 4a&_\cY'm1DMMMx V ΝH$t*, x<qeqOW/lɢ#CbX*bfF5Q=2j('#*E( H$R1T*v3JQGgaUV~ m[ʗr(m; %bP"Ԃk 50@[4詴q9If)"<"B(e8g0Z!c-7HX)?x%N? :g&V4"D@^Rj GF;II\zA45r?R)+%0*,ǡ Je!@Ϙ=86:\,xNC.m㩄?zp(i1M Mȶr:(p/G"b]TPA9t*i^<guvS+eFi P-Yʱu>B2FkkZ B-e%8μLmYe9Dž|gH03{8B- Z\A9׍diP2n){zvwyLp{}6"T4 & 7ARs^(,2TSqj g{uffN5~ƕHSϢѨmF-UawܨZTY~9n 8&`I0I C-Vh 0,ɐ& ʻ")n5YLa` &,1FZq , PAR4W6p0B`ڸH299L}}l%θ"E.!hB'ɐsBb5WiFh*^ݺCD2+41!,Ez+ѹ0_P1BF @kCɋ0hΈˉ&mH$Xl 0BBQUsa}NL F5T~?pA#7j4wOE~8 L$m ޿rí(R3=KYΓ.}ז 7؁|,ΜΎ3s. >U\rx޽k\q nsχw㩶ZmKZ3%;{Y o?q0o-@ll4L*{WvvBevm:g,l[sKڰF:O .a^uup'&ͨ=fwexණ.3fK9*3?O]=Qdж-6Ɖ,f)PZ09rE[B<\`pkPbNꦷ▽,X  ł+l"tݘuMR\5X S`Q0*on0*5{N*ddZ|Se*?F!!#@#HZ|Dbtt,X.|2@$KPpV+@]NAUWm(@"C YY{t:!0!UɎV\g֮Y]CƉ#9T (+mY fΚO[wEKۻ5Z 4 ͏:FhUJo9mM7?֗_XضcUH>:Kj%!0Ƙ- ݠ2/"*(a aoR@><75/kO6=L| .9 R40882:`Bxggg2FL0 ٬cVMȍq\ 4 6|\a, l0 +0 cS8Z5?y(זK"?﷾ymzn57g Z~W>{vv҅xϾe+tG?O7OΟ9_}oXym9Z.8Sk_S2|鏾/iZ?㧿\|'lmqŧZ`C}0rˏ7BNM 0U!j ˆ.(ƃK◬^i75.$XQ$DI߹`"s*&WMd)@bIr@frjo2څpS{F"?d6b4@ ID 5ёd9 *2rpjjHC" @b(DDD2d~\q'oyc>dž՟8MRXpb)9dSJ3']7rPLBJF@zsW)62uj um7?ܳQ'bّM|cܾXeS4Z{W (0 7l&z&Jy=x`+/X+ǭk۩t6ZS@+p>g3Rc>C糉|!/5#jx] d9"LiUI#p!* #J` !eW^yU+WWE:W,_79\FncS?"_}>=™s,moi,]ոl-O bu"73w\==;wO$؜xUwg,/i˾5Jtr#Gd\omܟ|g^aZә'2h4 Cd#@8czv$[O4b[ aY ʶ : # R hLF 8'Ci1DK(`R 뇡XD95LrMHLh5nS[:yT֗^žJ23 9S?q~qkV/Z\(PuMph_G)s'-C}S. UhH-E~\]nta#CJ0TXTX.,"2fJC&ohFryӃv,*dqb- L@;Bh @@JC2Θ#aB\؎%`R:Ntcckv%K|͍MMll#I922W 3{ddDk`agv& leyWUUU̚ cv]A0 һ̇*aSImʞ{1(1m~ˆB#wl{̙ڈf|uTfLbbdF\*y+\SOSi?2ߞM=?mR:6ϹkF݌={ ǛzΌcXƻ斖m/3./9۷+n㞽{0\7{]ϞP]KV^H@&[\.q!Ο闙j߾}l@(e=p2˯!ɬZ6GX'Ư>q_mM뺖"l:AXUa3f̄Ls1FB !On~뎟8F>HY&H)128XaUvL559ծ M}g#ЪϢ8mQZ zm[s<+&YRlgN1t >MʶÇȊXO~T}Lvbp+?#r?*p&rZ1-JՓL <hDx^{GEkފ!0J%biDȤE=$;F,UTbH!0|fֻoH"ْfBƒH\zC6h@@ۢ\~L^_%jX ZXH 2$R޼%{k]vE˯zˮ-_}I<Ҭ({8zs.lrЉ΋^9:zLۯio-䆯peԧf78}XdgG?{-nbaȌ3f+֓SRP1$9cE] IDATt d,0 MKMLkW<󪫫GFFRTu곟ïkV;{^She';ۛNgdX LRd2Y,+lIJwXuʁ?oU8D6PeYw`F;_@fTR0.Q䙛M#W;28JZ(T\GR)dkК!F%P+bP6ֹ-邤Hhʹ-yH/ ` 2 EVq%-m!C@3)"&H2B粩4g+3p0N,ͪ 1 25r"Q+K*6Sÿ鏖ovye-!4k|kp􅪯s tnjJ%fXrUB!g(Yb2P*&TƮiYZ:3n$mM޲fS$- bxtҹ?[IhrcJ,֓r^pΎ;_W_}QAC=ҭG˶vSv;ᖝONtt\)ӚW_{=)XziTz>?2 ›2@&)8ߡU蜏Qj*lnn9׶-\ H:\ʕy^‘B>uǢn JyhΘSW[sEflH͛?b& ]gږ]< 7}Mk[J;?m;f p|lLkmLW׾ m-USǺ5"p˵Q1HB*:m.b[ϲƱlԆ֚HkyR1.U2үHVJ q!*"UZ,X4vWUWW5/auHё!W|9$D"aLJ)PO1 gŢQL&_'"qP k*)9٘UaM1cx̤!8IpId5>g`Z*mLyFD8ќF"hH%Ik H "\<و4Sq}~m$̺N9Z+`<7 1oNR0&&$:*DeƸ$W͚s~Hјe o/e 1gC`T"1Sх>x=*N FmgktaOvVwGl|tFk,^!/Foȍ:V!D\ke;I˩k~lfAZwX.+<3:#W`}}=²cIR>޶ciG420>! r ƇIkme2R-vާ5 akkz !PAưP(pƕ$ /RvvvK;_  PVȬ>aǏrR)f_ O#'GX ew z5-N /ش?<"+<CCC{]p[̞=o}U♽;hxā˯zú`>Zlo!XtGūrv[PX/}?xuJ>{n7Ee(CY6I~9a:#1}t#6M Op@jˡ%mA?ɫW%'r"5Q\/09̞L]]۞ie<CŰ,5^ kZ#e;??p$Fg 1_2,A<=ݔfͬIqkcCvg>~]u]TbvGw}{[дm}_xkn߰_bΤW]uKٳ#~OݻC;?o.v?ҥ3 ?ZJ$j5@ |/kؘ r(}+|Oz`66`qG6PS}x&1ph1~ك 3v&+$Ӯ3éЪL +V)jJJu8=U8}ojPD0<pbUO5o '&bNPucfWp ׮[|E/l]W,YjkI\:]|o:w8BdD2p,mk٪k/*e@ l&#D12APo|K,i=C ]ѣ+Wwyر_Ҡɡ(0$,-vs( @A!҅˗Ocer|c~? [BEh-k.YƁc*_)DZӳ cxnUӥu0 вeM݌;G?UWtٛ6qUxW[Oٵ7u#8K^;rRVYn]XHj>11Asi?} gkSgWG"[?짣r9TJ=_gyzk{^|t=_,yekF2#'[[s4N&Ssf;v$ E)blwoZ9?ƪZ; 5U` B7S@ PqÔ%yueιC@BP.;sҾU#>R]qM C@][tӵE"59DǠ* Q7̀0QQe pelE \=.#*8krdXre:"~gsDd6<̺[' «[_G5gƪl.7m;.Zֶsǎ_kBTRꗷlx㵹lKWTwWtvn5k"YJàT7zͳf, NfP!D]w]etӨ1BB-k֭ʛ-'O˥yZ1j)_3O=;rp;J{w~w_~'XxE9a]G6nܘH)巴4?Oڹmۖ-!kXTwz&F&zzzK~mms;|CF, J5 wv{*XML}rhgRexuɽϾ;f׵Ng٤Bfw~pSvp>:=ՙ?{v*Ϝ9hhdo -JRɰ r`T?5m@,ڱ,:W,||/)<;scGO'}7`C$ʨ,J,2h4ӤQSC^nYu=x̞;IxsOsgy}[( ̽=yRܳΞ՚/hUt?A$yק0W =$ިEvK Ck׮miiMR+"JԀdqB$c I\:H$Q7_XP jx+QGBH(2`B  BdG[bH̷eBsbA4g, BwI)ǫ۬h0LhT0"&%zs "a+29'hP,obX$d sZo|WXǏ%}ydD8 A"]gvY:-[ʰ(ƓK |ɒWJH=~**r?zt8J |`pB8w?/=p\U?dNP(9RGdZB>=0j}smg׶2hx͸[ڸP(7<׷r$%"VZ#(=,%$*;2quw2~r$,(c" ,˖@ R2,Kf ✩PӶMHKڎeBX\X3νv۶wq̙h$RKR*˲OHPq qUTI5 L:h̲,x}}M#7qm۞W7Nkn>cuk˖-gZW  V[0ZɒwF{]^<-SQo$WU66ئŀ!i4Rv&{MdӸ_HK/wc^d[5}۞ŗ-/ij;y9 PX*@TQ!R#͙TѴi@4I3sbk 1 L;*g Y,PB*%ߘwQQ`LTێP@<#1G I0)Qܴ8)΁ d"\f(8OF)P@frY4x*f$<#bAoV~}oI {t@?pJdܹsuB:>oƽPGҸ%N^t }}}]疃e|#\.e*UTp! iZ/%'3B{l,N{ c}C"\C,!(،R)AJE(T9F"DܠrllL ABg0\3^ l̙c3 YKI3:f8J%L l8p`t&M qCG_ e E&JRJ@Q SBJtnkP)A ։\Y7i^.$T߮{aȉ&h3)lnmmmkke,z=$m(H|>_2='JIXqq/@0M3 UVVj2HKڲ,,v]/ᲲS89y*oiF"upzY(RH@Pp ERDHPPMo Θ!O!E &JC "QDuA>^;Ջ0Uń<"VP%EwF :i5QQD"P )(yXK(,T#Q yO.!T(j"hjyo}xEU$ϖ%NsIAv:J\|Żwm+Ϲ;wccɪIFcCꪪt]2ȁ wp:&a1LVR>޺gUg &;{l;v3'M=u4Dp~+N1:?zpcqQQiiR60K0+5r6Wa.4hhAOQr  B .-6ޓ&MkiU@B X,F)--)Ç TBמ-bEBPhȷ+@[wr^u%P%^|ʙ.78|L.P2+_ʮ-š!G-Z)Oz{>tvl&kP9{ 6(Gce\P dɟseQB*YK _q^nӲsp`GH4s!H$10r,\T*(%)aM{ Gp>#H_猶# 50Nb?Z$P# o*+L˦ٵ]{ޞk̙GPBJKfϞ _|ysy3W=vҋ^49|:$2VK/󹪊y- *1czƮ~zzz-\wޯ{]-3st=4LF{;.ZX7H*V^G6-X2ҟ~(;z/t笧m}` ) HBD)R VE)5m\] B @Ρ>F IDATM,0@mҤXdܹV*//#J׵kԛZn:w&/l" |zu;mn]}ʯ+ܲ"kk~j{ڏ]~Uɱt:[vW54T:~Sq?,9l9~M7n=lٲ;wi?5mmU022"K:CC-@%-/~t*O{6Ç81/X0;˙tf=lesR^XW ^{ņCG_?{;pqq/(@5q\h4ZTT92e7,߆!MBղںjT.;p?|1t(WBHl6 ١P1#s]GJMMM"A)d89sΞ=7 &:f2!eY՞d >}ڂys]A@BH$m{hh/~dOl޼0 *Bv" PI@C@ "p H)@|,2&*P8ET<ɀP%E)$D#CBYPJ).@<0Lr&r B(SJr=PhaJ}q?!H6Wlq5s-ʫrF8uʔיRH7\XEEYL3 @_*rʔ) . uu.pUQq !fXqEMB 'ipK.B8 u@Ss 2FP˂&וZƥQHNE Bf.]*K"dBX*[l'O^b|`>~?P@I^>455袋 9zkuvֆX.s]|̙Bp8 G"8s}?ѨiFFeYM7::>%,;eYw~W_Ѩ.o:-*Fa3v AQJ(J3e@,bmSjBi@(B:.'1Lf̰, %h&fY0Qö4(PJ4" DD)%UBiw]N##R3 =x팩ч^2%% Pj[).4:aIM&Mg>NT<`Q&? UAZ@P(H4P?*urh$[di\G\$KHL(\^a؄!mۦ3F{^JXlU3c*ev(-66mzqҥ\ %ps:q#ٽ箷bJ7^ۿ Nw0X3gNIIIKK˦M80Z]jJ)APTT.x>hoo_p lBeq1J BDG  k hs?z_\t1vPPH@ HD@9<`"r"487PnLX9췘yϫL4B2DQ $Q\'b0 JT(C D A$PG&LӉƮִ+ݖ=D̐24)sA*<40@q(dQ[[7-^3КG/X7f҉փɌ E#y_Bйe w~n:1xٗ]B{sͷX_ { =\Z?K^檗6p~q HtW~>UQ<6^dO:zgϙ7mzI-e':xRy嗯ƅ E ooop~m7ݘff̘we2rk[N۲өWۮi+MU( u۷%hh*-7ܴlבC/od#Ja4Fŋ&ܹs֬9h,=ҤCf"7 Bl[c떔 vLJyD⊒~oW)W8boRw.xY}xtO36# ERS L3G^O G$P $aߴZhrTJTDMyTJT"Yח\^Wc aR^ Hz!73!'7 ;rĆ WR鼓3M:<<(0 [(d$+ږJ%mD|d2_a޽{t?ڭ[F"cǎ]q-ŊùVOd"-;SZuZ+Z |6JR7tڶǧM/~NtL A)ӏ5۶ Ӽ[]ދ֬<u, @P#l:PݚObݺu>-GH|?i]M5(($j5ꮻnE.^OmZt'nG>}LJo9sq!R}{sԭLojJ++k#- 7z?lpe|J?t$1k@WUQ37mǟR!!HPR*]@RBC.AD3={kn HzӉx`o|x%x^4P~D$(  KD P.M<ڶ}"5P$Fqhn󜪊djŝvڶ_oOFeh5)7Deǿs4,+<88r!-[PF`YRR,(v[谤R*!'>Ғds{$cV=R*#?mڴ[M7tw{?Mqm17,fJ~YfimWVVΘ6]ܷo:4HgZ:&a񥔈#S:rm/}SToIYY5y?/Vz!|JH$+M&G2rK⥮-Ci}tmx3`[qQ0BQW7 _3<;?8v`l`< )#5c49."d@LNQy7Y@@hrBf%7=J0}eJ)RJQf8k*J3HŇ_9@>pߏ/ 'R UJI @yhS"!*i1l~JgĴJv?72V~*={>tmÌxYY 9F DIL(eQu{ ףS.YiW{;I*{waqAAB(N $\SVD 3Ar#RB< y7܉C{RVQa?6sy}c͇{ (!DIΝS9 ,+{z %2A)D `2J˧O}c h GYaqnn1zyހ %kjj|>#H42CHE S a 8u(y7 :ڱ}?~9?)7lp'>I%,(&XH$g fYsq$EH*S*ÇWVV[h2!mkB!;! ȉDnz鬎sd.XP0x;)L%Fe+V?vX*Bauv7Z~}Ψ /Z<:&z@ FV PHMrbzM-}PHE |FcGl.564zAI͔vzRuMUx2oq"Qу CA SKaC@`*ȨB¹|YLZGY AIQ)4 ]@E(1%D R"yPlgAm (Cc}H"~7C=fн5dnjB0 )/؝W8ڹ梙_?K.dU CkVF6iCg7g};}Ȥ^y] IqŚKIXjoW^M%@3g>S|=߼{55P.]=yv˦g7qǖ.[=cFzߧ'>SޜFhKyy1-+tۖWn\s}uA9Դym\g;Fg.534{t}cÔ'rm}vJ͉+4BZ nᆆk%()ey9J y0 s}P#0MS*{>Lp/yʔ)o~||X6v(T^^Y\\<442} !$]׭/m0% B7`Zd toC}F 2K)i3Nnz饭R%Iͳ̩&+˷nݱbŅ0gN7nx\$:v*HBRJ UT@ !I=J P BRqD!*|(Co !<#/b3YlXW@@  )y;Z[L&K Ji\4{Ȯd rR  hcNMbbT I/PwJ4QoJU(AR$bR( ;?PADuuB7ڗ͹^}eѬM<4&LUK$(e yٳM }[wpgǾ~?Ɋʓm\8y0U<>`ܥãO8J9yf\l{L)3ohH|^aڡtjxttu܁Hw7{lҍiZOkldVEEEɓ} F<9<쥌;rja̧ynrMl!ڦ榽{eBߺv#믿W>^8BJn |@M%hBAcSΝ;/L^>|xl<˅B!\0M|tte޼y6o_zϞ=k׮]tɖ-[F̙3}tF'q:ڂ$C/V64 \Hr{aHɤӯPύBaJ~Cnu!a4ZZPw4>znBQXbQ0KӦMkiir5! )-S7jgѲ\W L"K$$7CrP^m1Mstt;7U҂>4 Pxy R Џ388卍P(OId*2y֌^dYicaҪjB0/BA2bŊ#Glݶ+xoa̠1L&ϪJJ3L}}}x^4NO!m|**.z&}Qf_/R'hnnZrSvJzk/۹ckѫ6*򙾻?}]pË.hYrӧ52"~|Ѥ{.3&dيO<|f&h$5ZSU}M/P8;Rb_K{[>KYv:(IOPdʸc"Rqϙ{|˷~O]|5N=y_~L&mϗG+|`dǗ h&RkPJ}ߏE.[wkzj0 $ BH)087^D\'X%\Yx+N|7 #OYpY32R{={vgg뺕3fXfʕ+?,[oKR q>DLഹA>#TC4-@"V f2OuWHLܵs?}MStuwE?O 6WrI z8h+/?$`㔩6\sufKϼ+ywʕ'NSJr swAu]wu=ܱc^xԋݺ)d2^U|\Q3!K`uqƄ4@ ̧5Y,*  ^q܈Ɠuea/d$".,Nڥ˰,BD)AS` q/pP Êe@:J#I^_3hJ{ r_R8:iO>}ܞI}K(555555BBj~Mc54KG_b,9D*Y=C_y׶Ӛf~m3V$j7.Y'8rͷ_<0499'kmm+Ou_bNghw(܋?W<̦B@2~q*iʥ=82rj'e=C\vY3KZkݝ S£c}u;,0 v˦MWLݖ7kμ|˂U_U L9=5tC @p$%4Iϙ7JA:K|8a( &'ST˸i K"%Jjcjllny) G'RTNh%qxFQ *P@P0tin,] ٳK'kj|qI}Kp;~)k?ٳSԯ~+4= C#][6K<5F~ fH8yaʴH$Lvb13AӇ+{ ٶ,b{nDY*<窍ϼ t[o5%[W l2KW6e/=8svuOqaD^?rhʴ@A*\2ke'v1INzK/ͤN~W-בͤ+۶mmY4YusN]g-:hιT&jnTj҉PJ(4 JƱlX97(ӭi$}`:5kh?p8L&tֲ`l6/O~nW]uUSSӄ-fs??M:-͎(8N,+v@k(6MPJAӅ,A =tG+ ŌCc༦z,?5Ϯm[֓mB$MCJ߻m˖Ƞ.}*$Μ~L&?᜷yBɄ~]]Ӥ{mkkJ۷ASϤ3Tb Ob4 CYdpm⋌1 @&4[:zB@RQsŌ9a./=YIYcãcy^ eS]0~ E(-pD">epP@P/>DfYJ%RR*ATU,XV \렇u[W\ys?L RQjPDaE-8&t|4 *oċ[pniT Ωa8xq4rBJj13,R&0'XD_ hV8VFPtK*fV!R4-$3L溽۶xEdtm@#a;P6upsn!d pJxJqB tKgty.{Lf QwQbBQ%HlŶDZ8f{VNNJɏؖlXH5N $z`v?60"˖E HL̷FBFBVCqico,ڔW)"#!I\"CBU2P)E6bggi`nji1QOABS ep`ȣIVEɫJW H@eeeLMNM"h)} (RJAk%SD۵+b_,)׊T9,_'B 2@(f=ZΈ/u}T*J`6F8Z{PhB"/raڶWW$bW$j$*<8)̣(`%N U6aaBY4TEmD$ %&_ CQ8*Ee ֡Ol0tl.^AQ`',(Sehd-b]Q !4% *4(%h!ҠߺnfY#뺍06~hEQDd0l̃kBXf#$Ifuw5=ԠpR*0t̛ beK/RB(J5y2F aDD[v"8,A28rZiҠ-> %Zl3O H`J# 0&]~s:Eh˫t8a#1뇯*ڭ+p*;pspY.pՔ0oN[e:Hq)QDJ}v|{K_j7tc:޼񺦦m۶6WƵ{'?ǟi˖Mv_ g )4!++N^fltS3oi)VlnǼ;kMHW۶HHK[K:HU656T-[qmՕu]c{y̯rlwvs]ME+0>/ʠ0c-s;Zjk*+RѺ40r "aƭ7n[ᆍkjk]77ؖzME:U]4S(?pS]m6_sL55$cD hPJ#j"5j-Mt~bo>smܕ96-ضmYc;d2J݃HQOW_}hѢ2efJid_M",$8&7>~-"n {M*JSSY ߚNaQg5su4(wnKK*Ui.Pب(@D"D& Ty6S,4K?G~Ȯ5P!" m͍ 6ƬvFA$H@;ח+YМ0 "3niG*nwME~WlY&HDk DkطoG>a)-rz@}OU7lz|]{+7m=yrx<|兽d{ 3z`")!e R*ƨB(,뭼ʦ5رyꩧfe@=(Qv7,ݼu%%սM79vokevv"* ^o|qD|oooa+yQ0wdK E+@T|rRJ L**^*W5RJIp_ZQjkl/\oLU _Qvbv(W VW|ABoʫYhoܸ1?kBqO?zRL(PWrs>nwؕ#GCd23PJPDjmE,w-Xm?O,Ǧ.-]Cvy'.\p[z_җlB hj@/oy#'N~/|諾OBW/ܼ}H@Xȟ}ՁƼbvpxtуssO=8GmǕ.\z:H{ӝ2d[j?}{s;rlll{o &5 hʕ SV _h˹uS"ORLy8BG}<i沓S*fǞ{z'"" 2}t_|E-d-^j_n"SQ_9Ud|p{XZWfde. e\{ǁ(!@F1B\;7tSD<P(R Ԃ3T7uϐ],w/(] Ӳt]0\Bf23x<2˨krqߝwޙڦ ! "|߯ò~I-3gӛ`1xH$|+ԮP=Sz,RZ0 |2,˾kFBŞХ+WPPJ۷lٲ zVUQ/&8 3EĬ@TFX j\߿W, ;yv#x~IATJҿ<$&( 8J;vH´kkkhjjkG.207UcG"3U  2Euvl4 Tj30PjM1~WdlY7q B(x-,1u[ךnǾ*j;q9Xr?nH?qꔖT%ȅN,D^P8H"?tk/s҈9 B¶eNkΞ 嚵ΜJVUմqW5 'dњ#'N1\n'c1+ED+0\h-m[H݀KMYX ^*D "%|m͕T19 VKwn81^\>V]~`~փ+MPX2Y#thXu\9V|sų/&Ӹdɖg^xUD˒ W^\3wHEht9xaٲ7m8z+{*:Ԅڞ?oWon]r;S1V4QЉ?twSjTe[Uí1\yo=0>#(X2irBP66| ]B+ԌA`ʲ,$C۶x2HFQ`D@ N)=KbEDRr{ֲqۼœkʥ8BH@Z#$b4sf4 %'j%4b7NtBRZq,B!M!@DTA @kMlZRj߉SA2=ɄoRY (jpF7ab$~ޥQQFTd_|L@(@3@JQ' @!R %}I[dcwo֒M|aYφiOk@g׼Bx~ /`ݶ[݇?/< ns̶c<g'&n=swcb*U֮]T(-Z|ٗ_{{oN84gldvްZmn6lƿ;oasoBo`VjX}.\x+W/AXYY:-9N]sV^ty?[ܼFfQ_\3OMMMdmOwߩ=ㇻ))!3^ދE̤tF(~BF pc3rF]Mץ[eMMMUWWO[c+m|ީ&ڶ-uT! v-m[)?e2bضYeRx1Hb)?TMKT"',Ή$VJIj< B`8U SBA 0h! P DU'hP]q"3E SBA*5%)RmF06cm "jo_tghjSkys{^]#:H(gKp!GbB("=v !vvc,gԀ<'$N0?̈́MP'4UJvf1#D?  #J#JAQfёiDhl_tcX,3984ti(c)9clp(L*v}j 9(e/#'geYZ B8/y-v࠴i_JRfVbr,[<̙^'#gI?,|ߧF8sE^eh@`fJJ2ɹ<"43C\o30K( ᾟ=ZQQaɲ%ZD iQJ)Ϸ9`[e mT %P'&+y֕$)E@ +&BYđ5*.mWD,$F0H*KB<E hd/w`:oqNeeI$%+N;0\\rkb"4,@#9/"֬#Gg-~jڠhdHt3Q_ӲbVoLTU_ퟚ*,]qN {/Zں|re5Mlp]sKؒmO=͛rW:gEywqEm~3}Nd\{مs{/^$D1VT;BR}/m@q[R*@\nժW~d' X)ŖmwJKa&=Oմ- 6^z̲hKd28K{r=bmټvnEѩgl'[8ЃR\7onvrT*lݺۿu]=3 \\J%Ӹa&!f?L& AC5.Ƙ^Q(' /,քrug2D7.aŒR*麮²-fD 5p颁I)=30 q ¹s>ii6AL|+GaT^ȕ%L/8q ʸ-A-hHYb%ЖC&(`Nb)U(ҎPIjKVMduIHF0RDkDwUlͽIyx1; x9DI% ((3A4JpfjT9t{{4ֶ !xe*e17mI, h,j&Ε7Lx4;Z~c V~d~ S HI,]lͫ^xC=9ɯ^b[>}hw?ZVuG[:W_yʕdw˨unxq r lZk)TXhllB/.^mLTm1X0A/uww^`~ IDAT`-WU57]xu/28؟J;z9ՕɩR'׬YW*~̀sU ׻7m9Z]r`3d.eρ=z^=۶cH $c,_ۭ.vXs&&&/JTיK\gggՑeKOM{g8m16AsI~˼-84 `t\4F zL0\L&SO544444![P+&!Ki#BFoeJ !'BPJ ꚪ( 0 KyD&Tqq5֠./\SSSR0,>??}skiilf;v9x㗥%mΝmiiYhQ*]jQ% WpȥHT/F i:FIn*:qIt;55?0n˱$/^~)$:YaYTKEƈ}x׹@#~׊cdV\MYݔ8p`¢G+a"U(16<4sx<<˞;w8h^_JA@z;lT^ȕ:.AVJ!1>6Tұm%ucSݻ1o'"J*C|>AY8Msʁ4CCCFJ%Ҕte7qh(tQJ啼b,<cxj dy!DV REET>_}}ÖBp! S"J!)%J+C8E4&\߱{͚5Gihhܹs?񏏌#)nHSF-/LD!Oyp"5#Ky8?8H!Na[j?F̵yDr"J|N6?Uop*4X~ pVhb<}!P4Nf fQy5PS[\o[tb7Rφms cS :V\IY*'īl]S<{?۟=ٲYj嵋G>j@$5$Jk*I@ U \ˢ H ը,"+@R$҈T TH"E2B0{ن LO#b5%(OPDpIgAKOJS@ ܲ)#%s ؖE#4?9f[H˹!RF{ۚ甦K: F!Ci_̇*ɍXh J !s:CT6#AP@bAjEd\8b xI}s4 P)RB|hYT(՝PZqtTi?e\khB @U +,p\($˄Q32O.JfR} Զm#VmOH)!TNt\5wn;!PV@X-y! ʄT2bHbxYG(*%PLOQ bQJaYV,]W+C*J*٢׼]7oRw}w޺u"H5Taw:q+Q³N!ɐ%n./ œV)DODb6K\ E<)RAl_C6 $ҤP!TN%4jMu:cdIUuL`d! ϟ?['~<\./Vc<,8e+#ynòc=}GKL % SRxRWj-*T(%J)qeYG~bϹ 'O|=>?83~wt:'+*&`=٩I}[ׯPQYƩSxն^ڻu/iDfu%т-G/^槟yMWfusSぃCW8=3,ȀyRN-q&zPx2re Cb-Lu hNcOxk}8R"^B!"JJU[˗:tp&3eMݝgN,BO>O811_b8xuG;gk*Jc ν@פj|5諩\Len,vێۋxTL#QzSS%/Љ>wㅩR)^1;JXNd{֕;W~P,XkԌA)?eEJM&R4-$!LU^i'1v^Equ VpBr:QEAZ݈FeMF$$e9}0YWB5eEK *ǬDkzBhEjfX/]чĥao)BbeW?qҞsvR#g$]+lؓv7^<\iOC&_Lr{8u1FFy{gR3Ne͵]{"鏖vE 3^k/f>h}mW׭^::Nj.uش\4PhF~[!4,Q5pΑ*T@"M& HJiY&Wf3߾3\oذaŊ}}}{Ms#0@" .ikHs_wի^x% u2;ɥHe˜98K:zϼqX,ٌ=rd…/@SgjGdͷ|_w|CM՞׮n]C`,Z鳯o+KAuyNmz3?;2_oWwwuy7vˍz S 7l{`MM* vD;XwP hA뤐TI,)Bp~L-%5` eF=|iŪ@v|tdj*ْb$Ml#>hMPw;$/J5T|i #؁W^<&JI@!..SMzz:56.jb=&fH{a ~q;mv]) U"Sq] hju ~W"t&lop5RnT*d+VITǩ:|…9X*fa3ݲe߸A{{C=dY1BO>|چv+Pƪ-ˮrJL1x%qbZuVZJ/>{ѢE]]]nܴŸ^p!DhѢ0ߏcBa%sίsvӉKzdžhLD,FJ)s|*]5Bm?Ph.`(JƓ^*P*HYIYZF=g +.kSܴSa& mah4~L7:61כmINk\pd2K.u'ElvlvlT0Kf% AO1qk,w*a2lg+ YVX0 }G[f,u,[xlQ08Ĕ6b3]g;,[ke"*rRu]|l~a$*Ko&p9[,D922zӧ[ `VƼαY^K`uȀ'GSQ*eS8e )JlXP! H4+-3-I͙"М4{o"p$@=?ПBu"Y2\KX..y> H=ЮAL:I"ICV 8]!H .T}˥G20HqiB Q\ @j䄯D`ʒ Xwηm0Pva5@0Rf0Ȇɛ d%0E\q8MGQ~4_L_<5׮ek@ !|5K& ?::zޘx%1Ҏ˴"LMuXJjd12IёՊ. Csc~-ӮϏQF HI'EQz!}/a\qCd b %`bh̤ żQFŹ+㒀4" bq;`}+i R**,&r\th~qoqlaRD CC N9h60[kƘ!RR`\b<Pus,Ԣ8.VSX&$Yᑽ0قg xʸe*cEQH$,nfEKV*d;-"\Z :u7ߌ0q9(P(4c9 sa>/ 7nBtvvN2l=/yn|@ +&nP*.V#^Fp(g&cNP@G\2K=xlR'7x$}>z]X# {KOeu #63#@(T8]ߋ%"&4'Baki(0+/Ъ 2$HbHyYEu*4É bϙrC*dfWG.\y#Ri9=n`(.>˫.w xSիs?nՋ ]W'b3gZ0o [^Xpٜ8=1Icp՚՝MS##혁lnOl暫ʑ3†ÿ()nWg_pYf̜͛CHrfogh@̹l&) (U붼lRGaDMotpԦ)Y|Y.wlkF;f\r,4sV\1_݆كRs+.<7P TUx%(r.q 9s}OPQV:,"f|`J!%f(J* Y*YϹ #!6Ki7Қyk q,H$7DQh)ݖa!5c ,mh)ZK23Ɣ(c4};tKoӪD7UN\.Yȑ#֭ `|kx"#HeOP9cjT5,-51 IEIX*sBE؈Gehh`KxN8exMX^X5isQ1+=  $D: 0RHhP#c S(ɁDo 3 q  8pBD R)hr\gcsbbj 5#L%d@@FN@@0@@܃y.؁"_E0TQ@8܅ 20##%G;&28ppP(RRm;] o clB %ɴÃW546>х)r?)#Цw3{3űR>Z0~빱MOzes)wڏ9/i&/Kd'_u5-goaCgg_ooתU1SLٱck7?r+Ir4 IDAThm(/QL}}}>}ɓ7n<~xCC5 Cp8KF3 E*Ž 2g#P,rReWU\cθ)?nbl9dEp}U6F]QLm0"Cl|34 f@Fo/Ɍ`gjL'ɪ$ ܐK)Kq s`p87/)m'4@r\&DtkøE;/-ϤhlT#cQ @pb.r<tE|םЅC###.GBrTnik_d rdDy!(mqE>e t ^?2G^ݫ7m]rǁ3YpZ%w`hT'=\2*U soww]LGGG=eȌ1CCCcc--͏> $Aꚦ}{߆R7'޼u6 NϤ\̱Vo/w*sjd\Ne9vt/Z0 UգVB _Nu9APRXF/ƊAW;>SA aLy⬗za)tG].꒐^䎹fOQ&t%ѐk`*Aխwr8?x^Vb.%-@"#40%(B^tuu*O% t~20FtlrcE&~G/ T9uwߕ!B@dD PMZFDB81>~ح]?d9psAv Q##R]=|̵{^rd(9vҴs BMmͮ]F\fY݆I5WNZ=g\7|I_}ږ35ew)eWO&֩&7#lot~cdԽm\ ` &.d3(Dm1E08Z766 .a j˖-[pawwݻ+>JPRd5CQ)q8 ƒqH;;>cn{{Ggs{Ϸcn b cqf'M.p/(Gc[Iӡ'ںHBo?gdd̙3ϝ;ǙPZ57730cgav"tE{6Ȉ)5xM-- W6i&Ao ~H8.7O>pt=tHrƎ= g yz ~a߅ +"7cƒ%K"BimEͫ%Q^J/kjj|kJR(8tp$IQBf;kIgƚkkk ? QrG/m(LvwJ 吡I -ʧ:xe `pp0JٙSazW]yssɓ'x \GP2 "G4'(2I _ G g<πg0žW ʌȌR1 t ު~Վ \y\Z81HG@+{ݱm{멓 G>DTd m-肟$%rj /WQW~KkuG6\qɒ׬~Tܵ᚛O\mBe/_U.9n(gNxBjXhAC$%@ܴz&-ŝ0TZCZvx9H]BP],]c"T,K.ל7E1GeeT"Ƅ1DZuSu-EkK@@fP!iBt\fdr```pp 9\hmO 6[=44422"lw\Ắyﻮ=dju2]ǩdo}"d>ePԮ,/zɞ%|87~z_>W)0Hܐc(:k(!b84 ֌G%0Bdċ.IhW JBX yM}M6鲫 b9;6UNWe$刀Bd34=;踄 2!H鄟X4sAl5ta(]#DM Y"h[HEmBAhxMT~!.8LQF""жmlYr;n0T?C__z }KEu}'k?=pXgv68Kԧ>O_cU4r3Ƙ[-,Wee5'h͛}s_s-UmwyGgoW&]-sY3zj}W_ܙN86cҷ[niiͦ^ycڵ]f-w@v~QZy%KJif_sWZ|)MO>8N3kJnpxK/ xH$JagQ8qL։D c%WJ&P__HKKɓ2k9<0 :<6:jC6}\ۧLr髮ZM&ѕ" ©mщ'TK,q]!.뺥5%2TUUy{c[UС?/U8{`7c}>OdA)xW惏=̏0 FkD0_ND9:q5j(5ӱ܈IIS̹/S־#GP$qÄy+yv ZuIփ\Y!iH\U4a aX!z(K|eMzRUՆV.aN}CK.O@tI[o~jkq$~nNӝO=,%'޾бlZ]bO(3'W_]| B|eє~cuY04 YMS4 \~iWGqHA3Eha߹(j$`ȅ@DsO8w\N 'D2$Ǔ ,Ro  d*f)Ytŏ V.C4:pt"`dt82VdS͛U.+n (Ụ%hǟgTXLRT[)xpI&3YM0Zq8\8\x+G~q 8 .g҄ FjC,$QW<\,prQs|Sl<5%4sy9tJbbiK0Fs\+8Xs5w%] 3TAG`dDpdL+͵#(P KHx"ŋ0Ɛj.c`N vs,q'UMc ?GFWV2 G~K2Q[3/nE$?*x(4v%XJ-Z8W?=]b˫^cM]Ȑ׮>~G?NYxVZ>eƕ.:e֊˧5UmfΚfeSRnܸq>}g7|CΗ_~u]ـD444qsƃ:z[[l߾5 |9yCf͞ۇ͟csgݻ 3FǎQ*F@|9{&}}}-Z`R;{,̺U̙͝&_(m92PA=4}B1GZSuuyG;Rgphl<|\+USl[[\i=>zl4!$"x٦uW45.t<+<{-?tR1Waڥ=U^1\zE{kaI.CD"ZaQݎL"D "i1¨0eL1`֍]iheOBf=.)it4eN )Y S,0pb5G cZ݁L]X,ǣ0d=UR͕?sͷqvS?hܺ{mi]q+V\1wڡ o?cIsϿx_/n#Ҏnᆲ<7~#y'>'s_f7ܰ(ߵvuqÇsAǎƉcmBa@ĢF(G_k!u=o~3͖宮.!  [oLww!%umIA@aW\ڹَ6改ˏÎ ]#TϾҋ_}>3xvDU>gɥ##mH*Q~㍽+L>䓚gS;}oǢ<1bm  _<,mq=sLWWكe!œܽof[;TgZ?' Ɓw0dRR[*wx*<0A+JZke\Bz^D2ȘyEn "Z{ gl%S(C}BPJR֮]y^ƻ++hq(TXTJq<.G;"8l 74}z$c$A+i>}wA& .jQwӹ}onٰjvK\l˟}/|Ⱥ >|=Vf:On=seϚ2J4\LE}85M?wm[")gLoΦRb~Dɞst&㎎Λn4}ԩ孱OA]yo{箹X$ޞ3f%s}} ;;^|M7Μ99di%~+P sF$@ Dh8'Ή1`Z@$Αs@Y @&fnN>91pdQe0;"!)0|(N$ө !rH3ɰƍq۽_>9Dҁ5 L I+,fd2+Wʒ[1ݩ^]D'AXuu#"?! @  0 m2%X*RVcL&cV( 8ZR4PWed2-[6̅f{2DW~+.p\_jhh8o4(m.b}ovt'>Ck^m*a]}ŋW\basfϫ4?/>[r~-\>~W|3d~G,ct Xu 7v?Ɂ|}u9}őDCBeܘ B3G2C6^әdT6-$nz?V%ko 1>$8?#ΊF ˬ6`#18_㵽C].KoeZeG:uԧ_~ipHd3S?n;qQX#˦ 'Њ5bgiNHp| sXޱGT1`Z w;ɯ@&(f͜yӆk~_ruc4[6t2}vryg[E ;_⒥5qϫ۳]\;͟ou׬ڵzJD;fM;^~ox2GuK~׋֯Pu_}uo7Z1VIҫ/~i212Vx;@؄>T^ yN(dUUU{!DPȤք4HX &Z}߇ #1VT"Ӊs61F-h98dSS*+TF5#:s8޵kSxj5M?c.""1cF"H$ e GYO?O?W/~Âl x) 4_OǼ‡SE~n6': IDAT}Ivjժ}Rb0d6lgLuMϜz{JT͛a.I:AQZ۫C@ yApwgrJWepU#p;8&@OQDjt`<"B8h0\xPUUQs P"?$6p!NЁklt,,2BqʕphȋpǎMS hkfo?[>>_x]׭9~y٪Iuc#[l>~Tp35SfɇLMS92gѼZ_Tڳ3X޲mˬ l{~~p8wE?UW\_T8|'dڝ:u7:\pF(_G?ڎb2=EM%"lcE[*R`Li&2_m9FW~dA`MOlB $o+Bbyg=IiE>v#H&SxBBf+3raIB vO(eYuS0"yg;3leAY2H$vqM7GiDP*xD29!dC?r?aG~] ?.fޝ۶yNgo78' RŒ8Y? Uܱc+/{`xm:u <.Rwthp#8@$9aKq82#AH(4Hc@ W,䒙څ˲TCC}&M8*)Fc'ZchƁHiqSB ɉLl&Dz.jN' EƐ E\(8'È "b)EqGD0v1F31"$А)IR'2TRK>ӞbJM0%X *& b#p T9,/jٛ;6\ߘٲպڎ\˨ ȔgeHȌQ\ɑ*8p4԰'*^ro7n\PkG*yt3fto,G4[rtzAnc֬Ygugk'}횱] l.gN5{ީ8(Mjv65CUkV^sǁȆf͞RE:k]uԉBXpO "11RK&ÄSl4hMpVs2 CC?8x^$Jey8GL҈ 6ZpaFraqdҞFK;X(8 y2,J{8J[>8B6乮8AfFGGcA`C2^[C" `QGz}J^~GDFG '%EDw 'E'Hm< R FT^!Nqg@"`јE0Aó%2H$I/G5C 5'ԌuqQ{sϑ3ogPcysLW"$Cl|H2SZ;f@fs iM\4#h8`S 0SXE ~eM" cw!8HQկo~_ӟ}[g>bsc}p۷G|~/% [/|#oo/Www˄DTC#涳'¨$Dz2F0\0 ʀa%2޾s#@Zs 26U EftOqӛ H Ώ9rqPJGUg;N+E?}dUQkǞ ,叝8 xkQ*͹0V۷@n4;vlawmKŖv$,>tC '.0Z޽{틘'|^j&9FtNyDKXkO8.iQZgf D2zz0U__kw}-[n-!Ҝ}~{₂Fk!-%3LfH*PF#c47dhd9 !q R2E9\2X,: yF8sVSID\+#/ `F"m%.-d 2ƑR!CbwK<1 82BTKB9'9TN:%JŢ5 դʑt_Ed# (RdC,XEQkkٳpǎ 뮮$XYulj !H]G+eYXB<Ǐ[8)ZV2att\.L&S*0T( }(J qn筟yA V{>1=qℵB(U&1J5 [zŤJ2TfJ`ktj6k&U#9H&ub n uđ )--NLXaBsWQU9\lαGN1 :`إ]Jb1g14q9 ׌!p+=0,w& #@& c\0}f"?72<醍B;5Ѫ5Ĥ^r\-癗R_j䰺 9.~G;wǎݻ{=h,6l12,LΘ9ixdA Gh糳g̜y-[v~.Uw߰uT,ڕ쩪rk[AclΜَ_aNTWwF#nԳa5O744lشԙM L wsoGGqYhccùsg[sΝ3lƱCN=;_~UM]d vZg ۧuMORt4ׯ[xϮoܹߑ;/BH%˥fӦNh&uřN;?|<3RB@Dvtm[cddWSQq I,v/u Ssz/!Co D D_WT|^x0X,&Jk_{`Vk[n!l^{_R5WZU*-]F΅AũT88yf˭YOHx[[8MMTSssU"V5 yvRP3Sb+Wk}}֬Y[flhUK֤b-Pț8L:L^7~'<Ç5!uߊŇ/;ԇ3O&%0+K%)躮.&I3FUܗ\隢05u] s. R` {r&[")A Jp"aTweuA,wQ *4+!Po\|СC'DZǵ?ǟZp))I)B@u>&2"ґ@ 0%mDTT#$*+v4={c=rn}ێۃ% \* 2P B<陚/_dU:csP",xM)ZqDJr)dƹM)S`U\ 0\_ե;~Q4I6( )*qMD *__i{C/?eKԹBQ%98jJ (bIQS ֔2 Sp!`/LߦmhYU[ZZa& 7ք5DIiH$Gl6sޞ~Aŋ`R1 CJeNUUUmFC] Ӥd+=ӎ;nj&{k L[\>}hm[HDJd%B8q⍎ iX6u/'A,A UT<'>=u|3G^F0szGw͘&hރ~9e  sK29'nqGKjI3[aVݴisQAA#SAQᜱx 9"BTT 0 NcHYg@(1B)%CPH"2 +DcFN6)"9Vź2/?*>ˆة?l}q_Zbm7* v=>CFb˭͉t,=<. c;ٖ[?=YQ]GG,.fϞ޻N='1 g7БXu"Yjn"1ЙPxmGw~XsS#}'K^C,s(q&'K5\vq\DJ+3f@_qZDE{os E0S3 p.?Ř9W3m~8smӉD )CPLJAc1l=Ź*|PK.t.n a i8l#PPdI)PiF㾮JԙJ0J  R@ɟׯ5sNkK_d!\DD&b͈*E8bH]ss):%HMERT}AqX)DC4&3aQ4DZU!T9b 444]\.:"T4CJ 9灅ϴI7$D\/ )BA*!xl <rG]%@H LPdL |SSR"HNBn@WQ.dI I9]^Idd@4(%Dsܹlj`uβmLnwVcfikӈR9{G+;ٓkNZtExD l:}趂7!_r:. ^5k[ p@0)xҜ(?lܰ8ުxD͊k_4ov]'DU[ݞNO;w_iڃ<62<'wzkm۠`td22j|쑏_8Gʒū2ّvCk묹s]{[nd'Zf57mF~?3Mn>`F"3gukoN O'Zgjnh\`G>x}{زVJo%=rmnX9DK$At*36kr9T*\.766wާz'җORw]>򑏬Y&J?~<$ TP,+&:l//#`uݧz3t,+J%NvA8yh!d.mf._vmPҌPf p.+ rjg @Qs%x##) EE'Kq+)J$*O\} L@@)!@lY!*d dµ8RD1C\U4@P!*I%EmMD}irR0쉛gXlWt!Gž>BaY&R>s1-?WՖK|绽 [RG~,c4\GlO};߹5x̮ի2ng;u|E7W;f-h[3'o1[~ОL.?zJV-l E_ȌϦϿcp(suGŷph&FU^+nם/ +Mt8vꍋϿ[Gi5}߰q݅ vtt̙B)- 3AsYnHDL$Duݐ1BzjR)eYSSSb`^lܹs뫫gG, Rۧzg/_^]] M 3V0x@K xҞ1b %l4Fmmm`!tb5IexmZSS;0ǃҒNRP(d`.lJ\5)TV[D)T>?a˂g~埄 neH\Ju k_~Ο뙁3Tњ9JOu3NtBO=ws_a}G{vx}p^{>% ?}G&Z#1﯌o)34Tµ(A@U<)sY>pݦf/\z{!RN'Sqh%@EhUq_5eOvtT4 Hi];_b4͊QBݳKa*2%):w[!T\h`4 o}W2l:.\x!OI8Id{$ѧ~Z"deI#ogZg^5;|2?|tCTzWAh˓iۈ{|yj2@ ؈966tؙUz[VXkt:::K޾\xiwϵkٳA1m_BdVWuMf|_6^m飯= .g-uu קߗV]w`qKWoܱc{:3T_pykB=Tڛg7zܴz{!q ׽K,3qcw޺qӧן;SOmWv~v7xS{4I%It'XL)%8眄a!D>f bUU]wݵp†㵵Lfbbرcjjjjii R f3w@pǙ?}_utc,G"0cmێ@zYDx**HP@Ӵhf`jgF8Z`"}+cr&U~W$|tMoxĥN@ @!dmC?tz_ׂO<5<)φ??W߰aU!- 缃__f#/?:}D`A4/J;;#q}т~摾QǶN<}1=麤?ѱo-aUMݷrhϫJÏ|Ůν/IٽΝAcɀ@?tVDx1KӨt"HR"pϢ(b$ G(!T @s "A_0KRsŨ| 8ݗ6A" "uW(B(CݝUP)4JADDP\7ow>F ׵3[ŷOk(z+ @Q)%HQ(:>a=quB@)r^%17XFI~e'H gƦ wzErRR?pȶ+fzbh"3΅vvuut\lԩS(/VO@`k:WTȕ5ڵKJiKPBT KxrK/ @>308}}}t:Κ={YKKccܹbH"1qT*SJϟ?oYVвo˗[~Pi2"h 0 hi3]!1j:*e fr8h4( #hXxÁT6(KBaf[&lԔ?.YuF/k?$( ŷI@g~,GSZU7D<:18&m;^y'ECdAI_(DT}KmoΓn~w=CC5),"`fE#G2YrŪcǎ%SɦT=;ttZ-YpWg"C28(8߼)l}Qq"B1"ܷ +P )Qv JBJb!KR4T0ZD$ HPA"PD@:V @RT*R / 2A86#T([pQW(} h\_R@p4V<@;\ ZVӌ||%R<ѣ  =tCp"dzX (q2` )t*R ƈLӅRJ@BpCB%)PTpG@q@J ei?G P@ TzҊr="<ψ[rn| 0J8BJfja 馎(t˜|\T6(ն3;.~G`AL߱wwŞ)sf/ =zhƺ/12I R}:{̞0Bc +d\$ |?*Owt`e[Hl8Hיr .c`@)E("UJF5PcRt]B6 @&''})m MMebTJ&7o^`"4 L)MO4t8N&)866vRihpp֭ -x:Z D@lllt\P,bqښa 34M;s 466:ܢ&A HQJ``RX$Rx~WW0|uuuQU]æfBI(%^PR a3t$P~7:81T?{k6W wS?ᣖ]4 ]J+%=g` r>v mX0WN?O,Q2Y=}0WpȣG{9ӴߞA__ڞvB ;!%ȍ ljܣ ~(@{xU\:f3˘sNr "< MJ*PB   D =9J@pU ȔJQ GcHI@R1@QF|/t bmJ D(N!q$0\1J)))jWJI˾3ѿ'EH&5 YrP\p.Y*$(=QD(B*cseT* p}G(" +% yɧ _15R@o;42BM)}DR)`)7ׂfH("k軚b>vŒ>*ȎM}ӟ>ֹ!CrV__TW_WvE_Uc㧖_/}fThB(Bɡb0d[ܰi}$M/]Ƶkyo}sOV|Ƈwu[61VvUyGFFxzʶS]?Hix9^}U7_g?nw@W4r( |Rh m[DTCMMMBa"4MBT*vc}t:=>>><BauT1 Ig:mFWN:* Αѻ)K߁APJ8B )lǺJo/)B{P&*墒RI$xR.WS]]y~Ŷ\%5E L&Upf"yR$3T >*WJ&BF4g<U\ e[X _|xl⍵rIY-s/^:{mtv͛w#˖OOMFcgȂKR H\Xb|b\XMMM.X~]'d"U}EsSÙr묶3ZtH6LEՖlbH5O0Be ޺$&:.mxȁT]rhZxo~Ogom#95_WI3o'ߩR(涶!="6JDc񣧸=7֦s}';]HY_SJ-5/1Sc)jk[rBi*5]P߻P($"2Jј466767 xZXf132s JppвG?g[UUUUUU\4T.A@!j* 44RQF(ed22M3H%D4LӅB!`UP$3Jt.Am]"4 nv4崤`ДRkjjΝHħrdf==Rq]PAR ȹ4=@f&@DI')'B:'.UTzfO*H.L@"Jr4Br1N{s0t$8 omq0 A!tG8%+TRQyo/-_GOyju︛T|{k6( Ezuk׮N 6.m lٺ{Cl*dskrRmm˧&G_Z{ɢ'^j{g0{-Y|]w;vl޼yJx`eKLC*\!f?B5sK/fղP&JbIM7f77/h[ ;Zj+ 5YmcDHR+ERʦm۶]9zuT7x6_0iGjimI$jjH*Mݎm|m|ۼO$Z[[nƺeW56\11WE#D$ʖl{ӦUcÞN NR}wsm]]HR X͘u۷ }xlu^E3D63<<<)eFœr 뙜T*diFT \&-&1CݝǎL#W*Y%X;pxit0ʌi)%0<2:?8:/t]֣*_ꛘ\JqBxlk1M1&3C=}RN `R"!!/nx%Kf%K#ҀybMǬu䉾 l;/Xеm۶mذܹs#8wǵ)}qJR.I!rSSr\I`J׵p$LYhQ6u]wժ[-#S&B ӑhLJhܦ *  4jjl2p8ֹ MB(`%T T2%|T S񶷽m͚5CP>J)R .|H IJB-& VH5ݬ8 P 1d(@( כ.n $"e|VXbyy\\zg^{n]5(7ǿ$4o٭mOGg^G?W[o ~6.>'J(dMY-W\r#跿9sSv_|曟T*O2u5ī{xTX4C;xbD9!K3ԯg ! a٩B:v]/ Q#_we`(>(R6;VH%E,-Bz5=zy|J) /5*>$QfЈL [p+89Wy=QM*.(z}!.ҥ\ju-|%B>hguh+Z#Cb樂PFJ2,MtP}HXId _u2 = \>˹hYV(d2ܱcc$3 IST/_L&=ؼys.K!i:#r 8eTpeWNJQwJHm;ضH B*,g*+@JJ4cј8BJr@!;` Ca뺍MMەR=]SLTF1JbT(_oo8{e+aJuLkl=$! IDAT EO \]vuhs)'gNrp ch}ݗ5N_ٷÏ\ T*x(]A 6˟9*uYKxXg^{Mg ]{mS/zB!>֭[׷o߾h<?hښځ=SygT/_]U],Vݿlӓ#UU]7x݉GV\q񸯢&G;֭£(AQPo:$hn~õLg4JTR1DK pGOL L.sH)%Py\. C3 aJR6ԟ2iTusW"DJqrqǏ1$3')jѢG:4S2Rirr2V Ls\uݵk&I)PTz'9244F9z`BH$"R(E}OB\b/d8TSBCcie R Yɓ+dd'v-uoذ^(K,ill7N^!MTJ]۟e^Hh@BZ#'>;6 i&Oqo>q ?㗆.ٙlkd\)Yt#◾uW-X7d2~m[>dv$Tٻ녍 )®^*LL؎s19 gϭYӫoN?\Ȍ}ƈRut2Ɛ(T϶oE(`GӴv#g#8WNF"<b[OQcPB 3;{՞yG GTJ]>W(HRCWQHΥR\)aێH6߾ٟ]\fk|Y#wq{8BTJ|ʀLRDH7 c:H! H hB MDȘR ?Wez(P 𐥔d(r<۞}>WSOt,Yn셞ys[,g XxR3}kĩמ}}w}OFbXd4ZN;{N}=_U TSO~2開y9rh\kUkVtfP6,Q2Z@S_ӝ?z{?wJ_x^y Gmwy؜t]~ϋoHJ{@RBJTqmP,<϶ٳ[KB\0kٶJ 6666B s&tzppPhR y"a۶rjAȩeYQ@F)3t0 JxܬcX]¦3tJ́|w}t]X2&:x'=swӦ`;A$A")%Q%˲"|elj+ߛ8Y7ql'r,đؒLu{/`L=m~?0(YےÙ3۞yrQ ,u=ڶ[5gpÖ1 ߵ)PJg϶%L&u]3u+H]P'W-m" JW.bg?-T_<VQQh(:!3nѴd 4(00_ b@ \؞m{!1W ~k ?dR 򕮗^z9P[Rs྇v%K 9:a6 +)XP}BxM,w2"( s7mY8oɮ]OYonɜ8sܱSgv܉3'=/gZ׬Md;zƑPHKOg:._._8ЁW/$aJ(QTe@Kbe[9]RRyddOOWc\_l?VHBgON>W\ͦc>uxn2 D"a;N2浴XB(mG֭[w7_Riz>r##y;$ZXPJQNuquKKKSTE>[nv6 KMUUUjϺukēXq:\6 JR@7>v$aD"p]wll,}ifyeN#9A5{}: TyX}T@)CDeG ĵ]ݴ|38*thT(۸f_Y^.`@<$cLIB}>90)2ҵќ^"PA 1R^W" LxPM ~SdTo!@M* (V NtLKҗXٺ|˯xbifxpfF6ͩ=ו}˺mܶ'+-sE,6Cݜ5%2g+UY:J&Sec#͋\t}R]S3OQ+>x` 7ί5P ;=]'ܱKVR訦vX3)6eC}W/^s*Xq˛L:9EzJnO/PQ]ӧvBTWUnSoe⌇5uѱh$R\\\VVaR*7mOL&ɩh4:>6:>1(VT[byssȈypRJ9 ;PBPPu=UJyE"a]7 ##e2.aĊ ^`pf#u[nܹspx*9JNRSIױөd/p΋Eoiq-kB\t˗2LPbU+䙎 q3\%U;;zM;:v̩#D7QJeE}gNung q˲%>qs PnRQi}c{,Yqz|PrTQ"(ѧ-vZLގsY1x@WXyFǏMg25֤u8pqn]G{{wyw|o|d7)E=R^^޺z/>z7m9$Ҳdɒ]hʕ˷߾sl綾^Jr9 VG,RwC߼ydlaΜںW޼zD3~cqFv.^@)E z̒c=$P_PD0d5/>̀)ߙ\ǿqv]wޱB[{_ 4FC\^8eMK}UqBUku^(/cM;u_@ ZjQ/VsCFWTWoX^ؙnHÜ%+V;ߟߴ` Lȑhd mye(hqI1M7;LXOefچܹwyBG֭_repTÉ-bFn̳}~x# F4O٫[[rAg{PJh埧n!c~29I'''Ѣ042dl<h߹z?pR* SJ }030/hh@]\\lf,}6/*Y!s BMR8# ,S60}{ҥKcccmy!D"J, N¡R`YV*t] !P j儒?W3@\Y|ŎI=vfΞ_aNoZ5>cx_hK='?vh]WǗkZWLMV._}ѣw0аx^x1s=/.n?~lbGEE>7p sRI`f"p~M>> s=8~> PI "5W(JY<4V>;n7CUKV7qd󦭟_súշ߹S]FMeՃQTV?ܼ[?x>e|G^Dٶu[my7jLyk Z;wz׎ס,]\jYCɼUEws{zpkhfNC}V.3~ѢQ%Y)HzXeifaF3K  &AGPNwqةc'瞿Ц*űυJ/Y < o~2>05NH$Fb+<220GCe6zKqKפ‰TF({(tiF69LSSI?OWU7ܰu癀a&elF ]eƊX];opB:;A 7eơ`SUӪM-CJڮexqwͪPhVIqX{PBPf{5̘@_WO@WO_gwсѡDbtrb<>0ɧS?7>6~Cz;<~_Q*A t]///YH<Р$V+*-SL= uKUDBaCuk2kӯL# }#PP12H( "kD:=LN$PP%Fa縆mJLx~>smS6!WtG~FLC=7_7އO;,]w+˞yi)K/ʱ͕7?ɨV5c'"-7lyj<@^W-vBE^௝_ΖsT+.M{٘cj0+z]=]]#>qÆ TS;{f4 ϋGϴN&&T/Jf)ve> IDAT!˖=VNJc5us֮[ޖ-[ϟ?_FF}I_!D8tH/Фbl\DrtT*ŋ9"/E㿂zSlL1aI޹<@!%xؑ^&W8p}4q+W(J_ җ}b2iDk||xkd7RD)4Jb޾Knnvvݳ WDž+;_"'rCо{T*EQBڅk:@J ˈ<8=/ҁʿ_4B:8]H%Ӣ;\J)2QJ ݊D !E}ˤT*55ǏtɉɉOQL ^.)AfV3nrp>Xh4O:p/7hwttU}ᢢ@󼉉 +ԓ0@su 9|  O-Se3{ؙ=}#>{o/_|?9sٵ+zeETʵsVmIo_Or23o+Q#{G4g,MM9իהe3XiWYq̫sbEM=;&D xM>GyСCqzXZg2F դ=eF7xt8TO:sŇs<1kt1b>2'`rM<&e-nnJwpcN]G]{R'R.V5keeyc#kLdSv*ӴԫjC gKJ Ӻp1.B(||_j__СCgߐOny˳>i)2ʥT8UV4ISVȔp(ry #LKqN` w _r 0ÃY. %QBiSUcAsHT奓)Nu%D8RY'gs` OL)J!Hנbx[)F=ΜѱAu! ъ޾1F!lBn*S"iXUUU}:빎ںRGiy\pA,59)R$>})`y يLP@ S0ttUeAmjB Oovo6wTUM2}sG>+q xGwޭiU+DvL lms㘦^,w] 19m.2#e(Ĥbhzpd!u* qNyb[AӅ㇙ak*AdyNulGGۨ#X1Uɬ:ER9G(E8)Wع'gDޱ3t.0?2 s")%x29%s1@JSer7MQOf9~.fd)uͤ##BD|Qkb2>O`0}wR]]}79"6…/_9)Koغ!9ڱe-?GLè.--IOfhڵ!˨m4߱ogqӶ[nNma[|Xq֭Iq˭,/%nٲ5M\.dpu[R)+T7ݴ#uT5f~?ݺje]Mz[mݱ41(-?422VYU6lXOܹs捇d+V._v¦+[;2261;vSSةqct4P`~4Vbсousbb{BUVVTUX ($.M(=ߔ@pϩKSwO}'>wxM[sxb_x1W_n֮[|MPm|/+n.5XI5<WZ58xx{#_jMM]uu͹s}]8ab2˧ E,w/_}΋˗l޼ OVnveV =ЃNOUT;S@߸Eanۍ~k_x{E-Kϟ۱u\t_| ]UWU׶oԩuuKW8 7qnMҟԧWTUo꾾^x)IukN}55=]\nhx1eSwܱ3L~ʼnDP(5(|<ۙӼi _ei5[ HQz4""}ITD$"ARD<+){iN4$UwK} BUAC$*F: <"$ \߶ BWƘkJ߆XG5p-v*'56g &#ς"YT4eMPtM n@L~Ő  c$/)C\6$͉.!ku iRμikW?uڡMwZu/!"%W'aÂ$jk~=hg=_ٶ{J644ι75T0"]% lhW[[wI7 vts^(7p, +׸8o7~{x?YV8PI@xV(t #ccc 5#󶓷)7<כ۴`ruS j":ׁ2UAJTa!K")T#HKNW7JJEB`H%/M,J"vR(QPQ"h "q&i O>TpE(CN(*@$#|&be eGƨ B9(E)%>AJacBk;3mg.]ϙq3gε,jF%6[s'1igs"yңGΎ&&mٲlqrgNДʌn6=U__=HwONfٴxLYY{cC555xttts-LOLĊ"ϜM>72w۷/9O%##cE==ãڻ[,:u/]ꘜt]<؛/ܾ^4FFF'&&&&/_z]m۱ҁ>F压#'N,]lll,99>L[[.g3@<L$TZ!0NKs 2o=w3sqT@@V\\0qbBiGT`z:XCxNsjSWT,{||ddd$?'SJ4Ns٩ORJyHJ7JOfxMrkVoԷdyc񋯼x0^޷wBԐE%A'50n_s+)r$}\gUYRLne^؇?g=5mtӑC/;u;HG=ܴ}{Mu领Ou7TnQKˑ}/ kTGR> ?kJGRO#8xd_g[~ս 8%H(A j?RJHpAs>>>KKKlgpJ%''Uյz~ G_D8qأX}}}CC]>o{~}y{hЛXaCYi&%D7(L6[u2=YXodj\ػխkV:N+𚒚rfK5fy}=K*Z1d|k CJA=#J BUC'Of{;;ZEgG?2;w,2^+)(wؖn#sSO_zpևpI,qb|8?ɡ< W~+E BmgO8T N^yX2-+5sMm3;nw{>POww1L;=&1tshYy?|{oۼjmS_~S zinƒ_ 1JMwJŲv_}U+Cc"3_]ޢ,?>p pem9{WuN眙魽!eb~pcuuFx՛k˸y+";FWJԟJ>B(# (cO߻AJfuNQDʧ>~"^N+{>5"\U"EFK_| sL{\zRK1XDew:3{~q=cLAM2JJZ:t"FbFCO(Tc))GBFә:{ɩ]<}RH!}opp``{MEE=MLL[\?|>uut+ݽ/bCm==ښӧ#6m=*EX1U`@5@F@B4!\E-},5_FBxESQN@@g.p AIZ(Z"H #a-y()L* ůU 9ֿ)bYJFQS/!?_Ɠo}a \7TFz/%^fwrדa<6ju۱cG~Ѽo}uzQMה$&}Γk_}L)366B)}$Q! QzE,0!a!2$a S4 SM Z B:ؚ!,kpP fA(HLC"Z: s g@:bfHM(9 }?`$+lPt\J)fLk_`g)KпPGSJDZ,KM_<5~kBP$RP )kBiP`O;e PU0 #1 xa3XYAJ)Dy*]>@u)%"zi6nxMLj>\\_Vpfgg v &RAW@ $WL2bF($i%8CH2E}% SLIQSDc=D@$ D" 2BP  (&GBPy-6. Sfܪω3igLf||LJ]aWȼlK^g_=rxd4J;9H}ȸ^2'c#vNh(@!R(Rܼ  #I) _*)Q@T|^y$JPHZG Q;%HQ(kz@T`TMbJhVDJR R%QL@(\x gt*TP@WM2haeN^3ȶWEjJ)| 5)_as$)H&tÌ:h:"]WBR* EB`"Ψ@JJjUy{ $~wWWoj9zW\ַBo>ynȱ ֟|'}gEiG5ۼOݿfyiW>:|^~ҕ;XOǿ5ޢBx\l۲VS>hq{wblɋp䖛OL:;vE,dα%Ku75,ڰ_:i])[XK!Cy{hl[mk׮OyCl޼rw}~㉘W-^S74ϞG/[Wn~8wmٲýeeP֖OxˍiZ8V٧yy| gNذG֬|>u\():vyӦ-/mSܾt g?j҈7}nݷ)p7',Rqiv\D呚PE6ؑ{*ȓO>96:V*h5(p[W^'Nٶek֬G )4>::}v)Վ[nxƒyQ T=vm_loB!,))]beYP؂MK/,+dYc{gv=3<pk6^7lim--KO8~wrU_{+űv^57NJi*oRgguMoo+(Z,uE! 5GV[˴(%Y!D{_BVB2z<\#0 %mm_7:Xoqs7K|{ʅ"BF/[lphZ)d A31ʉ H=de]t"OQ9RӔ'(>#T L#3`RAQD DQ ؔx%z^^)ə DJPJ%D1>s%AIA%賀ߨRUUU7q\׽2 H$H, )(QXŖdIVo.rIq'K/I^M-YJJXDJH{-1 jز|,s=Wm*·{gc&S%ga_z9Ռ#Ajer?o,]822<>A-GOvnEU+WuxnPɠf=㹰#2k4\V7ߐhmm]jMRu{w@K20d#\w㘲~‘uduZڻEZ}dՍWsΎѱ")d&Mͩ8o2t[{PYY9wN}Rʾ…v)-I5751ι8aαQlzbg<+/^ZJXIiq`EE.)3_>`Iq@WWx֯[0w k3=]#ݽ}6?1Q+72,~`c4?5ͪNXߗSg6mj*t%W5,70<߳r媖#ͮJ96PUI+\Gf--ga[K&Jױ6>nXhq"hm-ZxK/=|Ѐ%zӗkokCKB1:2𒐇i"mcڬ sڗ=sB$X==@Fj.wʍMplً7̹mcA  \}H$C$ K kҕ83*Ȁ5*jgRVЁ`1 xzdd?`Dzۂd`Hb/os'+-%}}hN \]rྎd ̲3[Nްrcch-!4`2ũ85utD߳-Gt FLk3PB{%"EIۑ& aetV{azq cܑ^>Fŵcg0#,b$FWV23L1nm@2 7s"5Yp]d5-h1Cs0*T$ 5ù: Gq\c :6s]aI0&X\b+] Y"΍]L5bFPK\0U~ 0DN6*ߐc^rN>{R&D“w2x 9:RANbd9ҐQBIJKCXZ҄\HX 2&(T,Iļą8J]g820VK"B߉9J)"mIDzBJWץ]b>O%B80S/ct܂!;﯌!WYF>Yx! a.eɓRAbVlg;=BI#-^͚HpXL$eNWd\V2om(#WIz+RE ah ΚroDo}!e8wkOwPE}O^hXT\s[w|iw-zn}Jjs"\MOOnuTC-Zgyю7%}צu'Ԯ뮹jgUz=٬Yv?g]U-*l|?g}).ing!?ĦFH3?$4La$˅J:a[(fYl\# r" )~:.b )Tk݀"j6ɤ \j .`q(qE2>cY,z)3JI)2~֑, b"7A#(?hC?`jc5A`#c$E: 4#$  Zhgah,Z`bCy 1`&6B;!+ɷʳ"mCh2V+a> %-AP*2^krkl!#B `&T& |% rB=1P$H+ GM$[@H`U^NUehb H6 ='XƘ40"`ǧ pTx(='=hÉYq0)LucùLi5‹&< U:cAX!WזHY4 ~FFBRXsGq8W@Q#EiHؙO% .rS<R]xZm2y%E#Ecqb}pmC\1$ƅLX_1jX$YZU>#7>Y\>u=+xm-%˥5^s=w|ع_!j䇐{m8-[ZϞ=f~Myy g/r䐪^ze369Q5OyŒo7T6;Ms%Xٰbª U>U䎏G%95a둞E 'Z^xKNg'3Ool|ߙxE~c$kuK[rzdJ@5.\bμKjwo1>6:z{{9x"DlhX6::On8CGږX잟võWg/~:J`,C[a)yȍ}B4@ ,țJ TxqIJh" M"r8A"@J(XA$7D$HֱH0r$N1@N(YA!aPrМ(TbKq5B-M@ gLj. d7~Jq6|ߟi'5d]Ʉ1V,T &\Y%pC ~8XMʉ 5H Ua\x>-2=tyEI|q݉{zW._պm>+uI'ԵLċgWLǮ!}Ws׮^[Y]v؄婼|7_kU[D1%)#]w]ˋ?q:8[6o9r9s~m6J [ظ|KM[n @))XFPH@e IC%+ajcA>#P1b9#̀0 +f&'`bd "Ν$XPw Er C- 0ȒAňd0*Y]/#7ž,ԖV3`G)" Qux^UQ  Kc4AS4L(j1& 1XҮ#c` 0|q'q yËChtvtE<眱6T{G" C9~< ;wA`) VS["Pw{4EVai=Y`޼yB"UWPkwoM7o~.]"KL&ٺeKI:GjjjZt #7|{lZfq`tmm}.89>tXubZ3oc\ n2ZWYՕlzǎgK.?/~ V=iCVV^j޽XlӦMcu>!a_BO-ȋ _'^j h F6EE"YdTPDh#P r$Ihō @$`d ׅMT@jZfEbĨPݤBiJ4#C8J1CDSiB7vE YjUi`5܄kW}K_nu]!S ViXlт/?j,<:Ao[6׿ٓ֒_?4<:'SS߽sdtM)#܊u~_z}k9yk?_~_z+fi?ڴ驮'Ќ+RoW3yWȝ"6D`ֲHjg6J7_9-Ol\jՓ}u Dauwo,n-[ Xe+  3BH[Lb,Gi|EǛMG"Gƈ @#c4=iё]:V׾vlzHS Q ֢obaF$GdGD$(Bu]&DgLfH-$ahJF: KAH݀\u2f CJZGu"< 8Q$clNl#¿H/{{晝;s y??qO|ˮ]^wEV->nW^\`w48SAo ]Zԁo+QM/G{qّ#眵_ʖGGnzݪ?ֿӿ޷o˪yξC/y잟:Zy:ˋ|~Mgvdo@眹e듷z;Crߌ  /;ui;[Po>9vjT;_W>{jG!O:|&Lsd@ (kV4,GoΖrNMYKщGڒ A0Ĥd`%$kȋ˃sBsJDmF@@ 1)l YDғ6n*NNiO.dTɦ׹ѳA0ƔRiw `Z U땢cwlo页~h'8:UwgQY"(X0.c&S , i "B* >mGLմ=}Kl@l-=/N-G?}rS ݝ;"l9<~a"wVia IDATqq< %v87хoow|D鞦}ik{9%6>QɄH%bƍku7 `-33`ႧwDzD}Cq&suu]-g-=}jr"0{|08c2+1wd8SVY5*oJB9~"zyͳZy!1:baA1r\$ 0x9!Pӳ:Q֧Z뢢L&KNljdsDB"Az@^3]^ L`_nhM lX0 0rRNNOANCu~Zs&;1 |_τ b,/swb- voFxD_>!Y  =ځd>IƌyCϛ@ /B2߷'(_IddYFaQ x`ߞ~ⓜ=t%A~ڌ3]Yk6>מ uZ[}!BFWCN]𳛬,3AC@Dz:^:Pb24\q~;9%H*؇NY4!5 >??x7.ZH2 Du}/ 1`ETʋ'?tؼ/.N}W0BTJYJJ˿?'>!9oC%ùaKJ?tˇ?-28GDqƕR-9]g10N;ě1:vv8FRS^ .zudEh&a.Ls!YH@$H NH+A0[5HHt4ID J 10iW8!QQ:qԸsל['i'n8c/笘EࢅOP?]~hpO:%-O=+Zd7iG+ko8U"%#:|.1EĦM`^V H- ďfmL"̂2aAZ"D+BtQ\YM 9h(Zǭ͏p8J'"RmP,G$D 19Q  @I- 9 k<@[v} t܈ 4\(RN:qLGˑ0S<7a?@ы_ }CooXOwO.ۿ 7鲥OZWQQB"Z;6::fe|ҥKc^vi{{fK> qFT^^ xf_|q?866^[[;3F#՝qƙi9BOOUN=K_2; _{-qƸ=#&n":%*OGW*b0jE&QHQ=Wȣ3գ)BFiKN_P0 g'o[.|~fG> . _ϮZpi|>^wOXQd.MפB#2PHVTaXdMWX/GoU(TJEF ϔ,gُJ0'7<~=P*ew{Jc(&ƞ߹w i{6>''sM{w.{ĥgooms>km3E>xg{3: ve?Ɨv9s xݝãJXS ̪޸pn!}{<{ծ{g{Z_|] qƁk0 ߶=o)^K>ox#/7?̩^|`3eH|]ʓϘˆf=@EںЁq9nrO=8zS}sL'ο=ysϹw>;;On97d_icPG!F1"B}ݒ+~L{em8mu˯ 懛v-q+DQ%y-xs]_vCi"q(<Ƥ#~}1nο&bgXXQA{~db0q-"A΀B+fׯ{3:G5"+ D`/$ tu~_0Ƿ/[s}Pzrxd'?Cu0D9~fb1H;N"*ҹ}+Wr-3He39e&f8f[ ,L/Omۺ%K[ڂҒ w}_?:gJq7sօ_q58 '&&׭<':t0DvFD\+>fjmґgn8?1a :%x%RcOf$}oJxTYmI N!cPD: $b 0JEdfMG4V &Q @3%2ɤ8rPa'9gG6aqD:P:}6ഗ*KYeͩ)H^8e9Ef=[9-*C׼W}Lvi=҇":ۛ/dx{K+9ܑ ɏ}S=kVw^r)C#7=󵍏<ߟ8mBp,Z583[hHldxlp ,Yb幋["ue-}k 2LQYo !<RZ BqH@@@688ʧ8RȬ%]Аs8^R S]6W)|͍y~>ВRugf;:I&XjBd|8 uv'Q% OpI7+W7o;y>X-/^„ }vډU ;ž]|d%yӤ %Rҁy OxE:Zmmż8Ę#To!ZJ!Ιw uDO&2@$c1Y&gRqᡶӆq)Z&Օi4U*̀tL*2V1jk`k@PV1 cN-L3gErg&KB2RR,cY2#F")Z)J[d4VWHμ0;N~ښ'x4z޺W̓ދ?{Ή؉1 gLza۷m߱>xTSO̤/έXyw?8hZ.TYswwiz]cvG @0ͥbMo"d>}03FZYG&,嘈`aq`_?;ȋ/~gϩN^ڳj>opphphbʕAsaܵc/֖G tu$W<1jFK KWܹ-<3j:R vU Y_9*Mk?9(bىn&-[ \GOf7oȔ twuQ򙘉F/x[[0'&TFA$1%J.(M95%ra^`,HѲqN7y2-XUV}k֎ Y8 0a!_mgf;zvjgO+kc"+v~ҋ>ve\xA~я*=6 aޜT~-^w27,[U`nUbd*볶&| \ #+߽{מUZƽ`oT21*Xhbnu\)̈I?4G&ꁧ`wM܉ik~Mwιb`l|b\[L[: U<-5mm|(rx٧7̭hDz.f:;'FFX67Kϟ_E14>r'Og4e|.\ H~tx00̮:4s.4oj g_DC5_y'B|\~饏g,-9apƍ?|dgM=};bVUUUcc#pl.*r{:ӂʑN 5+򲟍 b,<ס cZQ I Vĵ}ʍMzNf׮?G~ĦҲC: T:8xhe}0w4XЁ*պu]wOv깧r㶞ak#GjվJ!Ԃ+Vg'/:Ճ g &LH+]02i%E?ȶg4<G܁[JVvvOYE2Bp\iǖ٤ڑo9ŨǭAԉx<14>odhcOѻ^ڽfW΀sJsӑG|]睳e볣=+׮z5Y|Kϟ`w-Zz]GFFF榶u'{##Xvt=rXHY;E*5IrB$DDh S!Bq\v bi8A@,c%K̂DD.&^p|lzc2Ӗ9%L51f2YaEKT2 pqwޡZfWc)D$<'rccI&PgKØț l#0$ϓ-B#@ B !!."$I`5)>DH` %D8FĀ6!)HlLoR־u3/4fisOMMDf$'1bm"rQEZӮ "1BAxxZ Zk k8BZ/ʁ@rJWs/m$O|:&+%fҙ~&t{1Btäp+47V Y H;a)7ؓ>@(DPD]@.h ! LiBeѵAH"@d2 4y]>_\BiB,"!01@EzJP TЯZEb 1 $H1!5GH)EC,G804 ,p "4\_a !Wm^cs lAׯ`Z9@Wi:9^֏{zGjiT27Eb IX,Ęa!@ BEڂ^b@ڷL^8[`TXdk& >agP$Vƀ%ejͣR5Y3H0& s( ,")#˗yE$`4L"H1@Df ̀蝑ȦAٮgY!5|&fR_" #I!\)|BΈ!F hC!D=$ McLpH<@X"0xk!!+$6HdQB-C}º}gd{ÓM;,YF>r ~[ߪY{ /c篺_矟wi?h[V^/|;DW-g\Fu\M%hrHTQk^vLG7E!h?cu8?tHmf9[3%EEmm|e%}tXQ^~W3,+lk;wn"5kVccVEêeC?׻-/}꯾%K懲okq*Yw^LK$3?K/iʧ,M{+QGiS]G:dUsI IDAT! lIúu ~8s7?~Uͽ놛?O~|l{֦œ#-kV贔r޵@ sn+.{ef t`@H[0zO?tKԽtcZ8Q:=N?u$%f\D\/.T)Z{ @ξĜz9k-=q?5oo?\sjhhXpџ}׬..Nvuq%]~e6mZϚ5kܹhiYs͟7w0))*r)eܷ',_p5/Z4h3;]X@;X41҃U(68-#[(]DlBiYԓ)hо< mpk絫ʃ|~,(MEFGwl;\m۶/H-Y}\s]/{X70<ڰ~nII9$qHKK6i8*ݵ;Wխ*T*#$$d@A@ ڍunkmkݨ`<ECD Hb!w&ȕ"VVV>6& xWveJ)4qT.x R-#8gMC4I Z#e Jzu\pκ~՟ׅ|7|f3z޶^XssK,~`o61AKp`>ԧD'23 3Pi2?S1eJSrkHdI 4Ө4JH8B"IB~?yDFÍ  bH$ 9yNP^N9c`F!\r& $D"s" B~֛@<` a QVa~~1fa0+4@-rlㄡX%*Ј&CԖ y9con^Ҟq Uu'q 'ζ+>Q7i|/~z 8sV*Y=wWϭߑ>/vo8SD+O_xƝwuy] @B>@k5+*%lY$]R^2鄰2#Jk(Xtw4X+Y1̗R\b0!CPJK&@0,"M>mș0 NL k,KwtECD9EPKOo-˲,а$kc\2A$hf oЛoyh'P1`F d+KCJ@?ák$zo>JNymxe]@Ww<{|+Ϙ?{] + |l`1iҤŋ?stSOmn,߼zm{uАG_9~Cw91|ODe.0Ts^>5PQ2<0*"-u)oh,$ ""px bH Fi%+8igEZk)_f>T![J=x0*_|lŹ~ꦏ$ +^~dZ+21wA8r<]W/#3yy@DĹ#eڂEuc&4?E!_( `}0ОM>=nذk!e1%F$# {lȊ%ISO @/kڮdt ^YN;w҂g~v _\۫g|cw79bT_oC܃9@25 cЊ#!0@&03 }?HKmZMDBmFO@q HC!ܻu ވ@$8ke=1dZ К11#Ep;V:QY8c`8kMh?[d5[T*Z[['O˖-ۼy]~;\.D aǾMSĸxɟ3CG8 >׳c'WXquG~@L뺏?x** {R* 3o6;_ z6@f1XM<8-pQ(iO$UWq=DF9nB?m1vzogpt3@oz~/Bp DidQ "@F h 8'DpsG BL# ` >0B'2Bx傣rAJuvvq˖-o>۶7nTJqc !^]^!T}J2z6|VtJ6m-0"R*H( BA+ T=9 B qOA)e'MjH獸g$G{⪥7:=+^<>w>opm[ htT0C!wM?VX984NQ_ no< p^|x_:̨ԇţIb(C-%bzkK8a~Of9g̐R Ji)IpCRāH@6SQ߅1,#i2L1LEbBkbZ{qv Drdi㡾XYԳtGb~.jJ<؝qV:C?:[)!iYv$=jd,¨0 CJ8}GmRX,C>a„T,7Zkι/QKs2G+*.8VB7 L)ej:~8?`PJ5g ]4i=):& B$r%)L1 Bu$hP.-8, IM 71#9 8peXr{LHOI|U GQ#+{RJ)"5I z>9"~lJu38Bziq<Pz2B ¡VBæiCaDH|ss5kbXWW<7kבRj'MT*|<BPyy8b51J)q\Ky}!Qg8G&ˁI0%jDk#2f+BňLc'{Ų%C(8^PLG ,!yf3<_ tPLŴP2tEԖ (I1y#MV!$d!ȡk2E:dXC-#d:q胟>"ҵ^2 k+BdRJ?J+5qr滇anܸq'c A9!SsDJTbpk{*bq.`R͜9jR2tl̑!03f!"O&>l[[[{{P Xx??q/!7a{##>">2:DAPrE4p̀}(!BOpx޻Ω:IՑ %JZ Ÿ>"J9W},{S.[,߿[o-[Ok\[@>{}.$Of!P}CQFRTHߋ=dnn|bP6e 9kZuUmkC uKșîSDڳ)%IS0ȚO \ YSU #30\'P:_*9-δ { *O+TSڛ4uN.SaN4m&c0tRʶKJ94#?c;RԒ%KƏoB:<Y3qhњ0"Bbh+=?aI\y n۶lʔI}IJ}Ik\kpX?sM|P(tuםviUUUa]{n(D=5Ms ݿX}oud&Lwn:n߾0oh}&ڼyW]۱+h>_ZvMsiSN]iSgT5L򾇟c<UVPE]/>۵K o\|]sc>kux'l5!L~jp8xpU66׮)Cc 4(RR1dJ+˴m|k_ڴipPXX߾=)]'n?Y#jMxq $"b*&Lڳoo(g23dyR2εRaU x\J9䓂F0946Hӣ%7oE577WWW[ .;ܲ,S`ٍ?έ]g["㚚ngˡog9ݵ GaL"y^B.ʆy>e< ,>gڬ5&>/ϙ}k_ݰ&wU2mIZk vl6{}.UVV۶X,>}Zccmۢe`Hxh*`$/g  5uu=5uutf(=΄izQ+R\rɎ;R}C$SCs϶맂kqGQ󼞞۶MMMK.bǏgr9qBЊ+n&5<֔`3e 'MXK`6œA֬].. 8x/ݻ2fhٳgǎC!8|>Jl:# tlwS־YF!B%9g=Õlj Hg`G /E'fj xC7]={vH)ےGt،̿uzhRJ)%5("PJ1]]G^].RT8L]R)ヱ(cֽͦ挳Θ;4^M3cX.UX}sx7OWۥҡC3Θiu@)]W+M~IqS="o:OzL*_ ) I>l~h@hvSSSOOϒ%K&N8009O D¶X,v=,^ɗEc37idda8ڐMgy !Xmڴj( xi``}aB`?HJ۶쩭^` qɤ:XVݳ* dy{``Kh;l&z=UՓjML=yz&3Ɉ?LgepO=.7tWH#t jl˥YRmcd +_L'LFv_]ƲuE%ȕO?,++WZQ0@)h%-CPy:s,tҴd2yO8pgI4MsvFJ)u+**ޑHH &BhЌ8o71}V#3|h|}(D"I&b{lܹSNq]1nݺL& uww>|xbq { V١/eF L / IDATKv) ~d[憏,"|7EB> DJ;!Z<8vDDבMdvڂ-Vb섮x\ެQ9QJ15h6WȔ?i똊|$4&R:ykBp)q;+l۞3gĉGÚ\.UWW?#󭭭3fxɓ=L&o~sΜ9~(! 0 #IO[ZXr",6iHM4ZX"㾇cdx0˶sV8GD1Zk sĆۤ4G:F\‘k4CFH!!|h cH8hO}}}eeeg}{zW>|wwӶo5uDr~JiҠIi 3,L GBғ_xŞ{~޹e&&=->.]o[l BZkljbRʗ^ziڴiA_uWTrbhFXo݌C<4`YV*=00{gwvv ͝;:tPKKK__ĉїWUc})"&`f~%mδu,YB`R*= rΙV dYq{~]D?v41"< MJp OZ!P# @Ƙ&0R(!S@Tض}rmO=y|MS䬙.9.fq3Y/켿>{͟m?niɺ_^{5WbB!o_vPچͳN>g͟P?Ͽp Y}ƚ5k.]dM_Jɕ+WZV`l3Q㌔VD@.;op8dY֫Z^^^,kkF+jgΜpBarķeQ`Z}ӪopkmФ!];vreYc}=A|8 2Q^K/-++*Hjԩ/BP8t萟;-ˊD"Ǐ:3D"rH$ RwSE$$B frt#0vEgCL }3 l&R]YWP$`VFDoSTXL&~y2-_|ѢE^{x'|2LVWW~qP4汍b2, =^}g;NK_B!^DuǞ5̈y|J~'6?_o۞2X;b瞝sq4让i>cJwyjhhC1ǼzZNXݕ!)DX:]V}l~tvNRٷzڞ]Bc6.]z#ՕG:::vڻsΩSKm۶rHGM}IC_0YQ9=1U{{[:RL潔V0}:k[_PWvzu}(ܼyk:[,;>|5uVz^_ o ¿}k.[p+V#z{ [lݺuK]CdmӧM_xشqSlھs1vԭ>v S}t:edr…CDY,Wϯ1q;^/ +5 J'Qq)3d|J떉-HSg?ëVf 5i@"Ƹ "Z~=c,L8ܶm[ccaTßf;n8D* .0aOR)L;ȐIs$道59 N\8q^y,qTer#ۿpW]tjɪq0,;s8"FRdv4{|I9|ҤI{˗/ollbguֹF}(~AK9d]T)JYU4\c8QRpg><41R3$+V *!L<vz`5(b;@3aY& Ci1֞ʧ5Cwnd\|ߞw]ыTwuiqDZ5#?g^*k*"? h+=A&ll.x\iڡ4xU4gDR0D>X͛`7od&Mo}/2yҌY3׬Z Ra>[0rYIoVr]W^9餓z{{}H$d+x*E"t:- *1` Q!*F+ ͟Ւ5 `ƒ9.]ł`kkɓ+8OsT!#BGɁ,_n='2 @LgR|@BQ6f. =T2H'Nі[RlYY`}}m&Le!Dss3uXl``@J]__/8plyHk-I'N\>78k֬3goʲ\#Gtuk҈^q'M‹/ZiM׮^}UUU>s=Dh~q_EI V l֟M~Tn=_8)vέ1sKrXpH@ W=m`(x nVB·ngX,yw%b۷[nYzuWW.QWWK/ٶ4JRgg=~mUUU/8fsC֡T29Fy-4p@YP4! 1D@ 'djDM*$) m=& Cyy<=F1-$묳֭[gYV8fRP(d۶:ht07xRo2AiJl6GK۹)Mө!%P߽ߣ\F0 ygthB==='|rIDڈnhhh޼y/aByKK޽{ɤrڵ---W]u_ic}!s|صK1= ` j?j 3PHz %GK@yLLМPj$"v1Gǵւ %#HMh$]"F@ 2Ԥ] V הC`0t jxP#2"B_瘹VDG\щ)J|> \圭Yw&۶}܏ b@86 smE#jYN?yP":CH{{{ggmۆa<%^r7~Sg~'ׯ_?S{{5P2I#1׉CWUUP(t뭷{$ٵkׄ } X,V[[[__?y|>O,D",ye1/n>Hc R(ݭ4j5xeƬ*#C;w8F)\fff^~e UZoZ/xŊ{UF `0ZZZjjj^y_^O o}[K =#8{ub?IuO᝝DJ:|`O> n҄T:gd0M:miQqUG%aEV0$xpD$4(×A"$VQk8N$'?`pT( ͚5yӦMl ZlY89f˗/}=)Dcšєrv&5U6$u^jH1f"0-Ξs&i,M.=ٮcz̍ˢ1PkN!Ens,`ӦMg?faDHkST(K|ĉ_}{{{*RJJ&͛7B1;އ܈1rXeoo/y^+?}E3g-9ofYE׏>:~q#?]<jLEb?~¹F]<5w}+sϟqޡ?coȆrɍ{;Y2= &7M;څC}o}_e\xÏJEg/^{ n?SL_gyf߾SLOm߱K+]s9$לq= yg5^ZCe˖-;|ԙHرiˌB >voIsg^xr+cF\YR ۶ǍgYVccǍ 51e) ~CFxtU]]]K"qcb1 Zo~5q% S FK.pXL$h/e 8tдi:::/_[ZZFYvD \*  IDAT~5BS@p:;kOAkfG0Ƞiޞ뺟gxkkc9m͛dɒu֝ xNrzl%XV~y&5t*vziUSN8P_+6umѸ7mƜdm3rl}2{p`W~U c(t3k}jd2940(tC`$"Ο??NK)9~Z@AAA,령LjP1S=;3q򥋢ASI׵ .XtOˋY錮i.p[{u/v[nI$>8a"ڶ}7.Z(Jٳdk׮R\BN=DE:oR&0Jynz sʋK|W7qn6ndF2]#h:;%R9!N4>"tTГI@s*~'u%3&fSI}$̥MH{W I@HKN=6cBרq53H8Oq PJ>i2t\+J* Cr66M/>Б#GR,//;^x!P(dv&!fΜYXXx0H d1=t{Ϡ)Mq.P*`\ q]I(2@zؕ#=䶴2Ǝ~zZVьVܱci .$㵵[lI$x'eg>588Foľ3{zz<3M3w@. t!)(`!hB,p$C)l8p=i@Xf\(" <0cɠI0VF \ R2.8;z̙3v}6$()d4ʎCG̀g.pB=ई2T$0$MFي\(;_ukkǟyT*`MӾo]T*u188x믿1z6߱cۻ9CLBCÔjR([ 3I!>8er& #B`BɄ(Jا 5hHrD1/F B?ir A`$/$:b84@']%w?_Ƴ@s4C"3BP #1VQQ1mڴ͛7Ou?n FߒL&:i&tuP(8X|o _qhZVAÑk:vCid qp 8=HINexWdYV~~ʕ+/_~Сh4#HGGG2曗/_QPPPQQ%b{衇^z\kN=и&< MlWjzy:¯W&46ȈCVc~պusilֶX,溮M_e2](ar}}0aB[[[4ue˖b M;y7޸o߾'xbܹh(//s]۶>h$s"N"ن1$UTQCdLj :pIoܱh 픐OH$a-KRpg?YD>600PWWGDW]uUQQc9tvvbg}'F\N=" RI1uj#'w"h6* IHJ݌ՑsA>CQN*i ޴ai1n!2 B۷o-Z9E =PXPSL=56ܳL3cf7TYYɐ~5©o Xp1_Ӝd~lk)@⣿'Nu]!ٳh4{9RŋO4ɲ@ 7ml.++;kL+55|̎UI̛n2Oڰi]"n:_i_W|A)3M۶{{{KJJMufJR|IӴ>u/nM0H TxK;1t؝n{߭SgȖ•+|_~m6m RǎջgϮ_K/\fMScs*zF8}0RT{{  WBnM[5}t.R󻻻 yyB2B=z_wɲ坝]RʼHq|۶Vȧݔjjo,OuZ<<wqǘ777'I&}}}555l6L< }^zΜ9 .bNёAni{zꌘ@ *B|X,v+&L`UUU).` ~{EE̸pCXϫM &0UUUUUUgˋE h4򊋋 ah^__occkyyy4 }|?p8\9qbGGG:B C@K7L6{%iB)ijVZZ,9۹x ~o4Q2:uyDbΝɓ'WWW><9͚9~(kN?;F@D%%%\rL u?ca0 CVtLwWG|x0cǏ/++aa  }I0GSL9qi/BEEE8nkk2e[ZZ10VXQRR8V)9CFR}4ngX~s ̚5 hGmn>"!mY]PPkKJKA2/q!`+R)۶m}ꩧH)ሔg}֧? >Hk֭[ynذa…xFH':C9G4fR8J$۶}{7-G8c^{+wSqrO=Ǖ?X[顱~Ecg>޽{9gk lذV,% ]SOo׫Ld2Ǐ+2&q+^}ՙ3甖F"QqRhtƌmWTT\:õv*--k/R?!׶9yX.e YM : ;HJX-|1 ͓QNF|5Eg44d>1v*kCCɓ'@]7_yv lljts'ic)ڶm7f%%%󶶶/|͈z10Lz}DH4 mV2L$3fx@dbXQQ444|.--M~&wms衑Yi!'[2t 3+!8d13 z,`Lf|//e>u/I`mmm۷)$Ǘj47?"O{|LRDOO/"!Y5V9sځ]]]gyfO9\czl&CC^wt&I ioiBJ9?*‘bqCM!=R1yjnH۩Eͣ&tu"RJJw1~LXȲ,L!"?[:=³ *4Gs|&7N;OǠVsWD$s6߸F8{X:z_o2dcg?Hq) wP 붅gӖ"삢sM2iHPJ #!b(\isI.&evƴ9-ǥxowKbI(Ȉ&070RBF5K,l& N3sih' R>hz!' =dz($nzoX+tfwD<skق]ٺcUWuCcѼd*A=󇇚'MWv'J&?hC 6oS:1Los٤ ]̯[u 77ݚ`X6[piᣩdxqqȾ>RY^.lhkkdbZt$\Uꫯ|9cWbmݺ妛nPQ==CCSϿypK5 #Wnv!!J XwwɓǏ-Cgθ䢥SL S7iӍ7\RZd׬1(((`HqIq T,ۿoUtٳ̞5{U'O4‚kV.)-umɒ%i(EojEix/hQ(?%%%lnsr#!'\=4 vV{h67B^LzgϜ6ŗ^>vC=p69r7}ĊO~Gڹcǁ:BΘz7psr y @)PBA۶C`2mfzʹS ̝y-}ꩧ1>יة5k>VwtժU-7S ׷mIgSTq먡zA ]*;̂bhYYvIK)W,Y$ ]"t&~NCCCe]{8csov<4l;kV&eדd&ZɌܽ{61AyKȈq Lh%rH&.qXߗvGCcS~~uM/TU -[~IYqQ{s7͋zۭH&≄Yˊ\[4 3q IDAT Ѹ8+1PҚQ=iӢPp=8*@ h43 bqI7͂lŚ{? 972`@D$H84FIO [ZRFJ[r=42%i$2@0Za@HSg,_bLB@:hH(H 8( F8j$I~9x?9_0 #.)B`HF㪧Lp4N\ Y>&Wܹr9%?hBTQ͏XA7dJ]  Rp"py1.c;2sƌ"hi.0I'[_0wշϞVqn=*-1i nruuYXN$#X[{K8X_;Q8LhѦeEPIgށL6/_O\}x|т-GNM<:`Ž@SQQZrww]炅Z7ѣG ;_Sfb˖-ӧO;wm6lhhh2Xt-i{6mڴh""zSx< )~UV~۷9<֗xS? i/B^y?/.Ƭ|H}HMv9mڴE!rSjNzlY{5LnʫV]Z׻ ׬y[n|'}c}g?ڛn{o~;-.*:MSeƚK&zKnOcg$螤c?ɺ>xGg[S[֟ćn~{Θ8~]}ϵʉ׮][ZXP[;wܹ=qa~Eu-˯ ^z%/\t/>sO|rܹw|͛69|1ϗZ&=Y^RTD"Jx> D‡~ǭu۶_|m۶qt*SZx4M𰏛vJhxx~Zg <5"rψ QϾwV^:c>fY {斦\~fϝAδD"{嗕榳`V,=qD}C#hoYYQyAY6eby$\40tbGcU׮]$uC hŜh4ګnX{ g"|./++[s5:ڻ]uѽ~zM1uɓ c۷vxU_~fWJ`"X(;d,i뺟Gq1FW?Ml#y0!"^D<0w j_mP[a~ɺu>|Xgg={9f*7ުٵGv57oZ?gf& v:y˗mm%;H ?Otr %vYS+['k?ػg@O/^k[Â@[~E}gYx{漪[z[vc'UW_tM'/_p.F$ğ\kzhFD},(yQP!!ܤ cGt#׷e({z #zt +/XϿp ] 1d[=Ա34by[⚆i༳jmouL:+q QG$|v:|7Ǿ* A#9܇sC^"R< gQtcEHYKOD096UN*?oa~A 86yg6"ZiZII c,ͺ#(%)&VMc̬( #(q4().b_:fsw&Ȅ!!iPcqvF/!ۏИhʍsWuP: hq/ZzM{^05WK,]T;w^X, =c7|s]]jYwoowyO?uBi Ͼj d΃V5}Ҿ'|ϭH~"7Gs#EԬ3Nu#kٛn}W,2#sCd XdO3P+eVڽɧ^bڵ7ϟؓ>Oʼn'ZP48<0=C䂚zgT4VvlKgK\ox뛏}6Qol$'Yի_F8ڞ\_{ź\3 *cFv+2kqKm߳m~ufή;tp@dᒋZ[x'_۰޶x<~'xbݺuͺЃTZ|5˯I2F2=kgNdqH 8J8 9 3`8ƹbMGp@FFA39\@@Ft. `P Afp pPc`p.8gi 3:@g` `aǓ}>u!C}GJ{JʡxMAɷ?"ȵ?@e>o1\m t߳ qv% K,ICCC'Oh<7 >ce& G2Skf^~yuwweG(F=툜QTRh,@χ& lH2x[ߋ#&=ȂX<:s]*6cT)\%O2)<{9J @!)@@bj|?G]u>~D nXvEn4Dk@rD2UOYslڐk?ϸSiQ*B+ucCC}Gd*!ƛK@WDvDܱc"J)g okm=ܬ rC8f@WJ$҃-[11z0(&rnџ߰ᅯdF}?jZН׾ֻF}=4R ,5[ ѩk&E~[o~勓.Xu_鍃\WgosJ?/+Z'gߥOSY{heT}pӿ , rwg뎷鍨 uF쬷+ھ9\(U]- sةh58l@~ߔ\FH'nS5=ykgK)@L;}?-;-E"B_җ9,xee%{ZX\5n]!' h``p)P8TUUEѲ2p|%nwL*4cm[Q]]L<Dkk{&DP~P׌ɓ';#_ ̓}-' JB͆SsdRǩdSIy~{ gCȸSbb~!O wA-YC'z~Jue?#y۾(}82To>qx_ט3'Ozq~?]4p.g0a򮮞'$9jc?Z)"f0c,I9z(޾ck4q};JKIfv&ꏙcVOk#R_8#aol[*I':2yV>v gd?{`Wuާ>sWI2 @1 1q\g'!sǕ7i$$,!HH:4M{83(엀< rܳZ[}ΉK~`](YYޮs|Sf(3+lnM󹀆р2kK2yG}jYEUEgrʊ ;bwrYuʢ)~mOyoV|r~~mۺ_Q|IUjf^zEmwuW("C IDAT={ysOGz),))+ƿѱeM1N_r y>zpMۺoml겲[~ںTػ}˱{"J vl.?^s*RQVzcH ojhxuKHTc[6o~~sEeins`} wt"Ѣ‚7o|G'tA@ӃmiluTSwg[kGoWw};?vc;tb_:GDδ34Ig۶L&v;N :Nl2 Lg#:$F,dҶmy;dRދcIDZMHgReP*4,`WotN6ɤPA3fڵܴ~+P(u\zҥKc(7nliiÄ{Qm'ݫpp.W@1[jTf?/񫢂ܼoo~V_oC?~)/ʶu'JgFdH"WoZtZթ=sps']wۧ{໾>ttgկ汧TM W~s9K<iӦE566>pqE?vկ~5 "bOOYW0pGh 0.n7i.I Hr@ԥ@Y/@@ęID r 8.*ҕRSu⊫J$`#Sa46fmΈ%X‘ 3lΜ ;nlݓ&9())RZo~{}ݷm6[! Cޖ*#=bեC@~$y μ垣39gt4nm &Ē!/ wlUdu5v U*-n1oT-֟2]WT:-!']>u @wgF#PwgүS~ Vc)溒nkF)4͗JF@r9"fe6HǶ]mkkcy>82af^]Fc"q @x< 8lC@ *H zn4Re(kT8G  y;"LqׯAql WCq\̦$cBhK>3M]x JГY nFU80G ն1v $N@H  ʘf@M亮 9W4ݙx@0/Zhxu #qē2Ǵnga8[gvNS7*4WɄo{ z˛ Dҹj2!84%]X.xX*|wH-i.Wl182B +i; rJ] XO8$A)(@B ,RTHT8GpȀTT#`La(<0r '2TEDqg(a+<dӫcbzCCIgGsT=RVel;r5SIC}U]抓Gj7weL(\U&%9s-A¥ee>[hqt3EYnqSsJF³ko6҃D8wXBcSȣdz穏ɘD7+H!* ?846|2ag?sgUj{Ԯ+[de͑}';=/'jfN,[xQm|;ym>~j`z%3;^=}PvUفڼ¹{_|1JU="@,`UiI׾_J .(}cߞO}=d2R׼i#o|ʛn=p꒩gLmΦb/ӱܤieI)~eYB{:庮aHd``v۶o喥K"b믿DLL&x1 KJJrrrl6M3 i)%\l2UU4ͫ<Tޜ S{,$D_[|st>8Q}A;07jR)Q9+O ˊޯ~ob;n}\SCΦ]T._sey̖]r-Yҟ+|ՕW54؎ugqlo<O7>z>zI+N?/tήޯ| ^xR+.)OͭZ5=Nb,[~sC /L#>Lq;35G[}i9ǯ= iFT,&3,Dm&g,1fB3iUUᄋ HRl0t:}78k׮ܾ!ẁp(L>| _²ekkkC"Z7}tD"vtt$ 4/k+ Byuvv984!!BH)lǼt }z_JN74 399cjAsSm;kmʸ֭ۮʮ#ֶD9dNپ[NR$Mq'} =ٹu+EE۶nSNC'O75=_?~5G׊d]џ~݂󻺺v<0'\[sO:MiH);{bC'D5vϛtg}X瘹q K+}D#BӒјA'h֎@G,G84Xi4GL#{8¾}},5x\,+/"tsMeU_E*UWa?wu{Gg1`@#IBcHY۞ ظ#FH"< 1b|_=cQYYѥ\J@"ϕ,Br]qPd:Ox:¸ W bg)MF.A c(.ƙ]QP󩪪)#aP]E3a{g9M\t#TU|cٳߖ#iζ yJQDҋ$$C#j{vEB(}_X`[NOw J+ɸJ(Ɂ3@8*+KVz@n4w/m9.I$FPJ> Y~M&-iM:UXmCAR==݊i^ZmjD|y9> DsuSQZhF"n2 rށ>iɲ.XFq~[D"???J566VWW{L4u]Wao֬YCCd">yKKẚ[m;gndS]ɮݻ\W|MzڴiǎZx3H)(R蔩3JKˋ>ψڒ-+}炓rV̺{Uxݻnysfw@(2;p׭Ƶ_lZĴSe|oܹ~G6n΋FY}똦 ]!2F֯j5#GEw\pq͡Ce ᤒimZuѓO=bɅ0n}u!9U󉙍 3#l6{6o~zoz)d0q ŋoH$3{$lr)e0B657Η^z0֮-,,׿8>gpsskH$L8;$ \pQմu59,8RYQ(~q˭;y('4u;v-wl6[^^}ǖYo|#GfTWΞ=ǟ?Lg߽nŊ]DFT*@&e' dFDڋ@xI#"K/~st߉H5&Rnذaͻv-ky|`0<|pKK˕W^h48N0'LfƌUUUT*77ԩS3f񉦦;w[z̽{hݮH?{pAmT5Zc9_|={ :;;;;;7gHt\- ߝI_cVYHd9UXZ:y-H^Ӎp<KUU3-\b9l7v,M*, Μ El6 sx@\žE\wHoyKȉFl۾hN.+QqC;OkΓC^}=#cg߹@eqYaA%jnj輽www }l$!'BBj "C\QںZT[=3n mu'[j+";1^hkQ1q)W7qθp/;DTxcC(]=?իi\QRdk۳-P_n0XyΘ('O֝8Q*" t=c|˛#ͪ #?}4?x@:clӦMk׮ݵkW"irJtP4=h:If'u=XSSL&#HOOO"P]x@1ɈH7WMFHVj6ZD"!*#@{UU#h}p8Lڐk @1M5 p9ʈ Tu42},/e+TU5H|K_ڹs빹P φaD<~+z``  & @44iҤ"jhh{WX xYY4iRCC4445k%\RVVu~Aӟ@헧bKI&Qg3T\;9#0!?y?}g#Lg?֑#O>$2Di1I zA%1.ݠ]&&ջ}3͚0dY{U )x:i=C\Dz5AOc r i'HIfL  2ap#1{{UG8_|LaL`D$a.h%88"Hxe< 01 :q/l-tÆ _򗽴 "N2%L:=p4M󦇢(~ӦMsiݻߺukEEEyyygg%%%mb˗_s5`Ћ !Z[[&yCBE'H#>N@QEp]&D $9ILq. u G$z]* d.p '2" `QÅQ_@@Rp]!)2R rBY31!?Z44ӸHB&mªeZi@9|1[R7K>Ƕs8[ib/ƋCW";vjz衇*((IΝ[^^99sf^^^:'Bai=== .R:SPPKJJ4Mػw׾I& b1)eyyʕ+ ͚5˶_W3gμ馛87߲ ÇCRJC9e܁ |(a1?1 DxdP Y na# bcS.x.9A9oxop B,ZtW&blfhgMWUc8J>X4$o;3EyXA}bsPY޿삋 цyӦZɼ`Ȑp+֯{.詭dl/>s !dhcF!xHqǺ˜{4<蜓waΩ9V9m#<X$NϞ={ʔ)hXqh4]]{{{ǏG" 0 h6۱cǝwY\\\TTnuu5XD"8?O5kӧ+++ꊊ oc/+'AFߟ%<`bǏb]vi`0N!4Mӣ9;.r`vQQQiӦ3gAmL}Le⚞ j[!6$37}龣g L \%~McHN?/ZFqԩ3fϚ}˯f㦍F&L[S_U&Oxɴ'xVѸQ3X̾VrT}Gg7Rvdn呶#M۲eKwOww?w2 =h^268kaC<#o꒗vN,豧v#;ښJX*rm?<䙏ϿdܹsOLژs/bxUN^isM }+s֖֖Ư|~iη^tJG@p!98x?yϷf R[u…^02u-*++bHhggg,9sKKK]׍F !TU}]wW\q &ND?!;cࣧ;'w9v[zW̹d/~pE]/]L唿⧟߰өĝwI]-Z}G\l"cZpSKwuuI&fM^#O0M B6 kƙ(\w$IMl!++~_?W՗6q\e8/0ٗ6>d4}/*Qџy~o=SAԄ{RhL¡˱ p> ;9ll'hnSNt wzYah4RW{iCB!16QK{-}˲֬YF- B^c1-AOG5k>8H(a̚54MQh4-]t֭-%ǔ<ŋiy&͝2 w7|c2 ,nq~8 'YTP5oHN;'LA.scZHtk=\zUkɉu‹̫x?9K#!N̯weսfu$qu]\d>MB}Jj >)[x@I\SU`@s#|i"'YEbΓ=nd/l\o #X çn/ս!,C)[HkPho$PD=e+ſ}@J{{'N(e"(&]`0 @`LFKYa=sBhH${zz` hhh?~OO]x9sfccc__]w9f$IO;'e,9]R`˧N$O2.CmipU%$Ԥ߯=CvElbjS@/{2>v_.bGpKKsgwkCCoo A׭Y̺͜YW[UIǒx]vE[{K8iCG2@AR 2ˤM@/xuuSSGK=ni2(>$@ L8t1H&-,,TY~W]\{+8D $q)n9,38\Ȭq3>H&Dp1l ! H!GA|Rчg7z9Iܠj䚜36*%99JɤTCQMɈHBP |>E'Q@'Ŕr82 FR" H"!og7<+T<OV4st_(Xʊ;fՏ1"'G긖+KΚZÜg-^z_,/I۲@ؗ_icL#X+V4$ C.% 0$`H'f2$ > 6G}=!BD@@lgG"5z AJ$1" 8·Pc_馛Ǚ9sf<7 c޼yX,JD"1{l˲uܹ //wܹSN CCCW8d2~_uQRI I5~[Jdࢢ ΈJ~T@$l'& @Q h@# #N.^qHVt UUNoTm߱մcNMV0yӍ7\gͯ4ttp·z^X۶_tET[[zU6+ FroD߷zqLH3`IFHLr$'#=K$1 *ŀTt6zy#{΂b$Hd$#8 B2D"F@NwK{A/5.---**4Kyreee/9| uhhȲ,XII?񞞞2o;(ѣGc999D"~UUɤa'NXbECC Ef!O4C_0\S t٨( "."0$\c(81P`rs:p@bD `LrF P+A$ tޯEk2pD$o۶q|٧t?!x ιG++\ +o5Gy2e&ذIwu-[2儱c0Bdm=N< L>}۶m[reKK?ٳ eٲ#Gf0c,F1\XOO뺋-e4%(ѰM ItI "p$' @vFƊ ;J<($ H"DG "@NcWq2k9>U4}?wp#kh.dLmNJN4EK#I"=%Ķu\ @EQtL$(Ɛ uA#.J\"`4*b$Bk]%($9p*c)'{?Gv7Fy*t(|hKy"qt]U`ҤI~?777 t:=88 Ǘ%=o<=&v(?>d} !V\q=F N=m9O2,k/MY/-MԠ8u'ۚs ~V§^yФ >?RIM*-NeS?яmwvOz4zrբo}>q8C4v4&3~D"0$^$ A *HNsDDi뺀HfRc=个sUfzr(8J,6Ne0 e  6f re,(hr3{5jًFtWWW:9σx5׼RJFS ٍ7ب؁c'N-Ljz;;xfL:a\5/!+(Iu K;;;ámX`oev]p]mm9H 7? 'MBGn}d\JQrf̘!.((̮x &ܹ+,+;;MeR{noԩn=17Ur}Rccy󲲲R4)6ypňե9s kmSy$/|߼]U-XU֚m?oָC}Sg\0͛7piN5ddggt{KSFFN\M#@X|}D<,**^pa(pD [nݘ2dzgp(̐BIgVq̙3,Xyih=R)uy555E"lT4 *h@#Gtwws׬Yo۵k}v> )vsٙ1H&Xޣ 2W^'Ƅ%yl7:˲pG_"ݽ'w[߾mJN>minq+SJ՟8#?޶mkZZ:<J1 յ\|[/?8g'{$!2~R?̷wjb6#"9nh:`d;"l $'IĄTBd\k$ZF qFU3b Hh1n V*;3K9rܹY9Ca41l Z=W(vrWkjh3g-ypO,ڶ NW/m۷8cˇ5 89IsݵkOyq醍:m-^ 6Hr][CC׿kZ W/%`sr) twwRxn ߲d29e]wJ#jkk[[[_|Eؗeq\W鵷p ===i3;]P:Ξ"2< 9A3B XF֝D,cl9coͭ-pS56!1Q,[f c]g Ohimu7(8\GncS?Et%38(" # GT#4b4d |F  Cacbn" Aj'%}zwýx7F` M~g0@iD=zx=toHc#^QGm27Lώ;!@O=[cr(Q=RCD#""plXHD)5uTwgæifdd)eyyyff1c'OF?k.&LrKyZW_}933>cǎя~ Ǎbs grUDf^0BE1 -'G*WJqˤv]ݪfJmN l~1 R}__\.TR "!*z "  42)lFIf2JjA&76pGD"irDJKKrsspC]w@(JOOw=h("m{IS5wjs8S٘5tk VF N5^_ݷYv8H x<9jexs /JahQd\ "k%vtL#9׀ El?&s,nC Ah$Ԑa{/W_9?y?/?_H*)-|;߾ݼ}{w^|Żv5\v٥RJ&w~Gyy^z幭[O}_/ܼc)\tQmms>~-r*!#b{暕 dd !3#KJKrr222=i>;"󋊊F#kr($z=[nc&gL:vLczޜYJg+C1H|ץ[dSO蕝rss9^}]\$aM4)'O>q{իWe]L&]Šf7R 5`|\zt!wYd)q!KTJTTTW.RJ¢"Wv 9c0QRiM#Ȳ,!TA~AZ߶mI+-˲ \)"JI)ȲҒR$iKK*++3q]H)ֺqMƏJ;\2yܲ)sss322qRi$G:RI*Bd2yБ~OBRAhhpw'O~nfeW]E8/ ֮]e!5,`iNNV^Nm%m;$6oݶ+?7O:N")meYI'//ođO׺qēO>y2D<`~~^__[>ys/[\~ᅓ'M6¸ g͙S>Uo3f}=ş+V\z}{j4Cm/(HKϚ='#+k99Yt'.YA |J_Zo2"l%l)~mKx2jZc;;zN\2g>xNaɓ'r2ii'"6ÿ|䉧 t}'wlOVpO[l`o/?i誻++D7L-J;xrhK{n_tںؾfʴɾ恘[њZV=H-9RᱸWǔٳK/KnW}`gݼm0iDb5_s帊.x}@VD"Ϟ7'?|K<ތo㷇uq{t/m_?\;,!nc=F@@%Kϗ ђ󫪞}$~Ikj;zH((+- O:i9ՆaD?2:Wa"Qhs8vtp(;ƕeTT:|x %ܻwռdd_WOhP!38*?}czY*es+"@`CCCn`侥0΅ fdd455!w߽|rW2e[nhhBٳg׮]Z+VhᎎJ4_b4\2-@)16=nǗ[-hUݱ}q㥓O`EYsmZi}_{cʹS|_{g'U-񚫾t❝7iW[ L{YXsZN7:^s@9ΉBGů|'+[tGj<_Xq7˷{&uݏ/3gڴw9_0wy vmM';ӧ}.ii%楗Oǎ֒#GĘw!kt uF gz'L-gFR,OzիmɑsPowwxx_?l^u퍱X~6{ RD"az}Ǐ}_G`5 ֬y4&14^o,sh4zͪk8 `xwcI%ܲeKF   Hq'n}{GI#`V^|@ HiƎ.^Ξ;r|Q#]]_~E{{{"8 Ä P c=@p[mD"V_X3i܄#~pxkvqcD D0i򜞎2Pf͚588hfiiʼ{<"9s :uիVZ6eݒ2nͲ,kݻcرc>4:!qaH`|/caUaںdVf`pphÆw޶i AFf'/8H&\G_Op?C?,\X@?Qk<00՝&X~}<PQa&)evV.6c#RsSX0=p8ąٙ q4͌@VwWt.L~'``aK:cCZR)x2rk6tz= 4m`D D\3TI ?$QcZB |”Rqq!D@jC #G ٖLh3I9^,B ۶=:>= LMM> h rƤ.}衇8Ri}Y ibǎB4Vk̔Č=;1<&( ._hVN"*r &@΍p8_ǥW]uՁ:dɒhٲeZ붶JT,Ǐ/**2sJMuSE~.k{P(S*<φu/ʡl3oH;V%~78Rԛ\\~[2iWe^eكަBCH8KOWdwC͌N Xοc~ o$|s` A?m]7kF:`w~'m믨P$7kTgv:?=sG/}≧2 2(c v)Jp}D0viڶ5B4dȕBF9»3 1";HW CjRsA '[ۨ3IJ$8"Kt&R~|8?Fc NjO1q9sl߾=ÇL8wZ688ܢ9saBanj\/8d=!ɗAtb\3lsKֶsdl=z@@d t $RwlLܴyb -ko *0\d&Cƹ`@ǎ"Sifum1/ܵkݻwh& gj<@3Z_ojehI޲e0pM.b:)?riweA4)M2> TG(53l`2,y=˧@8`u(SCk *j‚@R&I$1HkJLH)k)N ' Ƙ&,Ksaផ+DDxD+wahϭsQ!ιiyˊ"~`z\iK[f * 9#ĄՀBfB1e^&! bTG؋ɕu&? AH  '0%.DNLG <Ґ <mx@}PgH`04"㤘hS"zx0뢞9b.3a@@QfƘg M@[\I"x[X$L&cD 're4B@)ߩFt) |cTJfddrG9r@@\Jyիֽ)>ih8Λ2Jɑܼ<ɁȢ+*M&^zue jjjLt'Nrrr%K5<&ڷ&;`95M3.yu=v@jyk:z{N_k:x>}ʨlN՗IƐ ٭>%;ƝH$\ }NN=`۶rܹP%%%Xlܸq'޵%Ga)*+@ye9V\15v 7Qu':T>kmm7n2###G"wͧK9T]/թXqc^rk"~b4I&aJlB2 VZ-H$I %""` Ў[iQH IPLhmHҠ% !(Ni$ A31Is-5Z@&MZj)"#D s IDAT)Z(kDԄ\6Q*B0yJ  %h%CV~?WΰtQ ~J9C+J0.)-TL!FF9 50 Jܟ GcS&MN$}L@  #SON6o隬ۉڗw\r@o ?/ ˵D$u􀿭+ׯ{O֟pŊu[7mD.]zȑܖڃ{vq`k{_΂W-/<|!0785F̌}G&/\k! ZjoV0!MDR Mb>l= B.y8,O_ff&-$@x'Z[[ۻ7*+ן27l>`'FXqǏX,6s^{mJܜǧʊF}--mmX1/ZLL)+k l~=($$"*hMLѯVUUUsss[[[J(''ضT4]<2:Aeϭ?C].1EywJ$so΢<9u٬bѿuLߊ9ve1yW}_ǐ{9imݝK.hn9zH&n }/}鋯'NsdN֘憦7}bAŒK=h{wddW?=]s/=1Y3{:::O>k,!czڴGgzꩧ,#g\la T' iHi3H 8S]]N&#Fgo˶톆֓mJuhk[H4JZ 'JǏk׮L߳78΋/.-xa~C"&ۤ1b',,FZ+i%|qm[mHRB4p3#IX3nBm;vO4Mgo|ꒋoa#v~'_d2H$,YbŲ[neϞ=]]]^w9F04iR3az W@kmaXl0tq;KkР},gH)T17biNI#`0Lb" FJV¥u;RIcDDDU$84"#d2!Qc&!sDMP3z M~D$IKKrʇzh֬Y?W_}(**۷o_`c,--glRgIN|~bf_ȉ{޻çoZ20p艌̐a Wj?Q{BgJȑJ ܼy=O?_UU ǎƂ057WwbʏFM‚]w)c?wosgK04uΠ3s,hjjk׮-**w|i+т. A#(ݹH2ʊ'#&XW; St FDDZ8@q 27)fsix1EQd"`L )d+Sf q5LVR2ƴm R)WTH /FrטH_w}ٳg_{^^^^UU1ƲlTZZVNq~,_ WI0P CbǍN(D@YI0lI磱d߫khkj^[Y\P3 hpWoWTwvF@(p(:Fж?vUOdӦgOWcP8^VZŒd<mkk/,,TJO:Lg"FH "F @ 1˂q&A+))imm#T\hcǏ& 9^XVBk5M#LoSZѓLZII@R9!Bw: ` H SH ;rtVvH7}GGb^WnU0Y\# ? hݙSߟ^[[;<Ʌ8AzX tOa߂K"@B꛺Ȉ;~ QGbE@h8$38 粥p8Z0u!0d ?>O f'#j$_QQ0;v #566 2|(uCCĕ;mFFPh2K",,HtӍy>lNvfgg@͞[7T-^*-mO\v׶.^:ugt]f׿]uJJsf۳wO2ie' t}Ц XE42&̘ DդӤ+kV_ޮ _ZvjE[hak{puŎ=O>[n[v/rʌ [ZZGpNoSs:bcۚtŇY84؞8Gmfr,P&sj^$hۍF䫓@|T0@)P@DdǬXf@$G+HfF9J QhN 4ӧ44 t-[~豋V,Kp D6gq|w]s˦>5ec=sV$/|_?p]_v%3b΂CC555|dD?8w|@#0q H8?P1n`[+/PujyXqK}Zw!R L$}}}J8޹sg~~/=UUU_WƏ iiέ3),JIĘf{"t =e@@S :? PpasVQjI[JAdw!O)Itʬ|^ID.}tztk]=ڵ>mjYYYZzڈxyYs&޳m`o`1NEt *02Ã7tupPq9%vH4ktv(G1ԄxFÏ1iiiqtch4//^?.q>u9Fn6i2}j  nutpŅc؏GҨ$]uk82A\rɎ;f͚md$,7rSL MMMa(M8NΚu@1čΟU];ug_o(?v,ꗧZJ.55:*ߕNOgƙCD+pM)j} #b=ݽ]p$đL455"# 644144L*z~dJ#2B :V 9="6!rPg K"ר̈́b4&G}oooRΟ?{ŊBs2.M6mڴ[nq!s4޳wFw!wr_]F1&W^O^._qaIqYgGW_7wt`7펛áG R믿v%k&`߷}{+WܼysNnVquu'ҙ.{Ivl\QVH lٹ?rrmi_`bhܺo})r: Լz* Jkq=~\W:-QI)PtԵJ?R86F4hBk&\zS\M! DURשJ@{(P#S9H2BP&@)A@d4B `Gmyȑe˖p SN{o&׻rʯ|+7o^jջй9?~O689 ']l޼q=j-*gؾwiS~#|7~SJ@ o}W?-_7 {w```hx`ҤO;yy\q~O"jkDв@yYټlZ<{يX~n!hpvu}W?}W:6i\7#Y|e%6PMј7\{ kֽcs^~ ,6m#G⾾ލ7JGbG#<'3I s p&OZ&eD4cx Cjg{ydxVRk 5)SIe.d7)`G ܜDC=͋/d̎m[<.;&U=xI+/ݹ:[ Y^FEP!.3g7-+LJ=v${8'33i|=gGlX{wjuKOGB}EVwsvH9̉NS1z{{233w9{v"7xbWt'X`Ayyu?t.:?q{x8K Uk[O~c#!DFcsn[߾~88-ޖFsJKKhuxVfQ/$@f%Bc3ƤJv2/<`n?84| O,~Je e犂H<ZlܰЦ _z0"i?w6&z'Ob&ddg5innI&n#U+p9xnNNk7 $Z7iR$[PXX7yE4wtM2())"̙oL:5>aܸNj',\PL:wugcS}VVֺ) G@D\x<~es&l'ڱܜiW]Q9T.7- j$_c].a8o>׻nݺ獍Vjoo;vlQQѓO>`=c[F~-9"App(?KȤMXJX4f۱R:(h"}=m[Igx(#5k;v(aPSOtvtttdhiNvF쭶HCCk^~qqiΝ7{q_ݰ.wX'q?{oUu>|ST1Ϡ 3Ȥ cmM41$~:ر;O:}77wΨ8h;2 3PU5U_ x{ǩ*1"g^k}?LUUUDǛ+**N:5zG=P2ܹsgwww<ohhbj"ϝў5B!۶FF2&a!SX$$2ظP`@[J1(~GF &U' [²Pۀ0ʀ 3;0*c!|@`( 'ff#=sVH^gB'm[@ì?`Q?A0\sHT )}"-Ami}K_z/.OJhƆ .~W[K8O?s+r7G^}72tKcm/Uv(v V\yZPS0 $Yf3fl_jՕW涆h>jr㻶.Aj&0c.w^efH&qgz@НmooWJi!TOOrŊW_}oǏ7Yrc=YZZDX][8pCei!cnf4gJ|2`{ȩP H@`VȤR"☔@mK *A)xfK6@ɠ F Cjͱ ! - hf˒,Br-ڰ@фd$܆i!O{G}4l1`O2mPvsXEd-JϿ\PhMd ʏܻ%d[mƒC )#tfR y>mpFg-BڵKJ㿗 GMM Ƹ|ʰQ(D0vxϳ54XPP0bĈs 𣪪 kkkΝŚ{E-[_o7/kg͚5E9ܧ.\LQ IDAT'SHxBy繞d`LƨڰVCqip=P |(c07@ f% w6A2 m@) fRR'{`)яg.i "6aLؠ  TAY Ӑ.y6dq]7~>; !"R,/OkDhffӎ)aϑ!mG,+LeA(d10Ii4 $KZL`RH@RF81j퐽%n68-%DP@ Idk$#L+e씎(@ &>xcLiiiUUU___pK3L{ |WdMMM~}}}CCCYYY{{ww+++kkkRNk=zOT?ri!'JahoRl| imW\qo>G :) Dq0!O;Stg[xIc4g i>OT0(\5¡/+,dK)+oƏ+epH*#]0@f`-|߬^& )4~Ѡ6H,4&Y!QX* Q Carαyۈ#n3fc4 D ` QQL (bclm ( O+AA@ IhҾ <d` }0 cȂ[(je0dp6p}_P2Ƴ W} jq{g`(-n޼D"Q^^ǕRonn~'9NǎCĚk_vmCC]\SSS__7:t!(/, z\vm?NtqUw{a3m@)>c I Gf2p6mIDG6cNJN2l-d>?ȦFDΙ3?²/♥+$͛7Ӯ _뮾Į{zc% _EPϲҥƛeCΪ˗-D'OSQ!發X^"Bj6oLoE$em>vB!YN5KkA%YvT޻KSo +"F`D%ӃN62W6lƦ@߫rĈd2M4W_Fmmmfַ5lذ۷[URRz-[\{h/Wx!W4@OcƏ6˄%!j+}~CS=u6mަ4jOV^;HO(2dV-_蚴3{k>ޑ㊗.`ˡIc{jswΜ2tl׮X4tv5T7hM˖-8/}}<Te\l=/[zƒH vxcݺuMmEEcǍ]n݁CZ 8МcC"/M[O8T(NV5̟qauOXxycLጒtq%u3(ougϾ;}3zŊ+byS M2HF ^~W/d\iS O`[o}ٲeV,?rowS]2qh-[_hޕOȾ֛Ϋ[ϸk׿8>ZWMYQwe3hW;RyͺJb)Rok[m&d{hVYuSʶ-OMQݴ T!Q3=Z`纞yg\~럃7V\RR4™|ࡦلzh\O5~ӦMRg^ܺmg֬vS&ֆ @f[l%7nxKܿl͛ qN_qe۟m Ux߁B姎+TɉFU}p&/=d(7.$U=5o9uʅ#̙={}ڢG^y;ϱ^۹؎Ql@f=zD͛sΦp8|ĉo}[---?,XL&evwwG憆fq^}/| ߰aC&Y`o|;[n;}ȑ-yB҂L"ց(~u^'>Ptޏ-RVVf۶gVVzX;:PcC>H++EDBҶrAپӨQ]4ka-H;WR#+ϖ1`m@ r +(쫬`RZ7}_GlƵ-PHj̚=%aY} 0fǟޭ3g1cΝw_P:0%rBaW>i4ƸDCQEHS<4aC0h}VXXx677755 R^{k60h4Φ"`6$LHH=T0 fl;/Q2\d<y Bv3P!PSEf΋8CfF!q',>K* U F< .'Z| ˫:_ܐR*w}'O.++s]WU6ogy}%\R\\L&CЬY;6r]7vm7İaîL&s}ƍ={۷m\RRRPXdEyP0/袢?{7$ٶ}o֭)E4~D"QUU\}ߤ¨Ix.QJ*II3Kf(AFG22Pa5SDPWd6RXHF+I3dcٌ" & H)ÖD&}>;1xF 03TGz@Ffll pѶ biͨkY >\ϡ*ٽ{w0t rV+mF[R ǔ6AHdhIcd"ahMHZDZrm}AoT(6Fz9:02$" ;TH !J[#&t(lP0J'd\#rIk2??ժUcǎ ⊊nX,vڡ~ʔ)ZkcpSJJx7 ,={faaa__o-;k.]vIoO|ɏ>H8\PPAZ?T;OD(@<V%fm>m>4Y6!d1da"Dc<ۚ\ߗ&0K "hcPVqXY`d&8ѐow0} x~Nǹ 2f`Q$ Gs1tkHp ,+@k@mB6H B3hI`)-ÚH@=08K A .*Ema% 00 fZkA9C?# DUJBHVLYk0 69_P #`8qԐ}h`m; f!Oi.Y !X +#wq5hH+ ZP ;.ҡS+ܚߟ`  bID|BРa`@.L"G24 [P*0Ðq`_0  ZÐBT$ƨO9\V']m;//oڵ\xK,Ѿrlm퟿eMrQF_z)۷+<^)4&k gԸ +W\^[Y` 3orBicxU D(ca?>їLdFG c'kZݭ߶jʬ7h $Cz?NGZhܸq ?\~yB껿a5UR taa!k}ۛjWojh޿|<ڵUՇl.[5h_z./쬹H4us3b,;ּ~˛W.^R?i꼽G\["w|K{:YJZHvz Ϥ@b'tY_5;6d}:sHOҬWi 9onM?nT$%s{'~"~WUwq/yۭ.e#,~QhW^sºc%,F;oe[_?PvdM+vu͛xΔ⪪.?ۻdCUsˏWF,Ǎ>gPFr>d)))Vs~u]ݗ7 zKHCJC͟?+_K/t$/%SIK$b55.xQMMؒjmǎ+- ,\\Igx'M(+BxxDdsɋC!LVUUy{u9t` &;*K~>Ιyg24jD8N蓄CgEJ/s66vEь𲈄!mA{rHoKǎ?ܺ˗/;RtggW[K Då%#&]t^l\?[pѥMu];x3O_~2sF zժեK:șLZkm Ԛ?Ҕ(l6LJ~3s(m$dRI6⃎mY]JI"iwdݔ~MuMoOϖ-[ 򋫪0Qvbi3gO6;;;3lPyψ &@Ϙަ@3gرƎE^ ٶ|k@E&&#9}T,8tj>i8LO$}dUq3>mnE[ZR=?QY=|xmo'N`ш|Xe"د*atW7~71qH>zxm];-=m!7HBDog{‘[o\U^; min2mۿ/ޙ3gFGyCnۺxJqc@ i't L)S[[N¡w9dv,C L7aBd0?dtz&!b#*c&c\7:rԀ~4^(!>@u_|1#@8؟&t~V0ҙr7C/[8OQa8OLcOd&ٶE@&!mݎȦ"+NQ{1%%O?B{o8݁ gLCa4f)`r`f!`z@~=z{ɓjkk^C`- о-Og5PRR`:M- Hu}$)B)YfuVڑ"` J$N$)3i A xp Z{1`AZ{dtx&d,JZh c1h1[^%_n4Ѐ=iXB&|€@0"@v` T 7zz{+F.(5 zi:4F@3p9с`볛:l}!ZFD$L}$R4Z[J{ʕRTJ!M@ $kM|1ad&9Rk) k&f24B(f3gdi9gZTE/0lX>N)p]\%H(_w~;ȇt}'CۯYxIy9ϟ??Hu]7ݴζn_MHګV1BP>j|ֿZ33f,?/>~aS&O^j ĉG5o޼ѣG/]zɘni XHUm⤉FZbq>F nm֎E3fW}3.XT"<2hA`욋\{E\ji Q*VUa +R6R:F1`PdfѶmu4rK.:FχIf3?O6;iKHo޼iշ?l&۟'N}CBl6;u_mя~M6ErJS$06AS$*O|Rs##O IDAT߿ZRi_o"$HʏUZu͛>xװs5ȑ#ÇZ_tE/ʚ5k\V  #k'C=hv2:yF 458rXf?UG`TI2BRn" ܟXugdW^{aɵі00.X߾n[/^PX~iii,3ÑyK/Zr +QJ1Ñ0@h~7 J^{uvN<^#ϭv]4\L`۶ANtO-f<5v^0ڼysCC7 Q1rjjBN`: ";wzE"#G2tCCC_#Q3Gy/\[w_mذ!h=7~ܘ8:rĘt*J%òĆ Fyԩ}Xqy]]ݎ۷os2l0v(#V( K{鏝KۗmE+OB] i|J\||lO kϩYEA]&Qv]{%&^bW?o?OA歽^}7~Mis.[^|酥,w 1ˉ>D aDȼpK{v-|w3cGslٚHƛ[OE"!3 `ݙoɮh?DD}}}#Ffκعc nټ5џ C%1o[l;\>lXaW[6H'm X___ )D;2~v[[K7t9ٻ,V Fvc]-lGJ M/bw<^WWa@OaDk}{1N>#祒7X/ٺm}8HIA>`8鴿{!۶_w} O>CU BFG`@  3{:x{,U'N""62 @LNTwJh&][FΈ@___6;\9ġȀlN=VsS\q\:\u8  e H-401(@8g`8rk3aC+ؼy}]KCw~GV߰! c㗿U-i7w[njRUSSwgCѲ*$JyHH{*ÆE&qX00@ 9pD`CC2#p{gRVTT|ߝ>}zccc]]_$ c" 3kDbD;=HCLF2*"lsjlghLq€`d0JIsbL`DFfq?s\bEKlJ2T*$&!dE_{5ep,hl}U~;a(@+pǮ(\2tmۆ l1Z+$JyƠY !B3h_4jb0Gxo5hoo3è#d(xFLf|_{^@ @JX} GSY\nPhgg(t}stl++",6vxX[gkp1bv}m.080NˈrCDPoSQ2 &g%k6D @f%fО@I|!ԾĈX֠ `07P.N/qpf=(BHK+"G(Ȥg%'N={=ܳy 0 ,}~]Τ ːod:U?wgy=77o7[o[\YwkDGwJ.]֝;LǏ?O@@ɀ͊m]xZݼ`Ai[GO GRz{ҋ/9xP֖ٳ7cBb1d ]sQî@;֍56|kWLe8u]_Z׽Ґ\{q_yV~MmI\>ԌXq6U\X8s7d- Xw2h3"#0=h`p{ǎp&OP6779ߗɤ쐭xт&?ze˗'^/z瑩 OgﭬJpͷsw[ɤUWνw%}?( QV%ӠN7n|15j#HpؒuսjO&zeuUEC>UPGwvǛV^q,m7@L<¶z{YhQ^^3|ز@0ǩmD.Ğ!_BZ8!Ei/+ 0F4'7\|pB lohK;VxnDoYɝ1bFk GIG"4rD"!3JK/]>$Bbgv͘mmP]e{!@Æz qPtV-M= xw[NƖ:@_~G2\gLx ̱ݻ=>HDimM@+b!+KHbb6#HcL30kDJ}vk+bddȀf]c[1BҠ5JPP6EF~[=XP25bdb49$E.>h>DmV]_{ W_hC*)5:|<5x|ٮqd2>[v@64#w‘k\i)#YsLzQ{g[^_v%ژ򃛿 _B .Ѿ\"C/Wz {8)G$x `\j}{Y%2q&)BFkkڷ$fi`r_ Jh36h1@ʀ4 ,5 %?~b raqPt`R' f KAs]\w±c*/[qO2zduLv-%WBWYYٹlٲѣG=}}OCxF@́/xZ^tܰdgx{ʔItDEûT +* p4gO.pH AB@d  ~Y25B`Bx DMF@c |d$9]OHQ hVe{+ l$cPylBȒd3BZ2`HU2lHhDQhb_ҒpO0ح[`AMMMKKY<!^[CMZbZNh+Ki떭n|⹯~vJ&SNuaذ@ Lb@%&HSHB2d߰yXԇj_|eV~^D"3AQ.rpvq2gy& [}53B2f!S $ b4q#$& D YR@; [R,;<耧 HoaHd\qBYTeYJՍjL>Pw At7_/ܽy3uomZA(p!Ӷeֆ4,p-2`\;v/~h4;vMq0E\;>s^BF\wʔ)dez꩙3ٳ'o޼yر:i؉LZI\#zs* lz駐y""!>"-$4h9~8O?4%$@,aC?ݺY@dAJ=lۢ]A{ζ9zX甴SvXeZ#FL9B.r>qP&>;q)j1?{09L{oUu &# A0HZ^e{^Yzؒe"eK(A 0<3s3 Yk_{֊9U{3 :ӫ5]<* j%w00JDy}ĺ *K+'T UU'Ψ*$| LkF' Ka{ȱɳ'VvuvE*L&3=}ݖ4gښ'[~](b%K&SZ;V =lGP3uh8= M5ٵjJt,3@pxpp>O8-n0cl˲-iVRZk!@Bض-Fۖm !p8}`X`!AAؓz LHF%GHhmX#JB6Cgp&_>Pu-dWO-ۮ?sjS>a0\l&38@&ܚvJRgga]cj/&lkXHGk ~q׎g R2<0k40dV,:rWM|ߟ?o^ScC64qb_tfqC^6GJ|ي g9!oذa„/b,khhJMh :;;Q픃, pGRX!nC_-oY}/'}x̺9 ?6/k$ʳQy@BD~U gkݞh4jtu=b,k~V؃46v6̞5oR]m| 1Xaq 3gΦ3|yoq̙s&NԘo*`&fc;B t&ȸ~9-H'dQiTh8fZ{ H0T\&vɤB*)MBP ^xW)I1D(WRMP(˗_g<7 %eKW]kkj|C~9gΙ3gr|=7o'|~-[nݺ_ [rA@,P!Ws۶W(tԊ33[[jy|[p F $G0 /%BzG - C~ _ҟϜ9ɓDyq&iؐl-u"N2uܹs/J--mmeNl샍`55|&3 ƐG(D# d2LZ[$E0!`P MR2 f@d9]K$8x!D*:r5^l$c1MVHjjl8i^˥Ƈ>~~-9{y=w\(YD[0u?7v;ƋM#G"5Ps-ˉ:!GL2іvL*+ˋ,\~qB<-Z IDATz.Oٲ(m}k>#wcf) wTg$ |q `q 9ǎ O<J)= p%2;sD$V] *afEA @FV ¶AZF B* p#їH&S#K,RWO>wiU1g93Z%*wyJMDPH k;!1Ue,YF1dthT u3Ƙ`x(:xO3j ;W*R,@Ä!TF3B9(6Rj%00 ƢxOU!6'?wK繿կ<7 Ϟ=kY> #s?9W/3y].1!(;_ÿ~rP~ nٺ36QJ&`"#Hv;4> L] f|@G|'2d-a,+G%&:W㕷7C dDbLe?wl}wە@Զ_}/u74yw`IkN_~Jm;{wm8{==0o|d 2Bj)E6"$"`Zd $_Z!AaDa;vLZXQK,"0\[ eTk 5 |Qa dl$Ұ@"'<^3} w8hHD(0WxJ@?wrQVxK=g)(Oܲf9L=t}ݒ}ŞtI>oS@ws%KKKJ,D9s `2YyֆF f  r߾; )'dNey kjjB!kC~Zy^ȬY۽ju.5嵀6n8vu֭m+2zi?];JJ55ew8048og~,Cq.6HauÇ3Y;w F_زhRPi~ӧ_p!x C_/9|0J!˗5wΞ9Pzǝ{Yxaq(aM}}í}7z5͗Z 7;88޸vӤکͭ]6=i$No޼>)Tm7[|IkQ0"%XyK)XfMh/kNdS&t<(X*_ KŻ_}?Jdd|#YUV͚5kzH)oԔ-mN7 =ީ/8@q2"+=.K,ڳ9kf-ƛ҉-^9l{{{Ϳ9j[gƒ dL8 Ҳ ۖƢ,3}fϝ|)IMeEu=XAz A~3V8E#wCL2~/i'_=- qA~ߺ,i;B>}h|ٴP}#؎<]Lh)//ѺX@6o{ow_yA }{^6ڍ?F>r.g::Z^wqhbEe{GWCs֚O?^SWEF=q&4gzرwogϞt]PoXY{d@Ggc=*)FPęFB[bOwH!Lnd>ǻ@oHkK2PK#E|S,9aL{w@V0j`yBB@CC6A*0D8Bl'oc*m9ax\T?ovHkmi 8!P`? DKK2IG嶶6#gojB@v=`zBv.%ydK[w bHxK UU CCCPBc7iE,-B>`F #LR!>YR S qD`)6ArP` sHBI@ \`~u(<]^β,˲j`(Ba%yV2B̮ml`IB!{D ) ]R@ t".g(Y/0&xWsX"\4WWc 8Xy3@rpkFAfQd0uIGFdl)1i&[HCclX9w#(Rb&ј3Tt* 3P0YGh7-"8ODyr Tb?<^ָ&c"CD Th4K  GF c<bd&-G60ƸH!HygF+] 4RAj H!cn0gAoc']"ɘk!.`.$\o*2 06JĈF0 /X!)H$$@FdD&FbDb3"#0gM4!@#%c+2VQ FA  A"& 2" "a`b4$DCCFJ8U[kKw+BaWe2Mk&9e[N6p.)"ix]-ahH#Chƛol:}i ˮ[5.\}]qTZJA"{#p^8!&*`R0Bf2xpŤȃ/0~f&(v[[l#|ͳz1]kwc7xgO ݮ"rr Dޫ]-+-C8'uR|)JVn>#XvOEqFwow&:niO=LY~ЎI55nu鲕`{˾g_B1}ꪕKwugG{{ssso42#2[l-Pfl8﩮)50DZ)iHn,K R RBd qLRY 0[V2CX`4חA@#Ik|xyɻJ u:[EB`ص|m{ؼ[:^Ӣ֮2pDkSCO+[VXXO(qn'd%MuUǎI$36lwΘVz/*(o>1g %wݷ*(;|]',eյ$M\qWscPiyab Ltd \Ea:;OS!#܌=yi2sO>J{ƶG+5аԌ3JJڊ 5\ll{lK{%Z?#!p8C^^@\3sus<2#ݹ(:lhzs,z-42_#`1<~yv?G%l &N`@-#%ɞT {J: Te;PB6@#I C/A+`d],3N2q F4 0,cbPo?D`1 ۹pR'T|rB[Oǐכvt u_Zfu|p޹{K?̭immJ&X[WWg:ms;xko[+}C,Ҟꆋ ĊϞnkrYYYOg학sjN:"zF$=/.-X2+m YF` A4T##$H"38FtHe"t,0 .!d .P@fA$s|$%gtAy^3k?vNZlpf僛t^!|  dHjfA P`0Q FW%@ #1 !0 y^Θԑ- .\|Eh05|\}{goR"~䑟mAp{$)Xly~ mg;:8"B]'Omy 8zR*{ e#(K&2@6cpxQ]Fzز,CAms3E0Pz~(q0$ P\rl?B( σ9Q=!"ZK +nCfN!YQiIF 8 `%c!@A3F.@0Ҙ`LZ9e\e@0aP GJ޿z_7FRBƘ9s?m;Ҳ+)|"(2k6jzKTs{Y{K&ҽaI$R21s H g!X $XTtʲ;w(/w.*)I+LV }S=}}곩x6J86zpCÅ66ܷoﭷQR\N{{vo/_{w 뺕+ڒ7߸MXqu,^&]X8.MV2t/Ԃ˗,^: ? Ps.]رC[fϚą'V_V)0Jzqkl1}&;8pe3f͗R.\Uy {:opzǎ yC#`~Q#_V'S s]mOM=c^aVTϊPɂ-l8O}ˊpk,RqĹ7,piYНwmYdɥ6 O2q}榎)[}t~ O{u(;ČUw;*++v3n\Pt\i_oogGǢFֆH҉ B.z9MxA33ʹ:j*488nMN=*)tv3O IDATL)<,'O?4j;>'k*+ ±h0Epl\iDH$Ǣ*.,)*-*!8 VtwZP>~ O=p{sܴO?3Χ+- E#H!E K PuV"+~.˛v"J$> X($#aPm kLQF F(J*kF%{3BJ%c̨j5F[d$%#<>X'4GZ_@7OUձ?LMM3Kƕ:t'~1c<2qcS-}_{ gSX6gJե|3Κϛz-OYJ}Gꮘp-Z:қ6?|ر҉T/shC*8ffDeنcI̞$)(Om'A)̂ڀcke@ؿɏ7֏.&?y1Ehn1?ZLA'm'!͖Ξ= ZhU65x㊂HLyUk Ǖ-[|x(]=j]DG%p`mٹg!01"g~L>-6mP[UpΜ}mmm}V}Aneeeg̘6iG0L\[Q>nڬyuuSCoru9[W^tG>}ϽϜ?vٳHXXCwܹژo۴~mH1[ rs zPy׍t,uHg[j裏ޚm۶e}[_xy)c.x~s-:B%mCGSE}f$[ۆ 3xM}תǎLJ+*jv.^phuSO?DRL"Ɵ}fW>s/pήG~H:|FGߜhq'֕7l?{? Ox2}ձMVڎSO;ݝmO1vz Fv۲Mb+{_Rj;v`?Wc?&1e c 7x%PJL̶mK<֦gy=u&FC~c OhaG_O˅i$Vh[x=}=&>r +?_?|1_6GyVyrag-e2>j])ddF d"}Q`+ d&m+O XJ)6a&MT 3>Y &#0hQR2(юB@h00cy$󟗔1ƘE"۷{В?N:w+ECg+1!a; wzx9ST`yydqa>:v,vvyGafk)¾+'tO>"sZP>PFJY.<߀ Dz`c@yv Yl]% ,KR0B!cՖF|HYaZzhH(%AI!RAl"0- b[MG\5c]vicR?~IIqWW6~k[1ՓfJ -Aݢ! XW} EQ-ˇ҂WvIт&ө;0_#tut}Ǖե}`BńO:<.fg{v{NM Qy};:X7o-񬕷]7gw[)/~{V4V?Ξs3bŞ8~SoeMyyUuUͥKա/ph RSSO ,Ç<G>/6AOn.pA1]]][dPaa)3z, m+ZֻԾ/r]_U'NF Ϝ7}?y Q7iJYVk4vHg֋MS&V}pmK|ҊrO<̣qɹs͘1=.))⒢iSUĊc;.]jJ$w3!"3A."t:mYVヌ_;wnSSJ[\M G| SN<uLp'NgΟion>{@`CKޗO'Q'|d2<~옛X-#_>{sҔ'~-]]RM'ϟO$R)*uL`R`㙃GN7T=sb`|斾K#G_uݦ? K;:[/|9r4?vjmK{=-jkkB◿K)8SDDTUUqƱ[nm?˝I)|ChWZ5k֬;PJ}%guaayo'6|d׮]R-(ZfN[ac| n"h>L=D_YiISSmo^}ٴYk`x3K_e !Ѩk.(< nQQ y kC<6ܓ#PY"TYkȑkW\6,_0w)m-]6drEsw9?w۶̓Ņ^ygfɂE`۵zucps?&k׮ݳgi9"%?g ,>NHIE3CJqK"84X M/tȴH rAML b,:LV8\!'3$3B1`afD|Ee,E#O5b8~i@3s&@d3ekgI)ϟoXCI}h;Kzɐgzw6=ِRc/01r!h.0{okUgRIE`=_(-++SJeهzHc,c3!If+TdP  f@@@Rd8a̯H0B" Je>"TMPBP780LRD*BP& \ JJIB &! p3 *FCpUҊ! "@D)D\C~g'Ƴd0>إFyf[?ncT^FxM˸ċDdx"/\;d+ }}}apsR)I)٬Y۶ӑȋ >9̀4 J8`VIdO+fcqV._|Zxuu qō+c%jcD9h&C`:ԙ*\ g_ʐ/pJ HxD?S2m 9bru7=M]}70ΑdD3f'tH8"HL C28"Șw Icc gKd!=AFO 1PȀXQ[Ǽ+ 1$1q4qcTcv["|.) R87?ͿD蟼"G3@uݛ>?o^-(D|j_0͠\ۺ{:_S&RrƂgz<U`^NMN^Wvߞwwn~k{=@!(O7ќ5'eLfLs2" :d~? 23B!eYtF?f !XNR9 ")rܹs++\XOg~YEYm k;iCr W+6H&'@)r׷:ʏie(!}6O3NipB@1ƙ$F$@!0+(e#f%7qnd$\,5ׯ;=M7_+./=HC]9~/ojg#IwoX}۵zn灿c9ӫfG~O_?Wͩ:u/ ]zꛞ.^܏9KW^sB7vozckcU_qa5+\yu&7Ke3$O],7w%Îڶ}q7oƙ3gәH$}Hl޼y( èoXd̺RP)7S-m.yUPL&uۜ]|0U-[juk󟹑y!ܥҊ=H_o:$ꫯ2 $񦖣C=C7_{)'>oxD,3핗vԶ G#oBC}K&8S}V,ן,/s++8[?im-сoV>7g̘RZ8G_z;=lLE3g`[{[&BuөeYG;tuuɬ08:YykkkqqeU@ ۶URReee@ t4u;a#"5UϣnO p*JL[HR-Z|c΍/7CXӫ֭kٲf̜w~憩U%ӪcM&ͽSB?c;'Ny~kG_6^g;G=wGۇp?,6@ ) "$%C}~M+Ѐ?g~̥K{q [m޸+8Cpa2(r:_o!TP؅C/\Ӯ*Μ=wݾՍHv3|_]Ïe\\?keDtSB;_O&*qgiSJrss9vii3}+M)g,/+EÍ%e xN(0udL&Rʝ}R`0HeD:'FwU 8A"k/\<[ T*ǴJNKvh0sM;s$odqƈ ǝ;[׬䒋ݻk׮_bEE3J_Sgw*A`>nxjg 8sӲsg̘u}we3'ƓO?Y]^c֭?rr@db[.-- E`].Y(ϑ!0'd9I@h1 ܰvw~pٛY3'uK7}vҚeO?~B(k2@di mڌޱ"nhhmĉ |hhH)qF)E2hj:tyi x}ADؼiwo_aN :t!wuw;v(9\TTa'ܻmS'ݥ._ ^ĻIYr%9*ou09g!kliIuaw]}-0Dpd_OW,2LK)}ǰCwuW^^^NNN( B6l ;D=$Cv7tMYq};ܾygמx<^/ox  L|IG"P՜K-GMm'jku5#"@G_΂Fj;#~g=\/mW0Pkݞ׷lOҩol5,1ԾcWjNvkmqc=jl$ 5gc"b*FBH8NCpxux"b#B DH$!F")|cL%T$:L&Rd8<, Rp8B3}'gLV#+` t=m[2tӒCDB+W^}Ձ@ ^}'Άw߽i&4&ˍIn1K]K&4)*"/kdJY8>~r $Θ 'Gu8K,ٰa7n\vmnn.N\2fHD2l&,d.CX!JcTV0Eq@@Fe9`H0VsF$̱Gs<@ IeM"7}R"!$C\A43PI q_\+)*m 2a!f R\1$!S!9q/s2@R }G c*nBJOB1G<}_zC_WZ5I=|G/ۖf%+Dd~֭[uV0(*++cXss^8C9_Gpy{Jy<;zǮ銱S?h.${Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4Fh4F8zY'7IENDB`mopidy-0.17.0/docs/_static/ubuntu-sound-menu.png000066400000000000000000002575071224420023200216050ustar00rootroot00000000000000PNG  IHDR. pHYs   IDATx]wU?7m!=PCST@P@ATP@P@*%  ՙcڝ޾ߟvꭿ瞃 !!!!!!!n}lX$NsND$&~DDOȘa"ɤkjӚ}/A-䋲$$$$$$$oh 5LmMEUƐ!C1((r{sĉsJ^,B[GW: ØV{|w`ڔI4CM(:|L:L[Fd2ˢ~0f.[0 L&N67mjl4 }ϮۗUI'/,Be@Xlnj1Y@|u̞9yYjo%j 4KzC}=qDh[[0t:/Y;sIukN혦˺xנ?;$t}];fϚ޶g4y@6+E0@QD^; .:M޻ſ-Fhe䪪Q{[”qƙg]}Z}Ɍ1:mwumܸq挙GڲuKgW>c464434M򳬶]W  QZp^xL+7j@q4ĺMOc)UI?Ÿ>#Dy[ ǜm/087%N9);;7R1=^M;k[&ZBz !&ڕO?7[ߵEwu.և-i3:KW뗖㼳vE7wRp_|-u 6퍠lv=/ G.oMJ^p6uEdԘ%$$$$$-& + cX̞8s=.>{ВoN0Gf7x+~Ӄ%#7A@vqՏ%ؾGIVp;'pʹ9#E;acǎinjZ5/ϚӴkWWIE.G`wkۭk9b [ZFj9wwwƿZESL.M?_<|= Wz W[ba޺Z!m̺ըPZt~[}c@Q;;w×^tb~򹛶ag|S$얗vˏ=yu.> Ot㻲&E ݉y'~Sg2&sۗn'Zf}?lUHh>oT.Emk>2nͦ8zX&k)Fkz<k_MwKI-v׹ .=sAn{릞Gp5Z {=lox *Nש1_2 T1GBBBBB] r`8\a_S<1r1X>4A'"~fBm{?<VO4W=۝Qlڴ9͚1s1}jMMUKb.F ps/B@9C:)S&)!. ^}홅Ͼ+oo\{'>Fo~W?+g/s/_YWWJsl]]}</\uoԂxU ǂY?×~zF?]c>Gev':qo/5Mxس. }?5ĵ7<v68.RD3omU. u~Gɶ]Rs[wrQץ3+;@W &I(v )7Z_}8q{8S=lo֮J)x(Fc w}4n 7il9,)Ļ@d! Ι)ZL3$GtHPz~OXW?O[y W^[xfmf<|GO3 BQկO<1H.]4tXTܠ( c}9gRRr 7t&L$'N7ޜXi656Df1Ϛ9޲e+5jٲ|TM#b@ɰxwu|w oOws2;l̯BzW|yH'/:_?Bg?/ޑJƂOL:xܹqGNDXpыۋ͍kJF5j5 3j֮XY:s s5 7~_>W<7Gf $ga[m-"GMF!⶚ZҔB֨rDBBBBB]C 6G80c“sa;C>|ͻoO5T2%Z1XӠ^78" Uab^__sEw-^Rakn<g8'iJ oi+?=gHɝ/si8r~k^k:[]5-$8,]_fuϙua3/ nF nXe*C{*JR_?搣N?΍i[ NQ)G,!!!!!."`r1z%|/}mkI8#cO!xȇ~|q?vl/]yD=r҄gz\qٹ> ۃi3ag\l_UK`w=\pϞ=:;_]>ē> a <*u*"s"ɉC0MS JcFn:ŻHS|2^ͤ D+ҫ?;Η1Eb814޹'Odt9q/xPJJT0rmpt@ScD-)nH*/V3u/κv/G\#{צݻ6}s߿\~U~ۛ \tۑL^@}d ~-i0$cx1f%V@ف\>ā8~+VʤSab7߸sTiZ~z>;oLUMm0&[FW1ߠX۪][SYiS]nʔOk K=N%~fԨ_O>鸶=h"WQXƊ>lqcN"P,i0HJxώ3]jbk6-͸Wdt#2f=s|ǓY1wmk^=uHloDy 9o6ba{s&21$ja͔HE>9{j㏒ĻJNŴj;g]ZǏf O=_<пez!_ _@>_P%ӦOɤXY,o͛;::әu6 [ێ7/I!}sY **o/MQj&PzGM?vm튢SL:ҥ*ʈŵ|FhD"nco_;';v f[[[XI@R楒lW0;SXLvo02矄;/dذ;v^!===_o"BMMfHrjìwӦ-(ʤ WEwVSCd7N5bYBBBBBB-a۷TUeƘ< VQ&jkmeK9A+K%DBBBBBB⭅@±2,EDMʤpH`R\Z??P(K`m,wdYH 893.ND%z#oU*:GjBtѺi__υIRCG/V: \^Dz_-2wD".%2% tJW}g!y/nd"jѻXyNK1n&3 vz#Dj$t E?ⅰř)߃!BS7s[!Չ{gk #˼r51|uٵ+(s(xҔUl)͐P$\ݚ*Fr#W7E`xq?zyŌ:Y"#(r\Cr U1+P"9KBbPVDVAr#"@ZfLя62`; &7UsS=Y ]'c_G/#sI^hp0ui<$"V`6_&Y, ;C%L,G X,+ukY9/K^ Ƿ 0;^vk#`¸]ogEA>S*hd`͠(c~㟟<_N3zdǶ^A0f˖jgc }ZFL-B^b@W wK2-{vGi #϶Ơр"t߅3`v?t#s:+;.8Լ գ0; _!8{0V`n#? L4exGD"˶ҘV8)4H{xFƋ&M1z " V(%CS+b [;d792X*,i{ĩSe B=lj)?xlfMEH=(#C`=D. 3]b뎂g0]h?A)#9!q.ʭU,$F 1ĸ:pd_Ba":2TV8WB<+!((咰u" "EAfG~N3 Whؽ%sJ휫Nu8z՘C6œ'S{m!F;Xoߔ3mΛaEOZm)~5Dגp5]4,; E~a;DDd:m.^7 E=o8BS4q6ʹVrZnGkqŁ_/ zWz?-GJEVezB k~c݁dps@=;6 [ 5͊;dlZQV&WtPd&DfoSJfF.Gq0h#n#DLgig/E,isš99|]dGGEs8i3/8͡.1:&·7'i@.,G勨)]ڹ$XQs XaUKu2(aEk|jyWXBF&'}kv`X"?@UN$`V ;9M]@ /̡"$VB!py 2"Br0@UB㡀IDr7 y/Vȹ IDAT%mGQZ\׏y%l"33ضW0!h)T XC[ V (Fh3U%$P\19º1I2c|&0%'j!suR3qqC}E(I`%YDdVA$,1M" ʣl YGM8!09~d`*7++װLz]? GN]Ya#C!x_oA-}q:,꽼Ǧ@!s<,}GhS,Ƭj^.qQ "OnU>5gVB eǘ=DqN nj /ʐCtMzko"dȏd{(*1byD^-4Cl{G_ΫP#ª=PXA?]jIB ʸu5,Fѳsx1-h3i-@DE=;n@$ٌ[fxw3fDl)D"?cCA,h$p)ϴOEyah& |#Иkcy>~ŀA"WSju7KF[Eadh/s " `5TGi|䈝 D*m89VXpFR^&O,7c"$`i7wg,0۔(H /^}X/O釫 cb0Wg]qΦ!}Edɜݱ2\r,{ &I%hc43]걋unʑ3,*`@Dtmx=;FV2{G0~S D{vgJB)j}Rlı=QlE\ֺm+K1"! On;0ma}p^H,dWelpj#DNq-r D:Ml57npDjs;F4zM$a*;@$ν:s3ȉA>z7XC {G:[\RDo$v "0R"GTu*e1; dHeZw&+wX늻e2Ī&`"fP""w*BSлpPy(3Bڈkc @e hN$ q[L3aD0bﰿI,hB$fRxPnme#y&2'K:hz2aCha݂ .=15ch;钭.0D8 =ΒVo!zea0P*H j2%YVDo*@1C WʧȴB{'| &PKC`O)Xv yuQnv3}0zpx/0"%QX,ekb ]Ȟ l/qwbA]bі/ܥ-"!sIU`jB;֛lDÈW}ݝSau<L02 9+9EvA6@3< )O"˭Ԋ lT+a/x`x{!tE7lxHc ZhYXt}P}eK̮nY l[5ԢSXTl~*]8) G%,k9%i'eE@)NdS?y cZ2 q#욲g>9/ˈ`PSV^f1sp>IE%W|NT!Hen  Ȫ;B7 ԡ>%#b4F?#pGF@ "X͘y(=E0)^vy[V]+1B,GD,=@ZF/ pNL ch+J¸f9k\@D829q;R|1oFguޜ[n|h@̿D>QaMJzaB E}:9s#"#~ÝE2ALoi{ YIN~e#"B #oWT !2[pK3DAȌcGN{WcZ "KA)D"29oi]/C[%mh:Q1`[HB`W[,GIA 23H B ȉ&a/1@t>w(HDrwZm&.+G3p*M!xP9wK:3&0j|{qMQhu8z1`>ur?Hj?wՉlMLy>6 H̭~vf`yT90DTVM ob Jhr!0ndx 9qwRQ HZ/%f)_*ho`hPnY к#ޑ{ѐE[+FwE#!*~d8, ZFn*h*i5m@~puj\3+Cs"u C*A[" BHl w9lޥՀWsrk.JF6T\4B/(D+ 1NooXk\)G%#YiTǬ~x5.p~A'!E9{ f(U Uw# O⽪1WiUȵ皂 пQ nFN^uxokЭ[!@7\ht`[kOV"cD>_znBQ4+N>/w$!`P xBQKx` _qXG_9 NVq|G4M^GǙSf￈sڏ /WªnԣvAtVD:ob1׹ʈYۅOmv3RD:^Hw(SnE2-kaWI߉3K ߈ܺWw%c(5 i1qwqF#r6˅`f.gWz) "<^JPܬdhHӻ 1d žDˁ9$,O8؄"ДyD?}9!ͼ$ Qo1(UV 1"1sM8P%pU OB-x%9cT덜X!9ƙա$,3wי$Bz!(-a9Y#::7:1֙DЭ#O-'1(k.Mt'F \bO>z7TzEţQ$.-L^y>1YVf+ʊ?}g.74躙ukV0(J[K'1 .g 9I5 !dH{T~@%[!moz?ݩaHh?(bD޶v5@`i!=bZ+TH~c/,U*qzXC>z#o<ɽ8礑=i`Jb'Bltb\mA줣"$uϵ2-!w/'_ҧf'qpr !,fdya([ɡվ*" ]'c+TC7OmuYĤ:! Y8F&[9cEICL&~ B/+ b UHΗ8zfo<3/LNl/ts=#ߎEe~YPレm6W^:M nl[,l?B`Ǫ/H`5!(2*e -"Y!Wɝ.J'4wuǨ䲷ȸ}T! L’UN":$ 1#̒@*潼/u08E;VCO:T `U7RɖWTN<=:Bpxr+E T$O ї_nޛ8&4W;^a5ýQI7g90K 6ӧy1қh ?~CCk@mwy>z l{;zbcN*0 = Va,:spnUX!9DeXCq)v;Ys2xSt+̍gY  %ck5_ZS?g~mmM\亮ivXEAUQ4U u9D 1E;EoГĊY\,)Vnb 5H($xb$;ָ#ES 88S0$/neD0݀i#G4J4UmmTCgM X(9эkFd=/z`D".#RiܸqG45574wwtvvرsXLT}^r2b2iLf``wÆ ۷obE#uFO+6jִ/z|YNYBBBg 7;9%$$< Y\rp"✗J)[2q&p#6bXg7kVKZ,B2kv66֛&7M>0wqN6홅 %=ib\]{#F 眛&/X|ԨD,VՎZH;YM5^U12%Jf "B/'@%VW3MNQK&*d6Ot?um>)|M+ēJi_^RdBYBBB}ɘHpCgx7|XdltY215+(D`#)@[ԑק7v2ҒGeYRU7ѣ[D.R-W670Q׍d22-L9rd& <uV[[JKc5^zAy(|.$KM=O76J_xJ5ɓO7,?o?3_8v> x^3s[RPh]_~zjyФ*uՌ=&(s?lʏWP7 T^*@,8 ;z&jnЊ|nQ[NUdWU29vZYEq_'.C q3n3W/r,O:MtT*BAJ&"Ns nR䞒/hJC~3K?~|Ï_F?Y=g.˽Aʉ@^x1| '+-!'BD]!Ŋ ‡ig}s */^N)fYv˪pXgb [ u)QfdakaBC)p^̥g~N$Žcrx_! p 1@ι̿YO>[$DC1@\.z{1_ Ÿɉol7z;tK^7nZ]!l _@Dd\DvG:ݞ |G|`CBhEgf؁!sb8] Hֶ}ա4} )b,"~`9LAqǿpҤɻ|?#Ef ,k$ݝGr &?RFU΍= ]Ϙp#'P)J) 3M?Y/M9Лy ejP0Pdeβ4gr9'I"Dc}55i;$eR /BaZBc/ Skv¬}!j]|c[sz>PPSߏX1=Z>$,LaMqsN*TBBB=@tY'jR2i \,J֢iX~[esU"ـ"11M{3;ca@*qSΉ5_>x&2rJ#0UU;:i1(LdVkιmmmb `d:dvoBE|/Y\{5^ኟoW)+dv42?#p;׷Á9_߿fJYBBBBbd%X=[ZFgΜU__0 0zzz.һ];eAs-ew:^0IiO) F%iBnltL4Mq3{e(njn492TL@D&7c;2L3gEaYV7qN\>\gGg2&$eȱOl4Z7J˴#(Pkk/ʧN;`|)P۹iĔ~qOy8LBBBB/VdÚm?9?c̑#GrέX,9r̙3=?ek|p<o96% q.$ ΉL9bZ(M^_7"[Kc.j}}mwM!4 8'T298q-WԘ&mذ^Q՘g 8ф q.!nܸC"F.5XMJq-qu"X,ijE0J$- J"Y,9(ڴ;Q]R޹/ 0E'F!o8 DBBB=RI_RȬ| Ch _g'"$(_ː4D>е+MM6rDs(J<?oβ+ocEpίGuT:hf.O~R$76|{M[mߋWTSEȕ{,ŮyHƔ HiY`O*#E@8XK>LYRY[%c " civw1U8 +0d[nUǑY,:R9Ѝ-[Z$4-Б)J,Zmm5Mz+4O^n%cի_~EɘzKoɟŏT*ʏz^x?g ma>I%qMw"1x<;2L&H$/ lv Kh1 *iumt&ǁJRv b%!!!!!!nc.SqMknnھc [fϞnH ; {ﶄsd2ӧ+|Vխ̢9g&wX,ɬduMΉ1dL4&_-)ZI[;LnZϩ0Et*̎ڸY/s"c)nr0JHHHHHTRL7/s1Gd2#(Q,7n];:;jk7o٪*J"/[7؆F/I/7߂bnFelFWw#Pbhj9g.ZlICdso/8!Nd⟈CNrVK(j2tvUM8sع'K:s%SmO$nrKsfDL@E‰ogÆӴbJ4LEQ`6&aLaDLj'cG^_1+Z躞I+or-aͩT{D<˧Rν# b8P%]GdArV>X<| DWVєV@$7*`rSQ54-Re=fIUtYN݀T̿5M+J(zT4Mf2pέᖐLd"_W@Dž6ƒ|JHHHHH@{O>rNN\k.W:-fgWON:e7u\0L:iLab^Opf(q zRM:=00PSS+c`` Jif{1I'ڵ;͎dxOOgx(yoϙXqd@H@ܧE,;tYXl9" r`W~m6mχ7a8dScٜ,޿ߎ 0 ㊓Zv\I?=[+o9bĈӧ1};s^Kr|/VBN*VěO'>H~Leʔm g.H V ?7oT*yB4rԈݻw۾m@vv^zK߻-1k#]G`ѳϿ2>K<vt28ĉ&Ϙ/*x[pa2Qe+BH2 el 48L6`bř,pΛMxK[R鳟b&9ꨣ8Z=cl…cl{%a?_}⍜ & /I ˚}q3<(a#n!MBBBBB;8{gvWZUq6~hc:8!$$y !B Z17pw"7ɶdɶcvGvfwV[tHٻ3wn3瞓 xp4svB1Ϊ<&$7kY``0>tuF޹sIܳ-^8hy^E! ׯYuk[_$g01&͒jщ  Hnh6qc0KG ЉOBP~<'$Izϟ/ID4E)e -jjRbˆl$Q)JGM 5F)'OdXg~qfI5Qb3Ϥ|!F R[Fd.f1 RAĭ$Ң" Oϳ.䣏>#G?^/2ha3ǤsP9yu=ٸ|ނ{^ A" J+rB›(LdNX,m[ęfG"\ H UTí|;d,N Ŭh k5k`#'y_7b}=6Ӊۑzicz(.K VuzAA_R r )h<Wu5IU@A+\HYVLySo^?9% )>D{_m:xNyD!x . dʔ 색(o%`b3HQ84Ax\7po"VGFC{sC4|H1Br_  H1nmS\{6V8Reu AA D61+abɴ')"ێ5c掜C:_ýh`XEjv,q܊lX{9_ IhdæA΢XLXۦ}wg6KQXEPiOZnKҒ#&NPJѷ?@\fP2q`>[{n'}!'R.ؾ;h ]<szZ8msΟ僿É'x-୆hNVt?oV.ArEn Jb6B ɊO {i IVH.PY{MoÖv%Gu?;39:R0N^BV{vKsXo.n&)y]NKU-DJ)}O`#yڞ'pbsH&t~8/oUGO芟} ^A$=lǖ\g YK09NNLVۘguuzj?O:$]7;l*+F.KD)M5V+όltXC4cWwTz^Т3NU@ 'V}ǎ6eƃzonP=G]].u^xq6o=f=ZXq_ms}.7x;Oٓ?Y&:"6,~aޢmmE+~y±TH7_u~ęW_1mb?XYRumAwėGpcF:mH0*HđH'ͷh_NKȕh˪ ͛WE(L(%ʦ$4 9O#yc0}dW D)r f||1K>;ԓ(mQSc0xF뾘xs/ڻݵz4yܨjЊczvhZ7ŀ݌>p1êj>xW|9]=৯pYߛ˾˜z6kSg]}-{k~ bc^rOnk_{o;_~.鿮FIlѻ2'3E'?wi8B;񤋲j%Yz%mDn"CvwϔS"@=rZmo/ D %_,-o_4s}$ާ.yre!xF}ӫ#߼7$mvG5SO/le )V|r 4{yOjC nز;;z4ႃzSF-F 9fћT+sυ}5m/kmc`ָŏmek^z^偉7>w4/wbvd0{`js('0zEeփYG׭ N!.+;:m& .D,ڳ~۩4Q*;"yr2|iw\:6!<~S1H7; 'rX37ꩧMKƎvǡ2hy(5/}5"(v(ֲÝ|i>[w[[Lí ୬qZ }O/5mv!=8n;_zl܆يYOwF9p9B8*N"m@{fBί4wUoiexiK|DʓS 9v@vM2Ig㨟n]_{N?Qٮµ+ٲRfI=+kXTcFs7cL ?I<~Դڅ<)P2dMWiڼ0$&/>X[1zM̈́1 _nhRR>QE$ԼyO==yr dNs|1 H % Hf?qL+`<ŕIQH`Ï>蓏@ros+*$xh}O}:gX֍ ~)8jmc(hߤeѺ1^'U#ŞFKu;@*&^yI xhA+9PU" IDATwbv6x޼ H]vIt`@`ʉN Qfr[tPN<X`.Ev7Zzh%ge4K?9i X+fMo10Bp[~ ,iV]5A c"gB~ʖ{; #q80)eTTyD1 !Gy  åR>:쬯E`BE签QuZ~WzV!! x"3649i32LO, p$Xbe'q& e#G[9Bp=&IX(2h2*_-I^י2ů-*S~ܗ`WGQ%#R芙'4m|1!0vjFxD%5++Z[X k8M mƘAAr՚Bv沇ePB+(zQ(αDc1)*y}p8U,(W#<3cX,1ާXxMB-٦EWc.ַ襀 RH $40PJ8t<{yKK}C4 C19['LnՕW岂\( G4B=1(^/ N#?QK_դӅ,/S: AI|0sT~m tQJKKx[[0 Ī*ِ2@_^oi bv-sIĢ#pڛF0;r9p@A\*D9GAE[":SVí/c>Gey)yDaShb@ ^1cug#ďbFA|P6f|W P.{}>o{{ 8祥%pZrΕI< d]Dc*EzZg'@D]t9ZWpoO r|1z9)qUUU~Նֶ6)e#zb4vb ( !Z2L h9fi<3Q-Y͸y2[)kVhvpzDr4ןv@Mخ+З9ey!^7 KUz=rY+cy3Θz/VZUkKkzuD^o8t I('QaC^$Yurk{) 9$< '@(eexcR!s.Pj'5dL)%R :xoٺU6ۙ>$ 5%CeENv鯼Κ݄ʊϝ1fͮ=MT c,0A(ofLB;w*l˨n` HvY9flTE*lGr9ldG1}H81NOmBӸsBQB]83X,f3ӡ`TWW_y *O[uUW^2a7PF*<±ejr\SR* tܘѣFرsemܴYɶMc@ U8IB z;nްq_7oظW^?rHīEvbu2Rc-VvwB\Y)/eF mG$s.7.sRQ~쳦^4ҒRIM8K;Pa^Ȳ^VeJ(+Ǐ7b͛,Yl۶ֺYRJ$B)L>Q#GR8jY⃏9(%S2cR4s_VrC EBe0AќAApm&pFlAl( AI`D+o'ٺL!8%_ s9ѣ\sy~4' >Ts(PėbcN*͔Rs'?-^fl+<uR犊ʊJ \uizt3@w{Ŭ՞,b(dp/jtDLQA mgPQ/n]MlD#+<ɓ'}˖o>MfZӿ[ߍ-b+i(Og2բ;.Iwu5cfwwj7;lgEz^i<_wNޗG HGocVviY\Q8ι޺??n9Q7` qN~N&c8ulLM;8Ur%U[ ;ؐ/ϟnzX_Mi;)J1۹9[L )6?iRv?NN_akӾ)L_ H(\rjrѧ9+,5JA8HkplcNc$D31lnj:{-_PcissKUUew46l) tbRk2u2d;dk}A5?˪aKJF?gSR'Ah 7o+n1cFS*cŊ7FMz@i;&)rqYI_oX·f$ Ps1e8'nٲ'ڱc;DHOܱR"7<7Β͍:*fA)ŜId Mw3 dљ$s H$(iPJ&b"y~*rjjj~zϾu2DY"et $f-[zu_""J !ÏB[~+>X9AY)ҨhTx IC.vT}(:}?4챚OnVlGu:17;{͚k]?fF$CŜUii2;bכ P /ϿH(BN'4|!g%8)JB b]Yxժ5P*we;zATy 8^2fs&I%]W[Wt{\*ZE$lEWh_v˖+DMx;50Ƣј-A VGB${(PMY!>Hp${q*GemJ 5W>#Gh;zR$W5RUS n߾$H$z iuFt˦6QAJcgA[^QEwfSY^}UaRINA;1I|b%Ͳ3!ծ҅>L `ě; 1&ŘB)|Jc\mTvZhI\GFcp5#lW[VfBǑwE9 5囝׊+`חiWJɷӨ8:r3rcL 73 RG"QAb Vx$E!scd*zpHI^Gjk]!A$1⅘*oӗC)PBGg|H) ÑHY5gC1{1 EJK}fRB""]D8P((~IzffcgكCm!z]lFQ.#tJ ].S=ry78,AH4 GE!3nP(RRD Ec~I'z9c,cRRRLw9CUdy$t0$O1#d9lWIi =z= Ì1:2`&3h4&IN9(£:YADь Hvh`|aPJ@Q|Ւ$!Yik`0IQ؊3]|p_\Wica$HYޕKN68G{-b_/P4#b ܣ-al~3!eL[Jqؘs ~i hkkK3h _*GYFW w%|ʃv\M"X@p)qz+q_VY=m $8U$rjChQ2"9jaݱ/T4`kk|iC8â(ℒb6\ u9+.$A$ Pe7[5ك!BB3 PyE< ]z(ΆÑp8 ._3YΉ0mEA$64WN؉1,x9%a(EQ4tG$)Eh4J)-)q gY*#בNX  H66d]Chcv]?JJ|r¼@ X'41.zh8똻aRē[؟[̚qj^/Alh ũm]%$5!R-SM%I'+beA|ܑH+)t|t+o0L]z#pOi;tTCvn ,\ܻڶgҵ7 ) VPG92!R A#Ŭ^JlP!ܹKtЗNS:ݺRNZ7Kdm-Գ1,--7ލAbe~˪W̚3>"1kԭrDj( H(ڜ܋[Vv9rh\j+@_钨;fP#J ͻAe;^ B8l.Ᲊt=D9tݴp\\LA˭_vl$i_+ e]n>i6, 5ć s!8çbS ͠hΞ6\#E EBr=|ĭɄ}<('TO)~ŬM%Cs\Lzdo W!?C˩g "4MVjY8@hCV17S҄ke5>uw-ś|p].7p~ҋg[L2ynGe=ؘeC4zns0`pq1RY1xBH'_YfbS8N\4: HkfN.rWO7T·m*cp*ަf63f3/NZ@A1RBgc37X iezI я1YUqn=Y!8p7Qey` f$Y,b%^ߠFF.@HgB˩Xb9r<[|y} #g`s xߑ{ּ:@>V$[YY_r=ޅIHތX  J439Cpmm2w)ݻ #ѝIwpo'Ͻk>Kn'ιncVzbGvywe#TOy&CuD/oޥ۞.ݧ{!b*P/Y~ϯ@8׿9kw<u)fY(+#fBKYB\9OoShJzFI{UQ٪c\ <2YX'23,j E*1R)HK{GU{7~A{*OӠJ*cbVƎ6OkCpcƎVS z6;D [g g3+~r?ڴ'>n7ig]\67&UVm#Z1tLخd j_AҖΘ A$ &=%ݑ-bDbc_U=v~\pQ~WЃQĀI{>"GoMS>#˺ ׯ?~|~\4)>k7m^䙧>k\E#?2XkC]m]mm]}kmۗ5l)( 4jOBTf]Ԓ>KfXF^4N7{XN*F)uRW) jeH ',քub o]P7Ƹx0¾XD]7bis=~+mk!ý1CͺX<_\>[]GE;pxOɇ6!0l1+XY*:06Y9fǕqM3,rTUR5׃/@AC9g(3'ޕAc6´~"(@xu9=eɓ'SJKKKgϞ}ǯ~3fah?Pa|m~ ο3~x<ֺqVi'ljC- e]D3jւ"JAIO4k3ќ\l8>agʨĎ>{NvY;p(*ICuEퟄcKw5E!zh{T9zLIbqʥQ Eg[0֍ ~)8jm= $)V hF=^FR x6*ɾIJWٽ?lÚ~ߴ3_}ucew]͍B y4q-R9gw{orD2>oƊƻ.qnOjɠӰMm ָ}tΨ`Vutݧ{RK?9|^æYY.}Sճ{"N[A.*%@ҤWʊJV&k [r\-ibC78{@_>ߊ}$Rh Tuwϩ@qW$fb/Ji޼l r XgRXޮ s`-_xi܏/ Z{R^~v3挨]s*fEm8rb%EADŽ@ָ4 k~Qh>|S|۾c3i[ I,9b[Ͽ #KG-|rB#[?쟪#bu? k?P.[݈.fy8 v|{nv=n]gzk/'.?~8~x 4BЄO2!Jiv& p΁0I(edM R(Ç}u“-?wB Lڏ .& IV#P!m ?,3]Q` tAyM}8NIVw\ 6Ѹ)٫1F-L$ǂ:^o<;&Wՙգgz<.r@1+*Y5Y5rx_nOGt6O)LoqO;_[ߗụ<:"9 Ts#_f'ؼ)qt v+(n=@qu-./f=eVzsڞ+Q?\QHc 8q%lAƘƬPJVtYDa"T/:Amcsf19gM.)KS (׉Bh1qFz3dMe16h\,crgE4麊YmRt3cLK(=JN!])l-呆Mm+ʷzvvLo46!>;3qHM(L) qŬOcCX{YRu?ƓfuK-y]+ 3R%W?w[ AH?F!P΀Sٝʙ^8ʠʞXHŬe3!89 apY4Me8s)j̤2[k1`9T T8xɌsF ]㺦ZֿfQ>$[Y%nȮ̪9&:kP}p., ׭2 `]>UYGo>lW>L"L㪬fc]n)¬xgҏĉd'l*GS.79Bx #EIo{C̿q%g3E0EŜ`/WNA2+I6bw3RJ=KuZWb؜r\\/i[nd'egݏP:#KH9R" 6`Nt=+HD Gb_VV.< ֢bF:C152[yAA Ҳ0Zٳg%KuYlY6n1x≜p8 C$Iy8hՄrw7K8 !R<\R7@$)nXAHDJZG Ƙ2jX a ʟf]tu(oey޺X,pEEEn;dwG$<c,6 K@E$X$d#"zV t ?5755?yaBy9fֽOչ $| (f(P d{M_ݥ*I.&,f7掟VYY=L +-+}R0Ӳ^Ur崁n'\un_ ̂KFXn`!! HA!GR [Аi\c]ݵ>WozCϴK-; Ū@@8+Mޛ76K@8ٓ[ٽ moc wwwxr5>G:wO\^Q:o=f=ZXq_mvڅwN6)ܦЍ;+AWz@f5"tYl@o|޳_< 3 n|x<Y6ŪJ/R2oߢy|t;P1cGt[N⎟:֫T]=?n z޵zsܨj @+۵~fA)f/YW05Se5z>]O!6rWoog /-*k~MG[ZZ:A1[ ,^T󬙣x}֏_[S8Hz))>k7m^䙧>k\E⾃۰yǗwEcʒTyJEݫ}z Z忰m_ְP2p=AmįjPE)zݬ`N/W~OBc<*J*K 6zc;{Uyvw #PRmGX˦w!;~ભ-( ,"Un- Lȁh2򓳘-~2_ᚿS FX+c>!|BJB;#Βeb$J;^Zb,e})/8x'iIe DQ΃[hK.k ~tWIIE qͅ3]1RɅn\ULS&\`PP@X_XijuY9fޑ,rTC7ۣyL!",u>oj=`ch}ݝ`{^Çhm:#aÆ]~Ǐ^U 2ӷ'گe9H0 r=N=Gp,7\Ѩ6z^.ՔhR }ƒ5O=VG\hd]0-H 䢠INP׌6]A4  GGyرCݺumkk ò H1{>Xo|4lG%,fK!жw]|ԒAS/էaӛʿzbGʬ4Eo_^pt-Qh󆟕BvMXs+xTHӶ潰`n\GZ/Wy"@a@h%gQde= S̖::_9[ y5&*=1n?ӷ Н;w޽Dll<@|(ؘFÛ~a~d'-4>e[@9V/n|Cl^,6Ze%(faw5HƉEAr2I,„b5)oXm̖3|dG3KOhh߿?!(/!ޞ9S+XsdbT€d'Pw帝\[י^qI㪬O9rN:!ҕTv%Ȟ f+9p.qN A/*{S6fb9E2Aֶkr#s0;YcKxq_*)U:X8{}X)rZ~cԼ%_pB))Ҧzwz@r=yW]s舊s{$DjоzT1z5.djhأ [q ?ouT~:̾z^8h s11,foɁaR2+'}wYt pξo'Ҟ-<ϟ=N\?\zB\G>'.;N Xr֠|/;~:~\ڼ@ˆ}߿PS}Ala EAB ds0d_U2F#w8ݢ/>b'{QHSŚZ?xb5/xhy7hgyte gRLb¦RSoʫZx9M,A_63pƖު|j=Ϝ&0Šdf MGc•QW-sVF^=i@틏/j>pU?9߽cy(<&u /^٭?짏?{ÆRSQ noჲQRjAA:)&OagN d^tUߛvJH鹽hݺx=|-7@qǘ\3z#)R2U%bs{ʿmS|e#zHm (액~Eb?$&fzh\][i`]IҾ_hNNy j!28O>S)`J_ƹ"Uv@ҤÁ~Aosֿwçy}w?:,O!T xp/X'ۣG}RJ^3X$BT34%|Of{VU?d`'d IDAT灉߽j^Uݻ]ysNnƁHDqk{Q=^9K[J2 Hq0ιNcO:&zyR@E|6 ξ꼹 JsDBx|]*19<.rzA O PBJ'dl=VySѴ]nƏ?tX>=JZ],0xDPSEyǓaVz̛=潇쁊fEHΚx-|&CAGc*Ie+u/ݶ\"WTnFoRA)3;!1f!dR-Hd_8!w)J11nFA͊9B`ތ[$)>"cLs0d BYLJ =72Vv¬{vV̚> SԳŲR/~:.58-1zsdp-:jEnչkyW& HwLbfjK͕]U#rZg5nS{P|C/@5{i w fgZ,y =~Ko7G+կs+d'tڏ HGVv̴ P.;C{7g~LY︬[*'ݣptzcYtX;W+ E3 `;Ɉ?5 ycD(Ne@mԻ{rU+@LO+{,%*oCexdT;oXNX\Ɩ WXԏ HQ˲z;#ې6Q-p[-!Ḱ֖(OzyO[f*'7мyO=N (:'zT)[yaG4vֲ=kýScv~' fu#Z'1|]s͞[;ZhGh~A$ 0M޲<"~vo̹R2\6hwo͠+~8c@t6r},RVǟJaC)=ƙv߾~o\ *f(jw~CtaXD4cBb GB+I\]kĶ|!2:oN֕sی+hӮXI\6z˿73LYXePNN#3>qCI[pCd'_uU=X.\Քٗ?ơ)s.V*ɖbr+)L.hR\]]̟ͮحvSSi2XԿo3 Ca2C K:ClQeAF$aj6i ޴(9:$ԎӃ7~?m;,5|WO?"͑3X/JGl[:9oy$gͻ9X]fBRu*E$oyoRN@8"/ի/5We,!M10`؈}zF"QWn0X`b2GYT`aO:|9 }4A$ ك3{ މ- H"2cLq773c?SUM H+ qz 4+[ R̊uIzoWx)!Z}9!9TNjHP@sg0AAӬ $ONv7qAPyRf9̑  &IG 9NV`9@z! ]W1;OM"EsF9Ye ΰ~r\lTVZ@hV[Ui|M:.`qi<f#39 NU3Q"B G'&t1b M) @4RsMNL@jPm\bA E*ӔzgX~rpkeb|ڰ}ZjWVҘ5a,G ɛ菜&k_o] TFìk~HqcDJ)T.i)~>[LM|e<pl '\~y5ϾSS='Y-)n>L3Shl4~[iq;-_oi\C4^W8N{fX'MM鬉 岦zRdS3/o!s[AP1MǨENS!e&ܪ\v>?< 5]@W|i_rY1GV!g;:ߵUo@0O2 ⽨pA0̨OТp۶UnnVAyQB+^0 I!Hyk<{3g?T[ެR99{x>=k[ dt%^۟+cغcr箲8],b(wy|a K~aD^3EKyWX̪UI1W]R2^{^TzÔ],bwW?NEn > s.E^Qz(U0Hڔ~kIgafv?CwϾ&Yú@+OؙYe$K-{z⟦T۷P>+s?]&iHإVwHV{wʍ_m=EOٙ/oۓs\_ixv οcVD]1$H{Uj/fSQEQe$2ʼn"?햏ű" 'B93sweEQETTֻ̈90aCID$ 3U10c\zQQEQv@):/e.l-aŘ{jJTr*Jvt.b H+(ʞZ&O-/</[^c*_(w%]Sh{]10i)}((~! {hF-KU̙V֜&+(>ͳS$*;mp245((x[~( 8ZEQȅ(,.5Zg{f.x=oqV`O+;8xC V&.#fiiiey#"rju}mm]Oǎ)$@QD#(( f.=kuhȤl hy9p`CrWdM&:!8rCWn?#ŰѧKP}Ҳ}eEQEYln"`_J.C'NMNv9Lih> QQEQ^4tZȨhr9|JgYjcGonMn/o;qrskr᥶3dYQEQ_9;f鵴67~s̃lXaQmڶ!"d85t[&n}Gjݖ",<(,`6_yd(WoR/B뛘P " PۜCt^E@E%!+"w˝_#'O7o9_y6kܼV6K["$0oOlj埢(,&3G]Yo}\kfeeiuu}23///HIG˓tǐcOz|zuْl4 Iu9!#FMFEQEYT݌sz4, ?y3K=j.w>L5]ϹmwBVD^u`ePÐ=[(,,㿩Qe;_muڔuS:CIL"mKPpJD$jTu7ɤC`pdTw`7s6?} r VEQ= АZ&c$`-aDs#D`^d*.. Ez9h܈ipcczNzꓟ<~~BG?m?!Tf&Ү,M&]]n a_IgUD&mۍ^~ӟ'1^N_|6}pun10Mlt{^d.6DcW{cxgs%"p O|\~ŕr )O|k_>/BM|Ƙ !#P?İ^UHZ(({ :(E`̩ Rf-29q}zqe2d2MDiZo&xYgy?}w: _pPh&=&ԊHT1O}6+Xۆ! L]:^_~1]?K??wr-:bLke:F1 3+(^ `_$8N2Ah'Ęi ?gaKg܋!D@dQ(( 'm.k!#y Qtq,,2f4Ҭo#w)ȻHxY7=ӑvۉǏ+J>E1*(pycSgb&j۶m__kwL!E>[=|i߼LJy+ >1ʦ!"DBDfEQEZ0JA&~R?X%xΉ>"`mljiv_ҫ~ַmM:\%p/1f+ 1ҳ[Jk bskrׯ[__1 g=} Pf+|M_劢(!QU ˩h%AY/I1`q;/~'OEKxm-N XfVsō"z>OçK~|@yjּiEi_SG"**EQ.ƦK6$iu`auH3|c_M QeVW7}K^3?Qhok_;|i19k[Zj&zLFt33J-SEQ`ݐ}%b"""Ai]_|2jHdyyj.Odm.IK>@2x[ IDATf<ⴷE`bFn'(_ g㑒jk3-zoMoCl;e|翮Ҟ>JԾPLyMPEQő̦?FdHHQQ:VR)9}6nm0dMi[jiBu4Ki( 9tkWn"%H_'Oщ'.]#l_u&)Ih\'Cd/aO[[m٥~̟V0 0(r@4;~%[ \/I(FDzD8Dĥvcch5PQl}/&du])lF{w5^+;gK{u]6 @uߘpx؀/0i:ẘ(( J".#Pcw,DqE(lM,-kk(J1"DNJn?"\ ^a&[38G5Y[[ovjN V&GYʊ( ZZ!{(o#[4 ~@@+b}8tw-őU_D brc%gCeskҶ ޖ",-^'bf$;7abIY -+(fafDD}Fw=:4TuFP&)Hki%µͭӜeKD,_dOK^Y&t1y}}s2ѡCct*0m?cӹnVEQ&HSzdD17j \~M9rhsskmm L){;˙0a}}:k7t[[f&VNL r ~X3ZGtPr?(,2%Jj/u Es=j@!8[~%6tFͭa6@V4oI񭷞< b۶533Ղɞ!9g6REQe} "b,/$s,TfHton CڶM6+]׭; ^[[L}+>TDD4YGQEQ̶{,A?!V@$8a&i ]4͡C'*ƌod:tPsdEc&R((gҖH__ 3CPwB4ӑ#7'oG#G5NfEK`F!ln  sHQSEQ!ߵj!# 2@" !8eߌY=m3@fn)3Se+++++#g#"%w#;$`((jЊ9Ho$f0Y%O(,`Uaiff AƜt3 jkEn]]ׅirsAl~eY3fEQEY\ZeqӤ^fR,^ċQ؏0"MU#+(Jf˧/Gq8VMetOx];{`kw/{7_z^kֺ^]5kx_(f~f?XEQe3!BO|Y~׋H]m\BS|þw|}'Oܕݘ\_B^Lg,HQEQL1+S/RWx a7lhL|X#|c~u>7Gwݯ<tvi߀}?yO~"WǞ{!ؼ?onð~CW T(( @~w{ܨ^dqܝf!"B0wItɾKaɭW =._̍&ʟ{ _O=\mEu{~/r罼|c??5g+n| ~eoOß#/|ŋ7#o|<ٸiͼ|Nmu~UO[{]_=wy̏cxVS_[? 'T.bg\%#G]4ICQEQRPC@)R="\z" [<曗~o#u#{ rʽ~3'+\KΏ~yKxk7 |}܍{;>y|o=96n7jٕdI;b߾g=.7~7olug@sNԥ_eW |]?1(~6Dg`ҥ(XeU}i,cm.+ӬL@JY"nt`ڿy/x\p?Ͼoxyܣ9/<1/Y9KSg֫oCgjιxS_c7ιݛK~^ҿvr6G$ط޳=^9|ƕ~W_wK_;1]V!1 LDjYQEQ#[eSJzׄ!EGlWBݼ7e??~OwUv2wl ϼ?_|ƭ'}dl7?>{/_׼v ?t*8v,GF ))*(}=h(\33 0L8u~ͥ_}Y֍_;٦6iaRT$ ÏjoLEQEsed"B1 b#6<AaƝ{?_~&Z5A%$(,pD1UR` !"2ISCf#|GvZFOGv| o/@w_ׯyW>}oo>?VoKD_Y/~u''xz 0?uͪڍ/~./O]ɷc_;'>ַ=ܗ>[pxɘ=>?E/l|Ս{=Oz;y?}-^'?#ds O꘻IױL&݄yoy8*(ʞ?WmCMC-Qc.< ro\uL ]pfaw=gssKΎko\qzXjoShfe((ʂ! aWH_"l#fVB a\y`w?! ߼c~iw~˧}2\F"SH((KD>,">ǡ#3 POV-]/}O]ŏ:.{g*n5=N IGPQEQ#) "2p`Uu$ BCgϾEbg.hڿDQEQ0L#jv*3VA:+B_'֥sIPѬ(( xC BlM l"ZFIҰYbz6uV3 "T.+g4$"damfz EQEY<,"&#s i )uh6r*fuW<0EQEYT% _J \yq]e{žprŘEQEY8&Fg_a7o:D;BDuzF3P1ץ0)(@DD3h)l_#KzF3cs.- ?֐(,`sV*9gc.WO8E9cXYYnZМ~LzМfEQEY@X˅9ZU/ dj2x#  W]&a+uq]-Z¯3 +cw٣Iҫ4[11 -9xi~2.3 >nMӿߊ]" bp볆35ǩr(*H7°[Og.)(;Q8Ͳ>7MQͦ#71 P3^DLvӈFMOs fgIR'Is[soH'q;l=V"zPO@D46`Ҙ75)=@DNN"a~BbaD(x~טּ Ѽ!m1H&r-$P.VfEQEY0nGD/#VNcFce% &Wu 8qI0q:}2ڄE.Ij{; | 1]؏rpDh̃Xl .(Q6MӈH]RbdoV^p*#E6ډ Ko~`VEQEP61aX*4ٮ/e-E E̠1uV Bi}p&j.EFa` [)ؓ>ۊu 9B9~[ζ;"v]ba#A3<Bh t{Ӂ@x1Yq!\BXN" ;i_B XQEQw׻V#i7D$RLN}@)/~*XP9 >&ZеiO0?,*mjw9? Mcc6~BS(*?w񦉳QAKG1*j>X3{BQ0̮ a(SCU+a9f2U{~=f#mؽRmWfmܩ)((M%-:_I{ٸ0Ɯ`3icf2L8Wb%), ŚY11c1fD$jw0Mj  $@ŕVلP"B>0FЧ JLX:&7΀^ #,F,xB3K30GY5KsԸQQEQ6LI׻Ddj8ee_4fgN6lX.^FTc t Rɍ:O; 4zN8'[UX]MYa0%.`%_K·"#ؠH~ P)p ]3Vv0`bA%p0#ыb&A\߯ҽ+(/twl@'q$5J;F~+qo~2+st}Ë1Q9"DAU /,)C,]ʟL9*- EEf a|z D? jEQEQtVK#YEb+ejɚLSb0 ){lǼRdNfכ٬qo8s6H,7 $Gsf@̅_qt ؃4m%M4Ƭ((P4O2/#1#iT_Y!9>/IOrb6p6 dd vu71UJl8+y~m\]&ۄ.,j3dWg34zwp:kݟhgcvbS̑sd4:@(M ͟m h1Sݙ)(27x Ehv髿dd1H'Frρ(U6QocRSٚ fGbf, ,܃R()jp79RS<4 rB,:ህ-K~a.@ocWoozΎ tV*(rf!ij7e٦ľd5c=7Md8;5IҙxnR I>jl!|)+dQFh)laYcJSJR 9Dv&Lq6A_ ֮hn3}_ϕepͽBDc cRmy1<(,X- $ nEpy^*Ri VJ`C}S!TF}LUzp&Si4䋳뺺X\q?v(L#R Yړ`Ð>7'@Uqiiq& U FbF `ۯ0hsvÅ(JDL_ T̗*HԅO04} ]DL߇He%iH" ,ˏ WH0m"v.%EG`"DSb>`VEQEC K1"Ӊ1m+w-S ".#R [PX'qK£V+)6#6[ yAYoiDH C#d"SR1BDd;I"6:Hk(#'`i$.a3qhCFjyȡ.ڏZKAh((H%c W4LY3jޚ#mhWNVvUGZ5x SGMTԧo0=I&{@20vU4LB{@$4;2= aiČb}EQefD @Bdg:b$".ma׹͖f%YEV+u"tH\xec}-J?],|՜E5uڳnpYf WF]N= AŴ(,he s菴/ng'|(iP6 z.4pt, GZ٬٘A.fhqđ#D4{42Z݅pጷێ\r-qyAV4#)@R3YF+(%B j`s(JA@AY7Uq%4EXTI^ ڃR }^P9k&0ifQvkɥ.SMrtR5zTCX2ܬJ- R>뼮WgNGgs+3 33U>$۲\cck>%1͗z)SAFftG=*ǘg`&m,*^EQEY>DL?;~q*9 AXa1^0!p6#R,$ۚ;lrc0qw#m3гו],(/1氏,`5W|:Rљޡ،yq$EQEYҢ#/FF2+6p\l[.fmF a?|_$LN5bKAS 5\v.1fwVn2aOc&s\i])ms1{tMX|^ii] 1!(( c#wdJ|ŴpېAf4ߊAX;MY 0Y:c<|FQ$hm6C c8ZP0? tH]a9:k\cW0-_g}g Fmt{lzmm 5Ƭ(( `1ʀa=&'FB}'+ƨ pYD)$v, HH$d4 o((!0hAFHZ/H FؔL ŔMDDDlOYLUkaS|ffDX*sWj Z. GLŻ/؏XFv>,>Hbí @Ip`45he9 4[˛ k#͈}r/yM ȣ4 uK 0o܅U=+(^M¬^%\&2O5QSJQngF~Yvf{m5lzèpoةd1n)]`CdII.ob#aYze@;oMsK2:As#)\CZ8,#!0d,0EQEQ(UL@$ oao5F8}SD#M2L+"YvIKM(9+߸J)Fd63zZqҒ@%vhkze@`8H (Jߣ?˹r=A+sSy-#iDfݚr\ @fPFQREQ=G6j.TIЩFcJ@`w4)4r7!G |\m"P/2~- `Wځ8 Rm6 WF6:m]p~c821\1ɦ6 " [6jW#o8?iϮ[d,݉(({[l _TU4gM_.gC{:b4f>ߍۊv LJ хH[2T=p&bަÛuZb>̮v f>s'J4OOa!.Sf(@9n+1 T(䓵2=ZgGEQEQJ:']@r9lJ݂-Nx)7Dէaw b>_)rJc@Dpk.S2M2 ʩ϶<WDNΚfv0QEQPeb"QLl}3C6dS*|NHg?9 HɸW4ׅCn{dÌ0>c\5H89 ݙpU4mv6frCa;Cq+ӯ)ڥ\ .A_5)(kfWV64l n*azvxmHe6$սyA3 wS_)ˎj`GL.:"\viw)& 1V/b4 Nǰ|vp`mӲEQEu?CdsC YQ2c3&xmccfj@xnPml. F~S{TW71Fbf!^KeNFb4+p+256nH&öwyHɹh?7m3KдLWK& r ƍZs 1fYޮoHo1gճ(({ jEVTm*=Ѵdd}eY>F./ c^%U9 5b {o9<dskilR(!x!  sܡT܎ы򴽜 F0L@-2oG,L3mU2sա?iWBHٳKa| Dv+]HD^Ӳ" ! }w+7MX+yC0bZt1( q~c>ьIhfiOS(1 sl[=SFrhX-f|<'|ߦt3BB c dI@"cga-)NBSs 4Dy(&@[͖gLRuvU*,ul;![(n1;9f~ۋ߬ͫ̔[[!Zb͌%LQEQ4HfhRԄy #1!/D5HgwصTY)fzvV7c} y M7 M.+m뭝B$Zy*,m*)0eJi5SpzyxJ̒;xO j 1F0m;MGe9KZTvaiPWq6^oٳ;@5BL ځ|wwi)׃.#rE:E0.Ƭ(({Nh–] Z9)um!" z!agnu%",['פ8̱;^N]esd+m\30<K{V=F/p>i0C&idčbv~HREQŻ0L_0*a<;[ }\ڮK-'b?CJZ*aFsd;%#ވWS^Iz} #B³$ϛY/PQ'ue1LH ? sBHu8 4E1guwjbۨk2(,H& dP{97EEOGUGy˽&otJ9QL"|"feB!(R:uްۋe7cVwO2 s fm&Ja>F5+CQEQ "AH- "9lD|gf/vV5Iô?"a_p܀D ud}4g,pBI&a6BKRӏT Fy#* ȹsCW̃͹TB7qYVFՉZXEQeay}? ǫT0RW805^ze md;HXYT_g2%t@7Rٍtƒ!-KqVtCJ ZHPEQ6/`0&S j$dFZZ0-4>`qa,dEM|٢yE]$(le}9LՍZyQD0R/Nme 9Bmzm^;=iVEQ!!((@B3:>_} "u]MMӈB{PR٦ U0N);mLY"Qu3F1nӏ%|6z|Y%ͥuj)-zʇLCohlEQEYŌɆQ\uLhD`쏩ʚO7r9R@&FFdVLHfi6mjy370 cm"Fobw}3|w(ip]2fZtSɘqQr\uL"P&BAWc_8g170b>yCQEQӏI_6 !!kp՗ Kz>DD!7`,Ea:n\5^#㋈,gEG5 "&̌7!%Ļ!$"d4kVD KE6QX)$Պas;x`;1R\Ak1-†8fQEQe"B$&3|7(P:+%5U[4:vF+(Է(l}nb "`‚G匧FIiviBJ&@͝{rW.feXLw&h1E  3J &+`5L(({+hPL47p܀/79 `[C,+Ea6CE] @oGgf]ħ"0sC" bCq)fj( dS; 3"h BTPDĦq ZF, H B!F|sq/\bXRIsDYJOFw6` =Zg/#,lEy=V)("(fۘĚb?cKBAfePڥQLزB* VԭF^z+H @6*~sF"8 -6if6H~#u 7@:u(f!<9GuMfSՕRȣ( -7p`|(T/+(Bhfɂ[j"#z]kҢO \&AJShOțA"Q0K&zfB2lKԆBE@ `6Iㇳa̹Bݺ,j8pl즲C*Di#|iLL0ts>s`EQEER 4 IDATZE5XdlLz uIz#! 0fEZKt%*AV+A뚦nj:6f ' ٸT|K-z%+OLSmr:hn~DfEHM(点sP$TL {՛FzW.Ю-'daXتe`@eft:o<\XFp@՛u;ryxzӘ m.M*;}ќ}Axah2$3m.6ƏGFN_ɵ&p(bSfzm.ӽ&Vdt.F[/K^Nc fFh*:,۩/Ooly 0~s5[خ̥Dj2PI4Du9R ߥa)Dk%5VDycfk90:EQeq`fAs7CU; WGl6%Rj!J`Y$"ѧdT,K hX b"v#>9U]z03%8EBGTVkfG0fp =HpMsA.;I`ntZKf6(({骮 4({P1G%ܢ0٫YRߓYfd\g J(CdaTiE\,/J .G-Iyq8S$[8yI'}fb.PoS(NJ;`8 U4_(m%sfم+#6{϶D%`J" Eg/;$Ķy7"2 @Sz1yĉtS옓^/fPaRr4<_/"[!K R7- Gǘ[V*EQeafw dx\R¨W"jM6}.uzTVZF{B(f"v&;?S\, ?(Ml{tLKfө๗Er9t;xxW)V$F@2ۿ:2!oeEQe333c!g,sFfSsrj 2鷂 #/LgA$V*/.)(-{۞ /&̗|Q]ZZU9+35"buSe޽ s{5~aRұA·[:DQOhmL 2+(^cDM mn9K貮)!+X%_rQsV]xc4#<"÷XgW}&d\D:Db 6"f>3 z;r# 8${S PQm"ȼ$q! @3REQe1 0!$@dPQ1 ^ú+] A BѾVyD\N%l6dt@)cK"Й5@ڽ3dTCmP[PX34(Œ$L ԖWe?L%C[X[/+wCgDm0WEQ4c.#?^݌?8U-ͥ֫ݮP ~gre|HY, lWS(g6 TqjhPg?!zjYVJF΂sP\plW ^rq_8a?ToKEQEQvUB$pv#eu IÎ|9^f$&wdxgD/ ,v7Nt휦tUVꡱNmƘܶlzze((c2er=KU) u?D[{L6m\ǒJ3|ϊԌ~9"˨뻇"3ܓњsߞ0#X)TOצC4O9^1gg$I?}REQe25@DdFf5b^"tZCfrjr #KׅQ{`+dг{۾`)2$NjY(Jg8܋ s6c$o"ʮ51#ʬJB1XfYk#1^KGic"@Ԭ EQEki!$lh߂eԎ$Y+~]D[*Fnîɒdי`hZ6Z`wa #nNo~ީ7++2H`v:Ues< 'RK#C?#=WPh;>gTL}e~X_ W5Iug @8@i#3!Zs躛V?Ϡ}n]!`>v:}=53TN3Ce!J# 1*ك/h]y-V,B~C(5ɞ柝[d~qxt*4oqY[ŎGE~~be .ޯNbdl~:lvGhmzLX3n־DW#*@D<¶Zgπ+c,~ C+=2:_4F9SUe|=CE!4DC--2H>hhH:siqk'PUBE[Y0s׌H|[!>x$8kJ-lz[P_$ZbVРs]Y'xܡy}qqļy+Clh\F}p.#H,~35J{ y3ThyGo/z9^by56eOm/k{c9ĞCG7(NJ/xR|=?u 2S&VBΥ-H;vug7k#HC6%yo>z\t~16=vC=a`T7<|sX"Zb.G[bzSAezkϞ\f?m>ovT5ňUO~izeӠRDa1W={WNhyGd0%D4NwEi4C̖50#s}7XBBe6ֵ#Q+\)U3 i$lgtm~"&݁zEDt!r|}HcZw~tk{x,-E {ņN"_(I !*>oa.b.2t~v=^FHfŻc 8Q'Dv_ۮЌ!š&\bY= tֲN>tqkt'[009@PWb@$B"ok6u㏡ES,;zƣ {8]Ww_"wWy:Mǯ0~=_OlbnH'h}/h{|=ȱIrNs-O9D S|kF~Ke2m}CմUSVLݣiwhMYẁ$=nU3\hN# [ts}>nbLˉY<\94#"bd@^E29Hu8wئ qErfu_%pNqonہxt?*Tp{unC\WW@_rQU{|=effT3RwÒ}ww ޗoߙsJሺAm8=nh^I7xKB>PxBhnD& ?c5]B`#,%F $u5$G[듔5I'(GFnS/7]ho.*uV@>\WJ۬孴]cZʿH;\瞆Dc׌{|=/q@d$a3}& rpaKg#fh́o5ff^e{nq@]#\16.tI(ZփY 6X" B3ژdIw NCK͛vJ\ <E7-úLJ;U},0Ҹ"iNARk!J?xa&OYdE:HP|?.UB{kG{|=Y\j(!s.x&BB},NrW c{ȠUSm`S+h8%9Fso&{rU&dz_ه%Oߙ`2y&WPOM\JnkTɻ"RKu0kYF){[#i _ Uߵg;pZVvI{|=#&:9lB=H ı#s~ƭ<7"bTl{FSf[HfdqIȎr(>==}VS$n%URb%PVQUĠ_%V) O]xdcyHi<ϕREmbw;P>I1Z{|DvP`'wTu-~.d ߸%O48<tUxOf}:Ql:;Hj6?GFOlJFn ME G8(\y_̑;w9)˰wm2<$0嘿{|E(2%"d|q~4#}iPheGCJ̲mfyh1AW.ȼ↢<"jV/gF_hv:iԦʰ'c=s;yHQ B)xʟ[Ĭ%zX?p{Ʊ$ L唻hƕxeW/u`C#5mds|S#[X{|=wCc ~h'.0np-!v أ¸LiXNߞ:M8Ep"CU4gy:m0|GrP 22C?#1EVpmbo+ۜ]C!B/()PaΉnF4u$:Ϳ\TaE~r @x~j={'1;و~.BɇZ+,RffTHjR_:x "g]rhROLF3c@G^,`ؚlk ^t=Ea;d*sjagΘ sG*TlaΥr&&P~@CVIRe%6KHbW1## H fQu9)?<Fb`fu8MzML{9'_6mz}蓫uH3Q\V&O5 >HKt~SV羒}ʯ XZ OBd}F f4JC6w5Q%03enJH_mR(&AqXzR[t2FD"W|#5/!zJtVN kwC?9k:ez#.G.=ٲblW2&yTWS]Y &DA)Nizֺ݄Z'i񸥎u2OmnV>2~{| K5-җ :kڟj  쯜)cǛޞqA8BڱSr':b&0~FDuЄx^r[(ӌBLlœC}fa5@l6AW82c pDWA^׸݉^ / z귵3`?1:"'7JweH9X<~I'Ehֲ--:C#4֯#:ίHvqruxztrE|Zvr˽lE?zǯݾX> %m %P@h92 xc/p}.wTbS<2 q)fZճzݩGvچ Be%Pqsm&GC7Xu眝43v -[T{ͯq; IDAT}6" }HёybY؎31#$Ẁs2V)&rU"e2 ~hRꮵI + 5fm>vBb_{PhThωv^]A)z豯! eE4OF{$Gl:| 4 }t2r#xChnB0h0'g7> >i0lD 'a_~߆L.CRA5pf8RziXW b2p>颪]DzCL Ml8"Á>VJUMDCwq=7>yM p.,#uu.QK-Mw|zT}l %;~lh=4sߞHwP+OOJX4B]]A.<?]h5dFW(~B}xۙ6_W6pzY ͫ6Duz4KT(4B/n )){Dcܽ?9&dBkUó*Rv-L{ 18$ vCI1bFM/]tvXzW*Mj*>v]|K)L}`t {MR&6&ϔt{N٩a`ww|3[ zT*$;ԐHJcB+rgFyst u6gQ-bϘ< Z\j~'r5p\~2}lpIB/p`Dw N2?¹UV`{A6dgq2?ZnQ]%bf1[ڬ}42TٵC4 >vGum5JKT2W8q44F6 ty{"\zT?J^5ՓoIRGצAF n\UZ-Tw)T^m6kѪ~)? Yu)+vFBF%܀߈~VcW-t/l#>CdY/4x+u>P"]5 ckֹ%c:G:C>?U⢕6[j4Ec84jܘֳG.g$f.ڮw6q7 n9wo}> j/=n}A!{ T  |4xgq`G:Ɩr6*{S:82Ǔ[7ᶱ%P\l1!7MmپH3#=O\5E]cDU*Fh76tgf|bDnbyJN:XeUhgo5$ p/=a2%ltՂ}KF85\)""aOiMnlp` 1.ʞt rz5'QuAn;qQ12M/x N.޴]PMM%qΩ>6@됼{U??,q/V뮾hv}Ku*3eFLMs9Pz6DU(XE7i).ch9ƥMr&[XU+I:z7?U)<ncL7j _惠]ַ<T_d3[;8wpW3x|)CD:!n$Zݘ"J.ץUxhԴKֻXQֈŦ!"}Us|v}Fzkc{]MvH ^@trV:q^e | z͐GNb8(tg-[ePnr|n$5^6:F]L4fQ|0HN "Һ$Hr.UJ4p@MIq zqH-BAcN . Mm=օ s>`ֺ*ip86sAdtr٧(E8nMhͭv4^?:[se[ٚK[}'b׾Z¸a܍$dQqrtmqGg7ي}Q H]vT9xI }V&Ռ _戹HjRoH+b[?ˆmFa`ݾRK9x C ܏>ɡi~ĝ?uΩJ7w5Hw=d5{˭T{"{ B.Sre_ |>um▲0-9в cN TRy@}'=zU~{)J4b lVD^P|5RpSzϡڶys Gc q1p}kj;Ԍ!mthYmS 7y/\͢jT0Jm,~JH 毠*<ʡCcoUQ6J)FͱΞ8<.}i\9aeO_Qn:|vH3ˑKk5RI%DՄP^SJ@\$ΞVhn}{ ׃9Nj8*ެsBQeԨ ?#ŶTB!2yv5碋9y4{xUqe^. ㊊LWcB͡z$"u' Cz[Zx5oO*h[[3ɅYh^U"`oOTOlG֪s-VlLIrzٔ=\y>L jo 5ctJ"kXd(6ưUfC=3x# Mc&*&;Qj7'#\dH{ tߥ(>ݿ p.$H\_gc{5J0œ']*o5Ci7P meՔ?n=C5$:DL郍q 5EgIaZ|PS NЌ4Ik?S`>}\0?)Eάw.;4JeJFFP΁Ch T[1v!ԎV<^o5tNHPY.Ul}b]n5a (G\c&L3-<> T7@Yiiݓn=U+IzL$/cf9s5YVI\u{q]???ug'q y^SL^4DQG1\4kH۹O:VEG5l!eZ1އq.%ؼ0|)bVw/l ]71jf&/wNq}0`:]*@aXO(C {8>V|fUVΗѿ )3RF[}+ ?l[h[q ͌"iTt0VQ2CI3.ԅB2d -NԋD܈Ř#洟¦Wj.jMf+C':uu2wQ?EGa1#gLڻ~A9UufȀ\?!?jb1*(qmL5uA~D@1W46 G?w9wʝ\BK/9T?! ,Vx`K o|*C>Or.!W UP+~&i64hY'8lL~opy#Ƅܞ Y0A4~$à4]_pNK? 8a~CΟffza6Z ddo.iށ73iHxR.ɇ "-E#LJN1 6P.}r֥ĸM{H6ǫW;{]'?K 7OCsFq7zAU][Kc;֕yDN8:\s& l0Е*Κbc㊺6W2+4?"bd(ZG%.`9 nK)E;0V8i6)X]4Wuy)&E!9?~ #umُ;æ#0+m :'HxJ<h-ۓU_脚BM c:Rœt4a)$?s}9A'Mq**kbmCUVئ ngw2[翨 61Ě3-AB&I=Q:577@N-5#A1`NLZrz0.XFZ`hW;YP UbR\>&+5wM?a'K])s{JƨAd_!7~9 ]Kq|~'< 1vA $ͼvS5fրܬΜ'a-rFz}<@Hz.8`wYʞp΍Bd~T ^رy%mElnhfbN#̺},C Se?l9tF24%IewXgeWvB%gH\n-V?݌߯} S}.tt|GM8~oxoݙ챧D]LxxLE8šrYt7\*!=A+AvLLw\W(I1j OWsShz?h8-ؼ-f%#CC#N=:fc;h=҈<.K;$fd)[NQkO-<rawق?qvyc\_L!Da{ZV@wkq0*B!@`jv9UզHj c\_&pCphӽ֏H=iw*`IffX]a#q-4wRuy7^ Gg,F\#a\鷟Qy>ܮ{v EWRIDAT.jiN@E4'qiy(Su{iԉ4sFo:1}fsx}V>mkR=FW8,A[V*4RTǤymPCszxL5nU1]&0[w8+ ǸDKG8Z$)"q86BL}$d@vem/Szaa[g>/QfWLzZ̊!ݲYϱ>IтpQ|zJoݹ3}e[~?s,֜:(9ٸm\'݅Oį*R/ cȜk ZpTRF+ECnɸZޯE< k7?0\O@%f7+b.wI";Կ1OxMmpM*zwvb;"Rɧ*nv$H+r/vnW }Q>exgVCŨm#0ЈJCBӺZo`GEiH`oޣm_ML%Kf 2XI뷈DVpP⟡?\ץ?#hgGS T`y)4r%g}`vg-۞if&M~bA>Dz*E/UY9Xŵ:\>*r9L|LFa \/2{leFG!Ƈg=9"g2l9 pBJ xeE˜ bR_i!,Ì(@e'|ecԥa!@D, :)Ցf/G^S @lݱ2v[MAלq亥71JQs)M*̈́js2[[6Wױכl:~yC}I3]o[npn 2n]H=HFtEu><"a3+R9~"<]ں4;OH}"I'7IR0~7D> rNR<#zR[6#UQ?~ϸgC \Q& 8V;[C8g;9'4Gy|1j \ Uُ\3*fcXR>5h_@ӝ+dPg&vjU 5(Qo~z@2"(wiyYmJ&N0bOȝ]_Q]]N"~'H[kIH}vPI62@π/_ O ea[u#2 plYDbJYv]J_ Z=f DwJnA8ЧCy |[V]׿z:]woV^/y*qxZ5a=0ַ٦sĭ#; ?E&xAsba,?c6SLaZ-7a.1>T/NK}T>o2SSwf&aczt)*ޢtNz~t~,Hni+ζ6C-\U<'[5KOɚ̟Fo+[̥³U5]F%Zaޚ&Y/R%ٿH唫wNwt%cr`&m2p؉_Z񖖺kTj&DZU3gSތpNb1d6i([HzLO PS*{ rEP]/(vφ٫f VVH%SaET&] ,z4f \R4^3]Н7.mM4W JN62pV.ܩ*@v]i`\/%|aFO/!#'L2EW(…Kv/u|mΨ)gnBJnYWRVMu۴`sY`x1r|cpY{f,%%zKu> G̰|ZB=I[2E=" !8F-?EކUoy@9;5 υB'ITmM6e`򦧶EK}?^Y|.\+12ץuuk#c\?zƆvqxtZ5|̤wks}܈9zf[PfYQ\-}fD"oaiи"%xzс."2k~zPuee;PLJo z*W'V]v"tqR,| ^z0Ua RmQqSj.8aΒlJ\=1QZbi,FL%U=5?DA[n)]ftY-O+Isӻ-dqGn(ߨbemU qG<>d?I>HSF@l?xj+GgI_X0EzBW9N9EsT1^ VOxGV@Uq7 ~7Taň{+]7;rtvOd>YxC=0q<&K]<LJ=R*Ĩ߲?C~&~!$oq^Lѣ2Q/i\묍 dO˻x!LB`d *j@q[CM! W,1-&8Oɠ3A537~@zkFr22EI2JR;̂Ջ̗dG.os9y] l뾕 JިcںϡB/3!BLvܳ^]I;tE7Y5LZ 9ȿu%hZRpM.D9\c -lu﹔0Vi@}5 jT.iWѢ+.Z;T)EqR( ;q?fwל/C WcdR. JU;]"|JV1󮟣S<'IyM@2 ! D?!yٓ<+dn7TIg)T2v2=pLЗX+6ػzz1N)to9-O6]}A)@Ώ' "{=> 3C|qġI)'%BTHQeςNa3Tyv;wG`@BkJ[n4mp,>)]%\v3M,fEzS vHC~_ۇקGq3z(:M$>1cŁK+' Fٲ,u=;,HT_R2xtTr0U[]$QvOq9n*CĜ=ָO.]: ߞGHɭjtg8I kQj1I(9U1?^l:%|אHc(+ "߭sR68,Sc .®F#$I* O.KmV SWNj<${2gv hl^& *7VʳqV ޳6Xm zk!> Gؽ<™xk|"z X*GmVn/ojw͍h!ϤwpCpĬ-DAe.R\x%r0RDoBvãDT(q[9 KG#&tTG$U!MZ|ñmmݼ]ʂ 0vdBfaA-6=ingeoJY2mִS_&⻯Tc/Iur'3.^"+uE*iymB2M'^Cp8Dk}+7b6e=L +9\$=tm)@p ҟ!^ oԀBx^N1XoMB ȁHgtr懷dxqRٔw-ހ$&i_*#@P̗1_&C礩(e DP#0E13C5a`P-%#jNC'J0`baM ,LǀӧT<·5'tAsW/#O +%D1B`B\^}ΆX7%'RYjCWߦ69PKGtwC 1a% h1\Sm/'lƲi32i"/#g"+rCG{-ah,nrX )nìUJ$3.qadSvE(wڍI^OpP5߽t*ü`hMٵd}l)~e85zxNz,9 'tmg+ T.wښ SnPue4^)G(hAefpgr4[|v).bDD/3%*1 cMLN(=aD }YJgqp ehX\E\̂\C"#%ފ_z*e'?b/-7RM;&TT'\ лk^0#c6OLp͆ʹ'` 6!٠j{t}&|zHj,sάf5=#y9dC[ bm+Jd6գϓu7IyfK&oD25;6L8KeD7epO OEd *v]oδ6֬$M1T:OLr"hx0r虎+n)Y{[6gёqW$q;%HKvO[(#+&nU&J眝>Z$1u.=%[a8^g=jX<ҧry>+B? aC-# Շַm|1zyR[y"Q۝r>EPk6\=M.7m^-v"683j ߘÇ荦=૸YEX&)NX5-"тRjі Kg r˅?JU*tkJiƛKkYG^69N&3;V|=C^Y6cG#0뺆XOy<̹2/tELeлxNwci͞pPݗs-5 M~gM1CA GS#L啑]ZtvϮs+Lfw,9P7pklZV\V g-5ȋ(S3k+*#^Uc4syUsE'ET,L[^uKEP~nB;oU!Q4[3]cO+zӵO ethL)4"?*8&Rh%I5l7 Ysanak EA3_2ܝB1yyB6D\ΥĉrO|b;@jhc~]UC*SsonA1+65xQd2;h̜y%iY+@LA#Puduibh]n)G#Bh7uGMs/JXp;׃ȪQ:3܈ha[@z$Hོ+ whf[pWٝ:X#vtdnjkqȡ tJgҭ c|zUr7MDj3>׽Q=L`9d‚Ԛ{ 5 LE""EL{cw 5=8)OshEUedWo Z0L͸S|=}皂eɇѴu+;\{D0O*ry&8૮#B%囗hϣelUh' 'W#8qN`VIENDB`mopidy-0.17.0/docs/_static/woutervanwijk-mopidy-webclient.png000066400000000000000000001355701224420023200243560ustar00rootroot00000000000000PNG  IHDR~mftEXtSoftwareAdobe ImageReadyqe<PLTEQwvƺ窱7Uy 3MdGk;[gCcZjFHHIѨ+Fc##$/Ji״%8s'>[KqA\JUjonn^21 %cKIlzގӌМSsk1MoܟeX`iz}M4Rfr4Qp扺nxdPa|g7o|Ĵ+>xMmǬ߹5]rޞ»+@x=cHk_»333@CQIDATx}CSW%!p D#!O4hL M V6ej o ᫂v43B쳟kcm xY\ߝ|Gx}5}8r_}W~#GFVɑ 7G/6QO?jl/5򟀿 O9͏ϟo-vO/?u'q/>%%7ܵ8>~o6>y#pM]{lDn\ke𯾶8nȍS{i{dmu?~vn3/ ޞyt7]k<>?~}o|~L|N]{!om :6}f3[g.9rw_{tbh/?7*v{q~mpj{?h?~iC{^屢/3s?8zO8?c_{k|wk4_޷_ N~}a_3CP~=6;}}]k DpuNn]q^vGdzC.N!:g{8wP{e_;s> aW{ta`f{ǙykͿ8~xQC9-~ʛ(|xw]\b_?B&k5?p ?\o wp\??p ?\k^;4nߋ1\ S9u닞xԨolpvML =|84%S_I?P~cƄ11ҟn7;B>6k@0s~_LsGh~~v^r;HLx{zOg|z>kӽ144Fҟo.N\v:fNw塡k]ok|l`萻>ӻ7^AvVќzLw#jC~᭾vO_~*0'OӗÏ㿇ȤFC/?{>ݮu b^3DL鞾,4;{;y珯>ُ㺿9!pO]ჱ7|Kpo\gu5S~:?tܼnuO|lDܭ:q\77* =~>^_MW1?G~ A[452Ⱥh@~{ܻΈݞ`E-t+G>=Dܷs[ϋ!?׃Wd=;bw/ #`}%0OL7i_nBLϊ+W6> }^m^kJ}ـשtI(?濢DnJ~G)V`wQP-_^{3/cC?![/Jh^͑/4CxN8Ckz_TG+9PîɱOuWpKî}'\r^R{}8?OCbvhh=A&fS/{+ ",L&C;G$T͗? ~@ [N p% 7~'ݟ}j)Xb?;;;`hGPMgu^4!\@HS7?}uv`O3-/#7Ǝ\_9J?C^O'8J|FQwf/y}4*ec^KztEZ4Ϋ/bL ~—0g1@J!.~G밤͑Ǐ''_3߿'6t6|x 7{XAnzyӁu] Ӈ\=opOlnVה[6-x=8ƽf͉WBp9?zg?)^lXsoگҿbv ,{D h H1K?? @߹,,|c;'U#ߵ~dы7n ?|٨MJV_vnVW߭^=wTxL||)m;~3Yn'^7?kvi_y糩?8t&f~"g1;5t/unrg,cC.}SR_*o+e}2[܃_#~?}71Jm~ Mݒ͑}W ~]H {E_{W<;#x/rÅy%Tz^ދ"`χO:G_t`îOOzL4ݤӗ [Ke)-oVl@Sn'nn!64tzbޙU7n J 81f迵L}0y0r 1 |cr9l. Q?Uny;Ǥ j׻IBٙwX;__H6@xS.?u۽s]㷏9JgGRsҁ%1)"v.N>|{KKu+|u[Joߎ{'ow O}t<;CA9~=6yvouM]۹ /yԞ'sc]/ ×r磳" `?hss(6wU͞W/s+JSxxjKMI`|:ep*?q?ݔvcC^In綇|Q}%bɏ8# b3&Kny|ڢq3sfWϩ,ޝ}O{x'?._9j]~ȫx/:vk}7c1 5ү;<5csKd( .z͝y%ϼH^ox9#$Bkk}lJ3׻t/_v1{>na7'@7P])c+kV#+ד߳ Z3=H%|}<8h/\<({'׻`-A?&ӻ}Quͳmw38u:KCny@vLo+]yvy _Ƀ?qͳqy>g涇l-xKޖ7qzzJ`z|xvwz鿢ywֿ=%J{K/p#gtSsh{T~wI׺Kp.2ywEq㟬}A=g~͍F g3ם$Dm0U@'ߙzb->,};y>/3sr96]K&;Z>Sjz#u1[/Jwfzd"WݠQ^r/+(]=ׅx}Ipr7ykgzh|^pNnOd{Ϟ뷙x|?9帵7ϋGם ONg_xWП_#k;3 fɫљr|—gÇ-#d<) gxj?vZ~Zמ;깈{JofTSP~C)5=~Yer<d.^wV.]̭K~6w~<7<{vn[ߊ{`dN^ a굲>_^暬$=,Kny_FΏϱ#H0 O25%~Pw>=h _xTjN  q # `9>?{>nBEx /u? }<Yfg?^Ϟf?x|4_d=į YbNn\$؎ߓ=|Y?>m²wxWt'UcPu/e,H_xۋJOT4}aU+Q3{F@f.휂B\^0!s ts&/-ϘW>xWƟ!*oxP7==:!Swc$5=gŝW5EiA>Á(7z\=7J=Ǟ\m܎7d^H0s H% 5t~7֙N~OrNTYލXnoo~ce?.\oK-HKrS/t伭|y5?OGowo]ml&P\ܧ;g'ek|zI|=:ߡٕ7L|${isOn|ҿ3WC;/fGo%oLO7, k{m}w֡ ߘv<-{#=,7~)8 ?:~i~2z9Gi=7y.|"wn|@}yC/ `j9Sr0BW16Oh#a)M Wi/vg#ލG!ҿfL/` FMR/j^V#Cޖ[(m?>\Ƈ#_.,/8D~URd-.*#Co(m}iwwÑo/Svj/`~!]0;<t&G @xփ< [ލF]1-w_ry>Ka{P-uvrv>փWlx!P=`Ʌ'/ݼ}¿]hts}O6$ffmoaYǰCi ~R "bx}'fo/~bs뇱ܜIz?N)/mj wK^Kc-`*sm# ]a:.>v/az*Ak^>TCr=Z_?/F~XzkF~wBTAJY͟H;[ҿ2q& M 𿯌v \Hz|pu ? H__.__"=d^;{O?~:Ϯ^W oܸO}&3j6TbTl"ͦWB>jz~?ZhFGqJeD]Wz"UOD\X]KfgVSq|y ?q8/%R|,׿EG''Pg_U}pMog;\Ew,+Yf3 .sŵ;Ym/u%DLvBPŎyy]ٱ\;b5&ŝqUY1υdǽn.}7'u-,uBuqݝRn|*kPdlK, ۹\ 䞘*\R!R*2+R\.;|N>Z$?pv<~U-E6=K% c&+$X7Am_ʼnż-|I1UĴ`eV !%$.R狈qډq>nĊ*XAojDB%oSfJ!Z^1UP:9bm\[6@L74~LM z U>-,0YK' -/n/L^ SK+Fw}#N^^ٿ^DTC5zX|m0h y)-n"lp ,C!MTT?ې*AM1-_yKtO߮2i $&焷+"9yOK`8GO.#G DofW_m^O#O_!.U&N[ D^Q.91ʎh A4nRTY)#Cb 0܉ :N' Cմn׽{KQ( @$[,]Y>lU>Tj Kl-XQٰ* ;Jyo0E+F|!:D^tye'C_m!+ӷԉ%6CxŪɨgJU[E+YXA u$ VA/S(q@$&WtdܥV$Y<6STa%-.qxEp<+"F\|j1p &$.(OWAU>64c(ާCCf>y,/iXJҩ@@GoE=~ \,PesۇK^&,S.ESa];i;Vh#*"}=Ųe D<&]5qdEܾ"x%`ͅ@jzJ=H`1fUԻ7.>r1vcHWP2Ҋf>g]&@,C= #I_Kȝ--nO;4O5(# RQgtqJ)tM3yHQVLg B|+qȩDFw{$\S$avZT@ \X jUA_? ?w1Y1[QIȈL DBJ%8'0NԐlZ+Ώ"]%M@8/h):QZގQx>VB oX灡 mtZ!YNñs~S+IGI7g}fI- ;Pꈟ7eDyܧ%Fa+PN dbH,L+dthb2EEPWET[EMo5 bs#]SV"w7ӐPbBVN:<׋b [ͲBtJY(?{BĄ!F0EI./ʳh*a39 EQU+Cbzkt:r!3Wԩ왶egDǃL:b c1I)1a?@+EH$]*1PtE(i%ӽ޽%~j"a WAW埾L$W\}Y,J~bx)hwk|nl:9Z&p]ٸs(-~,qEx Kbw,g#<]!wwr{7VEvD{BuI Hwbr'3;3 NIPAK#Y&[)%C\|V]]k0Q6M`hjEodZtz[YWlPJ#ဢo7/LIG2р,=>ig { BE>>CEJ|2qfҗ1 d+D1O}H:Ҳ-n`D IpH^YD 7'e%D>}|>o5\.$N/HtS@+mK}p瘕cY<`ڠ}bD$Y)3pI"oC.b(g:0lJ.kȍhv| Slc~ĉ눰$tG$F~傁,`7}%ޒבOT?m>b5 XZK,- }P$^?\gu"J]RȠa1MY\H(V~tyBRM mSE?ZtK`H ibBPUW)Q@ ߻U-!oD^k}!B'>|,{ H.W dGEHC sysU"sCX iM$ܤd%HW$̟_®=:"υR9w/&$b vf,'է%xIZa8E$/ZT\I;%%cʜ_Ѳ|Z,P_B |4mΚDA,ؘ> X"fCAhⓐPr![CJ)cl|Ewej\>cLJ'CaCLݻ77+_0%}>pWEbV#'½G hٓ`k "Z*4/K[EVM&(j j i.m6oûDA_t?LH2*"u|iuE lMF\S{ % 1i$zC_H729PwϔOLW3e, 6#j>spB{;3a1%Q\6snذE hȖ%V݂ k+c'Cz\0ٸ*tڱסt;?T|n\A*?53~8%M "Z"S \ؾM$;hC<{ )sJ`$CSQ\LhXQCsEJJF!aV& 1ܲIC\J$3 Ovf'q"R_.Lx͔7ц JsJq*סzĽ]݁rn^jlb1yXH/F<$nu˷'Jg=ኯ! Y.hVb,fZj1.dyq&"D[Y9;ҚKʮ/a΁)Nt=Y*#L]ud+Pfオ-pIeݻwe1> TVwS _i=o+01Ŭ3VSTS+2ިuBe>Į@XP)i~Q.!dG\vCK ~Z!\. ln&dLJnC;v1dv8タ|occ"8b*a| DZ h`Z,s Cq"Ш @j(D~7ӹ+ ]aF 5YX,`!si)+&BE*GN,[bEV_#B$<բ@kQ.$*CwkKR>~&'!( Ǣ?apFKC@~ _"_e^u\7 5Z셫H% z{2y?;"ٹv fћ[@A7@VQ-E@.hQ /F \k8R|N'3qo./(V $"\W F*4J6FA42 p:/ޞ`N"/ `eڈC3L){WtX>%z$ԀK7OsL fo"Ba[ i1<( pt~4ȷV/$t b!P(:8Z(c26 WW\b}eZ31!jab%p86UdJEHmIóBr~1q95i2XHK`+nr!Te|wLb83YjqV+>qAˑo'ߧn/Ur!Zj@7'$KPl.m "S* ƤAةbaGl@)_%EY|>*RQN78k-q$٠~XHn!p %q*\YHAX,[!IR=6KdIb|>sUPU7ׂ0g. ]Gf2=Cj!6~-Q{i~ޓnSjpvqkkq<LdCFUS@ ^l]?yw{VLOJS;i>j|GPC& <rp&b~V2`|ťh&:G[lS@#<8q` 9Oh)>f~f[㋣ pz`*!ʿ8JĆ$*y4d{PɌp0Z(Y#1ﺸoI:R縢ymtKcqehig('J laC9Xd ).'C->b.q՗Ll+_6|bqH :y"3( Me}JsXY %b,σ_[8ɿF,&b#WV /=8o)d9fK"5.־NWv6-;l(r"~XHÛfُA6;2IPZ2(fUi"ΙpHKP2ϳjttqh@ /?!["OJ $'a!*-g ;T Ң]Z?2'Auo?fsz/? {޼ ? C;tZZ"W|LPF6=_J ,5!#YOᙓuPZ(2Vu1ּ`@D,Xv-ѬכMylzG+NHuk PtqS_};vi*E$P]/A;Ou9%E0O 6MJ J(*Ys7N)YzK鿠#ȭ'ggO(@7E 5 %pރ' KD&>qNfѠuTJ$1DlbY,w7&~78L4bU`ᓥ@tр SلW|pqh rNf/X.x`L'Wf- S*̐I<[}Κ ?L~r4-y2B(jMi>Fb+S%+JRFs:/#qѝXV$ʢH DQg.L&PBCSYeUg檦+!n.i!mW.#J@nxhZc̠)M,.ҐaulLm @~/|¢Rl6'>#|:ft:iE\cH5vT!*4; ".#\iԓeI]"|jl*4j |RC˲ Ǜz>>6JhED GYvV$x\1$ H"NuLk/ s%gu8J8"8;C_'ﭧp૬( x-;`'GGA^hUd{ -+ ҃ż GW 7a.iI 3t0{$k4Q֒ Y -V),vFXVb&}yd2 #R/T\ox 򣏯v?ms$'"3 H*?ù(8O H\UmS QQnvpPOW3ZEe,`BrbUW֦WA~)"_UQnʂv [~:M#ϱ< //ḁH7j4dpK):,JcC!-#O-uHArh%#_̑l! $"G?kϷ̤< E*Z'xAuuael9(тIkd37r >DasIi/8H@tZ Y{RQ/hI**` C%q7)tnKat$yq}DZ!ݨW'o Liic>O99V#S,5-TUE[.8"X/."͎f^ALhD{ɘľJH`$ hDEGI^3:A ci'G8W=X]v"Ad@lښ$gx$t|(zn j?{aL$Ò؏$7LG0J7m !{"ih |N) .4 J#--1?ᯋO"+l Д ^"6|.2$`f±6_ް0zg 8! 2륫 Hp[iK#"%k713>L;,:ߌ3M  sRUrH[pw\ )k*8עZ!$_yث#zd%pM=+0L(O)Ii IC1"z)9eѳd/E2i16QxH!m{~k4@NJaU*c=APe*"Ć&YGG"H"sɊJr=u*ie[Et@KHXX%@r I/0c+U;1PI- tf3`F-S5 T\w"| ffkavC*(9B@tb"U4g:'%4SUm0Irٗ:w89&-D$kZ<3d]bW ǿ(7-Z2S8 x&AV0ÚV1Zxy- qV( `VfSr5wR.){Z^VkL϶r[F`STVN /ؐw͡5Z?zVTgE~Sgr@maI}J#-mh> ȉ%1͝^طv~gjUW.\tɂ-KrF$Dl*$DI&HZ@֧ѭֲdJ_pf (ofڠ{B9h> TQDKP u2k6/0MdM*[a5jq Iȸ5@~}`kA !dv-ʾc.-V,x>hE)lʤ9ZWq@B⡠@B/V@߈%D`:mp!SV2HesKȇYl:8F#VJ`+꿢e\'aDto%#Ցy|[8/:RUEH+u#f`cYp# 56 hDVPF{5B8/|l73i!| A ~߽.r&y|CDpv4lCGCf&@,<|R" 4>h ؃wG'eDc:shB 3X)۩J} 9R. v*${NSg)]׃h/}=F)f]b1g`LY/\sԋR RՕ~C 3.}<6*-O!:)``a"SIyYCYXH9z\/VO+ xipnIAL]csM~mă Wp*bj0$`Zj`MʉLp9n5 +'p=}rBx)/ 4U~lkfyF'Rj0iieÐf%Qo}fQ VldaL g;`.%Ǥ&&z&VE+y%ɷSG $(X1QgGrt]9"B2qWt!AYJlTcMw̹4>6 W# ]OR"*m.DδЇ^sms1Oxn4M9iy a3?"@ͩ#3R5I*I̵`'$ @({;@A7q_6$@5T N1)8^xlOPJqRZ',bJx@e=$62%> Z7<ۊ8Mk"I; _V%PCVν IbD%dS#df(nB(&L aÀSN˙6%dIhi{ApA 8qHy01\9r e 7ISŞ2{%eFSk-U mpG"~& 5A_CjU $R)4r+% aWB0AE ׊#aZ$9-*`rLor;AxдScWJ,p8#A;d沥 2IfQ :̽H&j* [o}u/STM%g$Y2`h4^ێ{IO&J5C6@*NHlCdQjen i5Rv'M #c0A.S d- )Зn2S>/c.襎*S)M. زF9QN;1ZF  V6tz1}e؂b?!̺شZ1dV3HlJG,W S"hq/ Ӏ yf rf̺Uc9L(&BN=3KSA _V{K@G]W<`63emGe2P>LhT\.GTMGmMQL cLǚ4C>-՛oo5.PQ1U J~L*%)' 4i>* U["덎A#1G)O$Tr(x`myYŴz4 9'gBU]4#&*i'+N6&h{)En&xZ @¯,Ѓ#=̦0z='ab` '9,I`6!9kvNNYlb{6^LqEYmH(,ֲpvz2 *=B0"5az ˾B,(C(6KMae?a1ؐ\ n!yjY%),LkW(V|ܭbJ/0@8qЋ8y%YO-8@#,A#L(dA/ VŽ'f9xd 'TK9ȩV\J8HQ \I5J"v)9U盒S"'72-IN(_ ,hƓךvpoc,7MyϴQMsїC=(N1CPt <$s4JdIJXL6 O{]3/;G鐹C òvqڲ# TMGUU mul/+(Hϛ#H`.)r<WV^=:,ŌI L-g{>iCmo2  }$t9̷TNp?G-"I\ļfd`h0`qpFh(Qk8&t}(iF9>3-uՔk(v[@#f |iW+҉:ub/0in~q4;%Z"&R,vm0-^Ң-eǥ$.KQy^j ox$ ɿP<2a\(3уA*2yAg0D."sKc-&gP@t8uEL>4%+@xUO 0+8Oz T&_0& Wmt+ldEmM0t|q3.8;\Jf$Oǃ!t^x8JkΗB'to$ea A~x'x9}Y؛f>UYg]OcS8iPb <1-9u: 0t'yt;/=4E_&wlOWI/iu/ĮMBnJ+ \^o "@7y4iuTl4ʅY'+vQbH-u*͌ <<dee{*Tus%${ #G1Hw@S "rӊszP^ ,Q>ϏIcPk%^v TUѶHcd-R -y SMM w-EO?қy&a"gwj-si{1%`K`99'^1Tc'Zg΋_o^-b.mTt~cvyKvؤ#SA3(u?¬ H:X ~A*aQ+&seQz'bs_xdN/HhHJ-6CLo~NIQ+1If`vzUDLj59=ER|zgR)"z``̺G+KkBsn3♇VA ]_\9#'lMJb{r]ܨS4l]uhB[oviҽrj8KWhbA Fǟ/zt@nCyDN'[3+t#PCp-`˚k&ҕgr٦CAkUPOg'`'GFjPP+-*!@k k:4Q*R0*1NJgf5Mq^Vߚ*fE cw<d;Y,P ~aŞG8w0[,EH_®s .A0W.\m 0tZs OMd5:R܈ec(`qbj-- %.Aֱ[&L Ažn׫Hc\5wT-鄊qD r4; ñch'_O`D/ ujsz/-2 5l `Bie 3eV D'UR̹%UƔ x(k#&RqR%-˾BM"1M"S#KpQC35%e4Ӿ4+yk~‘&$*8h4 9G!+~DhzRd`rHRt$ZCr#;șA`@4!ۈlM/w@BIxВ={tGP8>!bgըLqM-}S&Cen3p.9cEiLI}eOώ]啳NCM$K4^WzTypIi`޼z%֣mzg'^RaI][x@CedݫeɘvTwz|PJ&SÀWTlUҹPG/+:>-6&GcjpT8Cu^o6fÈ%!5:JάʌmjIGAS*;T~mtZ .k? :){d4y ۜi*i+bPT;q)ͅɠY1'S\1'_ך׸\= [vYl厶HZg 3TSC<9"jKH-Mg^̧]80D=X[xl^WB`cr`z6BdSJ!FNj3n Q}C_WRzR{rZZ)15Yl)sId IĊ:_Ib(7&NKaS©4T>HpGQ'zYkh,RGTJyzgO)IP1'\8UGA]j>d &TOVڎ/fAoteDk8c\I^NAZZq5 []0t)uINRXѓ 3'S13 %~ϣDLhKLׁ?;5PJ̶YJ-mfs3i\@]T ?ΈFĪբ3$c=FNgF# f[wlx%+ΝEL6v"-s¬K'CpzDeBuG=__y^(d!#ni^`pj5Br>,õ+St  z(XG\1D߶ JKkGB?ZPj+V j%ȥ o'be$"֑ZL(t:ioȮYI' J?(UL8cTƛABʈgw43xzvUW#ޮNL1f?l)jOp Gn7e/j32|nw:!򃫅x'8Y>dxi+<7p} oSՏ_ޫm::3'Fh5(z,ym%5$ޖq >@e5\V ,2||w[dx:<^h]b@XTRx:[10x`ΊBM=J?1jk-%~eW"KHQƩ!ar) sN-GXPt'^z;1Oor@ʉ0b/dknky4/s*,E|uO38d7Z_,Tgގǯjؕ)߲ufRFSCgz֋ŘTޜʡmJ^Ml)[7[^Zh4XպsvLX\?\69:0GVT]G?;бa55[9)Sq~1 ?K9iS͙+-S.@DIHX)^o7=8Ma 9 Q .;FYtK]=w L]?BMukR*s˅@ dY/qB%KZtDu&(a:sh86!\L͜5Dȶ4KbS}]?7O{nB^3P*pZ+֓qHv^4e(:I%oN^rQpUG/Ug*^cT^r/k՘0NlLGZK{4U3"qTT"8J%0@H~|R3{Y4[ࡷC0K:Ҝm(SJ}CqWSF?U\X}_U6d+!ɘ0q=)US& wʫa@dlMÃ`#?Vi#H^'Ɲ j,тΝ_[rogGdPPX ӄZ'}54{J?2@O zP'K%zO^v$K"C1%# T8'ѩv:=SeDMjE^!Τߘp9ڱX&RkC"aLDjC(jomC$lNr[X? Յf7O&d'Q.wĀ(T>rgB&D=N>*93.`:J0EʑT۸QB[=3'[Y$}=0>j1 AZv?QFmdz|Y^> 8 6@'q-62PW% )&>:'U1DC6]'gխL%}.;+W||  ;=O)jbǹu|4f) mI(A3%NBk~{/ =]dzhv͛#ޔ!ԙWI.y:lU6]]Q?4Ûv/N6'}h G<1f`>Q\IbHiԤMO# ҮXê!>9}nݪSi%{ J.↊=>L%IqeI4.K_N]DtLP[Y\G݀b.w k2]{^Y<۰`)md#A=- !QxnLDvNAnrifeZe>$:ò22Na]hBއSٲ3J%+Ekz🊘56mE hS_jv+% s:ZhL6vU[vߩE6x\!K+ӠPgp􄖼N&Ȉ͂g'̈́ r9bHd3h6k`ӗi .K0EaNqz?m'W( K/zMCueic(@d ~H<$ cAx>VJl`^.NQO"{4a5P}̦nE_-2"'Ft"vb\2Z1 GBxIM49ə$AĴsI|n;6`p]&o$mQ)elZ](s~˲o"#E544cX\B"Z}Vl$IC*В|C7 8;дC;+8OSs}/&ZvE*3"" e{2pVݚ~k˞Ҷ\@]II324|Бi HZ訴A%wJuQtcccܐWlM޵Hu2]'􀵟=V*Sހ/Fϰ(/Ꮛ`yr8p}}  8I 16S]~LљHcy~dE Jm*q@ A \wu0=V]V3IY}ъ?Į W׋?ޓR?8)ixBuiC;z;6iA,}/5x7r0+Pom!HTmbGkq ԇ秦i!2|#g@͸kޑX:_{YtHL`Y!PIqIl*e+ P ?Cs< ވ,GWQdC4 ]tP w[);C0ҔE8y#иZt^ߩj: R>d0#z"$U$L>49rdqhGeڴꛤbSFz:ݏM%͸&R[Vk0v^~̪OY.;m%!idI\JnAV?nꨛ = /_3~t#k Ƴl˥6x%G 0X5|]v- Pܓfsתdyn朽𖝑=wiGYH^ȋP̓w(̒<1B䔾W[e󄑤%NqR_^# IG@"/ +l a?QMܘ:5yWjJg1 ȩ% {܎{#b ,n/͐<9?DSi{-}|yj[ <FM6#sjJ i/};zR9q ) ,k}-؋0(l\6C._ 'zr4?]C61HN/JVsIQ0upDcg@`Duev˲"(jcg~'}.\ͲuYJڬA?Jykn{뙶Fxyzյ^D™x썩+,j2CMVZZT g;>5E -,П$&Z5P ~^m( z8Azuch̨*GJaIKHRkk =ѥg6ǁ7u*O~iۨ8F}xgOv:;Ο?%f_")iMSw2Mw4eƑd 1-!9HK0 H݋*or/AzN=Bgpd˗~ {<(Ԗ7ܸirҽK4S!*BSoTSXFOLG_! E"lP|C_ٹ`)'L7ODcZ(ǮJ{xVg5 ]Usjz;Ĺ8>W0$*Lni{cvWW=J/iL=.H,9)[;޼@9uv`T=TyF͍zz?N`zIӎ$"־fD(>`ڠ;6k*{AN@_3ÅdwAuƋ]xzF`Ne =eZ6#${\N -vRS'SEȕm4 o='RSK 6 0 *Cd3qtԩݾ~ˠ+o4fo.h8]'0L̒}if, eel ʵ;Vf5jd`6Ae,i7WȻH?|=xt\:;{$f7fg|tЇN;174) @5u 3&ftK܅ӾNcw"/-̮ |kOޕ:'{gi @&\iXQW/tЩ*?5b9f*?Ч^@{n6²$~Z#B Rg_OKj9QBx.J m^^gW!M/xfg[V"ZOwUF+3ԁ[UҙVo9_Wy@# _pfO>p|8MHTZ[",sȰZkDQ5j:AmbBx(.I/6Hv c뛑f{~fɢ`]4o1wZd@ '|Ԃr'~W .J ?;-Vl T"8#ta#gիKhmVBi" ҬbK,8FdQu jy{ja } mzvc{fV(\eJDZ[VzOiϋz ~GyӖhU^|78t'~U^Cd}XE;U5;m>ٛ^sMӮ\s#ɕ$/.VmZ{oݶCefu.܃8˙O[>Cq[O 2N߈&Y*($/k[ސ<k u1u Z )'=DQaԎ`qfw<|5MU5(e}ެiO Yi*qjцh~0<9Rs2l2?MŧSFa>:'~_$#YM G?Y{w:5|ڎPՕFlz`Ff$?~/ i"*ImlI`ب5~xim]:{!3S^W2vWب{+%{MmhֳZb~a?W'9"ہ_/x!D3`-5QlxNpWk)gQw|X%aּ䓝[:Gvww ?ARe_+m9{;_ WF}K<9s~4C2Kb)׵Ub9c%c&EIp*YGf 3өmԝ~&b/v`;}_=-kEKn B&ZIge:`m0OB EYL c>{S^ҭ0܈b&\}r?T;s]YaW$> }0RXy.ђH̞qS'.9p$ʖ>aGe Nr&~B|DnIwA{5קv8~xy3&u$Ȋw8vcTeI9Ď \$zr=l"&9sQ 떯 /1<WτT1Hz~@y#RyᏟ<ڙ~M?Xh3=һ~c8ւV?V Ow,L"MY?:WyR o B"Βj?hCC:WvV}jiE s$XoPeCR)jD!,]6{3kg<xz{ylussgua0xc@vzpdL*AfU!wu+_/F&Bc؄y74|_"?KƱf6W#&ԉrLvOvk {b Oe3r:i~'#?U~߸jHENOVv[Ot7(Qʑ0fL\ ~ϱN Ÿ'Y?쯳jB MM4*_2Pؕw?6~˯GY^|0;LCp{(mC.$$+ ;x|aayKwMdfraJlHkt2'v OWxEMk`8Ч|h& JW$O#׮|{ {~[{~Sno>݀ s$*|5,տ.9PvrFUu1!e["'avyǨc3/%hgD;7Fy3g? "ӏ6x=Ɇ5Lg =.?G Q>o|"ɝʞ?fif5Uk㴟-XPfu+Uf" DйsZz1}<yyKȻWˋ*@WS 6 ݇O]FfhBnEMo5~keez=0!`PͶaC՞Gı5^"eLjץQG_`j)m)Ϩ?H^S 7X'$żi؃6\~Ĕ_tX*`yh:S oYf1k)SHO|nRc ^JGU4S֨](Љj=/]R@y3>l`YO[ME9?4res\x%?9e۸6. t3iM*WM)K nFWՍhPUJC43=61T _t}}γwFH:sXpTi,MOggVßMh۹T! 2EdQnR=S\flAd\`dqwlG\V3<~ ](\ĤRG?bB" ϫ *Z_ͩѧe8 {]O_8IӃ.\5>(zmTe Fysyl㜘6ù">ǡ/>ol=l3Dl2OE4]'5[jVSڐ4Uk-f:)+Zl^9]^ oA|;vA/5 ~gMH7l|cacQYVCbl|h M9R.FQ9U/OL)0 'I4YeٱMhT3z)Q!݅-[f84~tY}VW4M3'&nleRSړmϝ#Lo?H_-Mubms53yTy~roƊl>Oϓ!9Io]\-@8!=s~ TBiO fl'~Th 3Ul_ZQ4tP /9xxͶ76wQأƴp@ZQީO%+uT렒鞊i߂g` XF;"IP%颶F"}{H.]aS'ybO(vhcd7. o=3]wnRsUxl6 ݉D4^:Qɹ{3O}SJҢo؂x% N%Wx+l5X[G57L9 V\ R6_*̰# [[G@ŒBlc=E@nY"4>0N lD?{ư!e)UϧSr#ydgg%؉Xo32t̰t;8805ؘ'1Mu=sMm`zQdfRcfg{ g!hIBC֌G F1 *m%J4FJMhr\$ Ӝ-2R M#'C4@q^<>s7$͖M\la{ EʂIAQQ.Q}%n65jb2|8(=UL I]Dmli~LWмio>{G,OIApd԰tDl( ?`mmNzZ~e'joLO% ]j+f7*nO=@7Ufq,\# }/=+T 6E]Єzf~!kp*Ҷl!62 _!*:\[3B#k{g-~8~shF~^@`fv V` r݌B(X' )AeB=Z]9;Ysbf zSzEf"V&=lI(&:PȒ pf6!i[Ĭ)Zfo Ȥ'JM& P7Cʍ 1X>ק#}עWpkoo3? }ߊ\$~'OQZQz^tf,*lyfHeA^RӢ%4hO@O~yp5u+|3lJNmd eU73sgJ)맳G7Q-3y?ajn[=zk2Hdմ/hqjb_^kB#|۴`Ώ/:(&=%aN!_qr[3OK~RUo~ʇZf9>p[z6dƷLz#!kXh۱twbo %=ͤJwBtiya~-!Yjzώx?ӧJ(%cԯg~]b|%([dg15u~_"~!/Z_@\[i6 2{At"$t~FTwr*#[$@&y( ZuGbë U}7ZĹ q+F*IlrL&@iZA 4ʯb^&;kv)mk[[WSc V[~W;`vЏG7(SLk@/Κ )?)&RͅޚǒaxZh}Y8fle:|E~@Q/4#FH@ U*i&޾>'_|D;x<5r,\ *ټmz%ǽ,Qzt _.~V3E)˦D6kY& R8yE䆔4M}1;(6,߉dƲV;rvLmkkx)/@Jc+!b.d_GsZ?I2[v`B21R|w#HURe1کlD ?ȃ0Ul1X.|- O/~ y6YDۡ͑ M'b*؆S ,j5'FĩOT m=Un67gN74$05Ϫ<^>(7"aC% ۙ*3W"cKM^HAM9w(?6fxiCn@'>I7F)M$4GJq݌cibr zK]?|#p9s{G>fB =Bс ~]~\ Tp N׾oe, ,D=^~/[WjROQ[g᷍SBhlJ頖62‰UXljb!KE"L o4h;FRR;װ)šdo<@-qɡ2-c|+v\sSgj-'Le QşCR$ .e7&%d] {PXړ;@D'?+J-LM.c&|ñjKܵ'=ƭ2Z<Eln 9G6db]ds[B` esA Ӷ N !JŹո=b{?U(ΞsX/TJ#_j3r:aCj mjᏹZCCi2ByMZE;;;\;@A꼅ƨ%:+mlDM qW<'Ƚr'=TuEgBO­e tϻ2Rֈ%joEWw7EpR d}/cg Okh uY8y<&'j܍@3CVVq%2*"$kP0yS%*_هTT#_^LBRA<ƣ8B ,K7}'ݹ\77:#k\z=ns!OӪ!̊t4:E Fj)K -ņ z&6VpTE7w<߲Bf$I2YY%K(2IO6ju͡Sb6ҥ"7r}rX5ډ<hr]q^]4"m!,-C՜.E ײL{(dbf%mXom]ʪ E6.CN hn]]1vN ow1A/U&VHjGT`[Aeu̪0ioV)%HMF;@zti_t1v^..s~.p7P 4OijC( Tnk)O ܥ:* )Sn@Opإ Dwݯ/h^{vV&4Pb\>:~,*ݏ@li*s08V_ qCLnHmO >|='G7cc?MȦ씟\7F5xl-@ 3&8:hVi)H_ej'p|ruXï%夗} ۽~ssZ}b8{/!I?Rb ++4_# DY@Sn" nk!iS:FG3Rt,NH?H~i3p!DVNVTLkz\kG8s-Tw!C g-q!t d3EaÜRo+~K?J^b2ߞ ޺YF_dZ.cū&3_D+\mJnkOW q~pzşp.ZN~ڨ)0ZncO|Pxǂ B8 ӦXב4KWYY;M0[!0U&zr#2T݋~~mKciBmKoVg%y:Ye|X:ϓϾ!ǎى>RI|2+98ݚG[?Z<E͚:v9 EP #CNEu Sbv;XQ0n8 Xl $LC)seo~Xq EJ} >Pzc(P,R*9t0f =奁R%?}YQ+co rwe3דy~_I}X-5ة8bgي %VI4'b@2ߕUEM@W DZ^~p(O!߲^]ߨ*tf՟+Gs*[V"r^~!ibF̠S!4.-FiZ=_P>D+Pwh~Q2\]q Z3F8I Sg!JKyY™g9g§w0egr9Uث+|}OVYE7["$dZ iyxt\˴\=8eTjq >Ñkw~\Oꬹ z1ӱ_z4y f&I^dr -޺N L`dfr/?v.ՌyFq3紃͝9$)4; ?{~ O^e;Avf5%Gw >-Zcnħ2>5Z(NZGݹ3\6Hpymrѣ2~Mt6YsJ Flf'EtEs9[[[E@?uSqH |{yi!,.ɿڝk[{B- A_7>9>t?;ʁϋ>2K 2a9.^_7u=򩂈&`Y1ɩQ--u{bz),Ԗs>K^ A=0&>ß`3a_SR.W(Fyb2{_2;CT`{$a.̛ D!&]5L- \Ëj{aZ3¢aob6<ʂ2>LiV?`.r5}{fxW^ ׹QiEiEM\#@ ߢwW=br~\OvJgν,񹆟IAqM{3s^T_h0%d!&yA5Ē\S!7IeK [O)?ᄿ'AA=ǼٙrRzDq_b4y^"C%c/8Zt_ LErCb6b?p-܅t_Y _.zKlW;ѻgϞguމ@?2PQq=CrA5J͎y"O熇[cOngzQ { sW'-+ qN*jw挊SE7bEZ+fH>h_:yF^ .ԓiKz[#0/N ;>[t?V ><)ioz3ʦT&-iڷ =W [2w{JQkBym.%|iW/Us{o߯f P42aΦZtIJE2L4zC_l> 1NS)2/fz7a% Hl:[Zs_jyfU4]^lS=v~3y*~c4e~jO? YMސ]CN$ҍ@U^rԁj;(Qq//z" o ~]m5O~{#dыc/dis#qeX;( QC*..~ڥz=U^C`Ϗjy*[]M)oo;=(,+O`8j}\>c\ޘ, ; [ `>ïWmU&:d:Uf0,VAaӖfN>yM]ڗvct /s>o|g)#?MOpkFϤjAC=a1nTxG}EGsk-=!箮ˊ,Ve[BHOCWU*{2dˮ_^!^Q͗oX7}?{RbH &aDϺ72B׃MbVnd\\~~OwKߑ#ΏY)͟,+aߦ[#5D$owj޾ Bӧ)S'[[sAtۅ? g&b-OʥǴS*D77 Vƨ ׫Un$W0 UvO/qh':pE4bb}v2N瀽X3ʆb܆5+-u6o-UUn}zaw1*qg+^P2.J MMӇOH=u)aUi8՚>gբxEuC8E.][*fZڗ/]hԪIn:vy[e?mmoGOGUu<^؂}_W>,r{HDql tg`FDŽQӮV|x+Ż< 2+so~a%cllj#&118uUш 3ff9jͅ)T㰸X6Kڵ%&*`0vcr,@I.ҭ\=2Tˬ9N{Euoѣ ѥ߾׾~ùwk:@qЅKRo.$cܯ`-0eQHǞS0(WZ:Pk elqqqg1J= 7}rA 4>ڠZ F$ wBz S2 /Vjv/zra-L2Q If&SxtAuCG%~ZM[4m'v[OVp @c/#,a1l? i ge,+O37 ?SSsm3@q6!O0'ғzD %bৱZŤEVee YnU7*~}PҹG SSυ`E#}rFJ &qz(' \TbKLDt|d{7]G{l`(#qƾk/y5Niqcyyel?^vbL~ X۰uU_AOaFƫKg{1d]SdjM}tL- ۿK&]VyFdjL}8 ~O𿧷޼{tnM](yL+/<-TA@b)4D{ Y);4y-bUC-sN3VUX6uՇ4}ß) k}C: h܇mHu:|OK?'gҹ^[yM:9_ 7tl9޳S\{8=_Ο="BϛŸ[;η?t~UAġ\,_>?]Uoz7&ON I}ƍ;˾SXc':&k.t&oW ŋ&WUs|~>3=^;/pa|+7r_?WK_3Zx;GITPԗW'>:Sޟ/LdgW'OWOX!l|uKd_?%{:WW3:Sv>XTg˫WyTťkBŨ|}yT}gzkjtT=Ol AzI)w?zF 迖 c:q[ŝp%'&U{{O>S>{Ta6S}}_7W`z;%R3:wNFu<;/>ii ̭x~H磫<{+8n~geV>|KE^f/@]Lt~.-N!z˗w*_t.QS/]G&_} _jgoTrjtrqkgz^/o;o>{|挺{$wst$tpj|k$߰ywovOW>N?O?O?OO?N?:-r:Y'~srë黽|Wnw/핾 ' ' '~?}ۅ'w= Hk~ɽῗObۅνK8;OKпqz>N߻cg,< {cs# 'Wq?=_=sOW}>fsw̙ۻww$/Ͼz '~]?w҆ާ}j7Kr\O?=sW鐱 [ /+> g8?7?g;9y6῟/5(ՏM@чw 5<{~c0;} Ǿ?۽9zkV=?_=ާtR>߼p{1)|w_ Q*wo}p>c\?W {]8{[gi _<}ɏ+~ |Oz.w?z@ωyŽwFhnmHA ;A!n}[>ToE}9B~{QCPwa0ï6hAE_nraw+;{3J){ɗIzݛ/SynѩW7)~G"Uz\TC!/ycwa ' ' 'O׻`4ջPIENDB`mopidy-0.17.0/docs/api/000077500000000000000000000000001224420023200145705ustar00rootroot00000000000000mopidy-0.17.0/docs/api/audio.rst000066400000000000000000000016271224420023200164310ustar00rootroot00000000000000.. _audio-api: ********* Audio API ********* .. module:: mopidy.audio :synopsis: Thin wrapper around the parts of GStreamer we use The audio API is the interface we have built around GStreamer to support our specific use cases. Most backends should be able to get by with simply setting the URI of the resource they want to play, for these cases the default playback provider should be used. For more advanced cases such as when the raw audio data is delivered outside of GStreamer or the backend needs to add metadata to the currently playing resource, developers should sub-class the base playback provider and implement the extra behaviour that is needed through the following API: .. autoclass:: mopidy.audio.Audio :members: Audio listener ============== .. autoclass:: mopidy.audio.AudioListener :members: Audio scanner ============= .. autoclass:: mopidy.audio.scan.Scanner :members: mopidy-0.17.0/docs/api/backends.rst000066400000000000000000000017641224420023200171040ustar00rootroot00000000000000.. _backend-api: *********** Backend API *********** .. module:: mopidy.backends.base :synopsis: The API implemented by backends The backend API is the interface that must be implemented when you create a backend. If you are working on a frontend and need to access the backend, see the :ref:`core-api`. Backend class ============= .. autoclass:: mopidy.backends.base.Backend :members: Playback provider ================= .. autoclass:: mopidy.backends.base.BasePlaybackProvider :members: Playlists provider ================== .. autoclass:: mopidy.backends.base.BasePlaylistsProvider :members: Library provider ================ .. autoclass:: mopidy.backends.base.BaseLibraryProvider :members: Backend listener ================ .. autoclass:: mopidy.backends.listener.BackendListener :members: .. _backend-implementations: Backend implementations ======================= * :mod:`mopidy.backends.dummy` * :mod:`mopidy.backends.local` * :mod:`mopidy.backends.stream` mopidy-0.17.0/docs/api/commands.rst000066400000000000000000000002251224420023200171220ustar00rootroot00000000000000.. _commands-api: ************ Commands API ************ .. automodule:: mopidy.commands :synopsis: Commands API for Mopidy CLI. :members: mopidy-0.17.0/docs/api/concepts.rst000066400000000000000000000063451224420023200171500ustar00rootroot00000000000000.. _concepts: ************************* Architecture and concepts ************************* The overall architecture of Mopidy is organized around multiple frontends and backends. The frontends use the core API. The core actor makes multiple backends work as one. The backends connect to various music sources. Both the core actor and the backends use the audio actor to play audio and control audio volume. .. digraph:: overall_architecture "Multiple frontends" -> Core Core -> "Multiple backends" Core -> Audio "Multiple backends" -> Audio Frontends ========= Frontends expose Mopidy to the external world. They can implement servers for protocols like MPD and MPRIS, and they can be used to update other services when something happens in Mopidy, like the Last.fm scrobbler frontend does. See :ref:`frontend-api` for more details. .. digraph:: frontend_architecture "MPD\nfrontend" -> Core "MPRIS\nfrontend" -> Core "Last.fm\nfrontend" -> Core Core ==== The core is organized as a set of controllers with responsiblity for separate sets of functionality. The core is the single actor that the frontends send their requests to. For every request from a frontend it calls out to one or more backends which does the real work, and when the backends respond, the core actor is responsible for combining the responses into a single response to the requesting frontend. The core actor also keeps track of the tracklist, since it doesn't belong to a specific backend. See :ref:`core-api` for more details. .. digraph:: core_architecture Core -> "Tracklist\ncontroller" Core -> "Library\ncontroller" Core -> "Playback\ncontroller" Core -> "Playlists\ncontroller" "Library\ncontroller" -> "Local backend" "Library\ncontroller" -> "Spotify backend" "Playback\ncontroller" -> "Local backend" "Playback\ncontroller" -> "Spotify backend" "Playback\ncontroller" -> Audio "Playlists\ncontroller" -> "Local backend" "Playlists\ncontroller" -> "Spotify backend" Backends ======== The backends are organized as a set of providers with responsiblity for separate sets of functionality, similar to the core actor. Anything specific to i.e. Spotify integration or local storage is contained in the backends. To integrate with new music sources, you just add a new backend. See :ref:`backend-api` for more details. .. digraph:: backend_architecture "Local backend" -> "Local\nlibrary\nprovider" -> "Local disk" "Local backend" -> "Local\nplayback\nprovider" -> "Local disk" "Local backend" -> "Local\nplaylists\nprovider" -> "Local disk" "Local\nplayback\nprovider" -> Audio "Spotify backend" -> "Spotify\nlibrary\nprovider" -> "Spotify service" "Spotify backend" -> "Spotify\nplayback\nprovider" -> "Spotify service" "Spotify backend" -> "Spotify\nplaylists\nprovider" -> "Spotify service" "Spotify\nplayback\nprovider" -> Audio Audio ===== The audio actor is a thin wrapper around the parts of the GStreamer library we use. In addition to playback, it's responsible for volume control through both GStreamer's own volume mixers, and mixers we've created ourselves. If you implement an advanced backend, you may need to implement your own playback provider using the :ref:`audio-api`. mopidy-0.17.0/docs/api/config.rst000066400000000000000000000011121224420023200165620ustar00rootroot00000000000000.. _config-api: ********** Config API ********** .. automodule:: mopidy.config :synopsis: Config API for config loading and validation :members: Config section schemas ====================== .. automodule:: mopidy.config.schemas :synopsis: Config section validation schemas :members: Config value types ================== .. automodule:: mopidy.config.types :synopsis: Config value validation types :members: Config value validators ======================= .. automodule:: mopidy.config.validators :synopsis: Config value validators :members: mopidy-0.17.0/docs/api/core.rst000066400000000000000000000021161224420023200162520ustar00rootroot00000000000000.. _core-api: ******** Core API ******** .. module:: mopidy.core :synopsis: Core API for use by frontends The core API is the interface that is used by frontends like :mod:`mopidy.frontends.mpd`. The core layer is inbetween the frontends and the backends. Playback controller =================== Manages playback, with actions like play, pause, stop, next, previous, seek, and volume control. .. autoclass:: mopidy.core.PlaybackState :members: .. autoclass:: mopidy.core.PlaybackController :members: Tracklist controller ==================== Manages everything related to the tracks we are currently playing. .. autoclass:: mopidy.core.TracklistController :members: Playlists controller ==================== Manages persistence of playlists. .. autoclass:: mopidy.core.PlaylistsController :members: Library controller ================== Manages the music library, e.g. searching for tracks to be added to a playlist. .. autoclass:: mopidy.core.LibraryController :members: Core listener ============= .. autoclass:: mopidy.core.CoreListener :members: mopidy-0.17.0/docs/api/ext.rst000066400000000000000000000003431224420023200161220ustar00rootroot00000000000000.. _ext-api: ************* Extension API ************* If you want to learn how to make Mopidy extensions, read :ref:`extensiondev`. .. automodule:: mopidy.ext :synopsis: Extension API for extending Mopidy :members: mopidy-0.17.0/docs/api/frontends.rst000066400000000000000000000030451224420023200173260ustar00rootroot00000000000000.. _frontend-api: ************ Frontend API ************ The following requirements applies to any frontend implementation: - A frontend MAY do mostly whatever it wants to, including creating threads, opening TCP ports and exposing Mopidy for a group of clients. - A frontend MUST implement at least one `Pykka `_ actor, called the "main actor" from here on. - The main actor MUST accept two constructor arguments: - ``config``, which is a dict structure with the entire Mopidy configuration. - ``core``, which will be an :class:`ActorProxy ` for the core actor. This object gives access to the full :ref:`core-api`. - It MAY use additional actors to implement whatever it does, and using actors in frontend implementations is encouraged. - The frontend is enabled if the extension it is part of is enabled. See :ref:`extensiondev` for more information. - The main actor MUST be able to start and stop the frontend when the main actor is started and stopped. - The frontend MAY require additional settings to be set for it to work. - Such settings MUST be documented. - The main actor MUST stop itself if the defined settings are not adequate for the frontend to work properly. - Any actor which is part of the frontend MAY implement the :class:`mopidy.core.CoreListener` interface to receive notification of the specified events. .. _frontend-implementations: Frontend implementations ======================== * :mod:`mopidy.frontends.http` * :mod:`mopidy.frontends.mpd` mopidy-0.17.0/docs/api/http.rst000066400000000000000000000362531224420023200163120ustar00rootroot00000000000000.. _http-api: ******** HTTP API ******** The :ref:`ext-http` extension makes Mopidy's :ref:`core-api` available over HTTP using WebSockets. We also provide a JavaScript wrapper, called :ref:`Mopidy.js ` around the HTTP API for use both from browsers and Node.js. .. warning:: API stability Since the HTTP API exposes our internal core API directly it is to be regarded as **experimental**. We cannot promise to keep any form of backwards compatibility between releases as we will need to change the core API while working out how to support new use cases. Thus, if you use this API, you must expect to do small adjustments to your client for every release of Mopidy. From Mopidy 1.0 and onwards, we intend to keep the core API far more stable. .. _websocket-api: WebSocket API ============= The web server exposes a WebSocket at ``/mopidy/ws/``. The WebSocket gives you access to Mopidy's full API and enables Mopidy to instantly push events to the client, as they happen. On the WebSocket we send two different kind of messages: The client can send JSON-RPC 2.0 requests, and the server will respond with JSON-RPC 2.0 responses. In addition, the server will send event messages when something happens on the server. Both message types are encoded as JSON objects. Event messages -------------- Event objects will always have a key named ``event`` whose value is the event type. Depending on the event type, the event may include additional fields for related data. The events maps directly to the :class:`mopidy.core.CoreListener` API. Refer to the ``CoreListener`` method names is the available event types. The ``CoreListener`` method's keyword arguments are all included as extra fields on the event objects. Example event message:: {"event": "track_playback_started", "track": {...}} JSON-RPC 2.0 messaging ---------------------- JSON-RPC 2.0 messages can be recognized by checking for the key named ``jsonrpc`` with the string value ``2.0``. For details on the messaging format, please refer to the `JSON-RPC 2.0 spec `_. All methods (not attributes) in the :ref:`core-api` is made available through JSON-RPC calls over the WebSocket. For example, :meth:`mopidy.core.PlaybackController.play` is available as the JSON-RPC method ``core.playback.play``. The core API's attributes is made available through setters and getters. For example, the attribute :attr:`mopidy.core.PlaybackController.current_track` is available as the JSON-RPC method ``core.playback.get_current_track``. Example JSON-RPC request:: {"jsonrpc": "2.0", "id": 1, "method": "core.playback.get_current_track"} Example JSON-RPC response:: {"jsonrpc": "2.0", "id": 1, "result": {"__model__": "Track", "...": "..."}} The JSON-RPC method ``core.describe`` returns a data structure describing all available methods. If you're unsure how the core API maps to JSON-RPC, having a look at the ``core.describe`` response can be helpful. .. _mopidy-js: Mopidy.js JavaScript library ============================ We've made a JavaScript library, Mopidy.js, which wraps the WebSocket and gets you quickly started with working on your client instead of figuring out how to communicate with Mopidy. Getting the library for browser use ----------------------------------- Regular and minified versions of Mopidy.js, ready for use, is installed together with Mopidy. When the HTTP extension is enabled, the files are available at: - http://localhost:6680/mopidy/mopidy.js - http://localhost:6680/mopidy/mopidy.min.js You may need to adjust hostname and port for your local setup. Thus, if you use Mopidy to host your web client, like described above, you can load the latest version of Mopidy.js by adding the following script tag to your HTML file: .. code-block:: html If you don't use Mopidy to host your web client, you can find the JS files in the Git repo at: - ``mopidy/frontends/http/data/mopidy.js`` - ``mopidy/frontends/http/data/mopidy.min.js`` Getting the library for Node.js use ----------------------------------- If you want to use Mopidy.js from Node.js instead of a browser, you can install Mopidy.js using npm:: npm install mopidy After npm completes, you can import Mopidy.js using ``require()``: .. code-block:: js var Mopidy = require("mopidy").Mopidy; Getting the library for development on the library -------------------------------------------------- If you want to work on the Mopidy.js library itself, you'll find a complete development setup in the ``js/`` dir in our repo. The instructions in ``js/README.md`` will guide you on your way. Creating an instance -------------------- Once you got Mopidy.js loaded, you need to create an instance of the wrapper: .. code-block:: js var mopidy = new Mopidy(); When you instantiate ``Mopidy()`` without arguments, it will connect to the WebSocket at ``/mopidy/ws/`` on the current host. Thus, if you don't host your web client using Mopidy's web server, or if you use Mopidy.js from a Node.js environment, you'll need to pass the URL to the WebSocket end point: .. code-block:: js var mopidy = new Mopidy({ webSocketUrl: "ws://localhost:6680/mopidy/ws/" }); It is also possible to create an instance first and connect to the WebSocket later: .. code-block:: js var mopidy = new Mopidy({autoConnect: false}); // ... do other stuff, like hooking up events ... mopidy.connect(); Hooking up to events -------------------- Once you have a Mopidy.js object, you can hook up to the events it emits. To explore your possibilities, it can be useful to subscribe to all events and log them: .. code-block:: js mopidy.on(console.log.bind(console)); Several types of events are emitted: - You can get notified about when the Mopidy.js object is connected to the server and ready for method calls, when it's offline, and when it's trying to reconnect to the server by looking at the events ``state:online``, ``state:offline``, ``reconnectionPending``, and ``reconnecting``. - You can get events sent from the Mopidy server by looking at the events with the name prefix ``event:``, like ``event:trackPlaybackStarted``. - You can introspect what happens internally on the WebSocket by looking at the events emitted with the name prefix ``websocket:``. Mopidy.js uses the event emitter library `BANE `_, so you should refer to BANE's short API documentation to see how you can hook up your listeners to the different events. Calling core API methods ------------------------ Once your Mopidy.js object has connected to the Mopidy server and emits the ``state:online`` event, it is ready to accept core API method calls: .. code-block:: js mopidy.on("state:online", function () { mopidy.playback.next(); }); Any calls you make before the ``state:online`` event is emitted will fail. If you've hooked up an errback (more on that a bit later) to the promise returned from the call, the errback will be called with an error message. All methods in Mopidy's :ref:`core-api` is available via Mopidy.js. The core API attributes is *not* available, but that shouldn't be a problem as we've added (undocumented) getters and setters for all of them, so you can access the attributes as well from JavaScript. Both the WebSocket API and the JavaScript API are based on introspection of the core Python API. Thus, they will always be up to date and immediately reflect any changes we do to the core API. The best way to explore the JavaScript API, is probably by opening your browser's console, and using its tab completion to navigate the API. You'll find the Mopidy core API exposed under ``mopidy.playback``, ``mopidy.tracklist``, ``mopidy.playlists``, and ``mopidy.library``. All methods in the JavaScript API have an associated data structure describing the Python params it expects, and most methods also have the Python API documentation available. This is available right there in the browser console, by looking at the method's ``description`` and ``params`` attributes: .. code-block:: js console.log(mopidy.playback.next.params); console.log(mopidy.playback.next.description); JSON-RPC 2.0 limits method parameters to be sent *either* by-position or by-name. Combinations of both, like we're used to from Python, isn't supported by JSON-RPC 2.0. To further limit this, Mopidy.js currently only supports passing parameters by-position. Obviously, you'll want to get a return value from many of your method calls. Since everything is happening across the WebSocket and maybe even across the network, you'll get the results asynchronously. Instead of having to pass callbacks and errbacks to every method you call, the methods return "promise" objects, which you can use to pipe the future result as input to another method, or to hook up callback and errback functions. .. code-block:: js var track = mopidy.playback.getCurrentTrack(); // => ``track`` isn't a track, but a "promise" object Instead, typical usage will look like this: .. code-block:: js var printCurrentTrack = function (track) { if (track) { console.log("Currently playing:", track.name, "by", track.artists[0].name, "from", track.album.name); } else { console.log("No current track"); } }; mopidy.playback.getCurrentTrack().then( printCurrentTrack, console.error.bind(console)); The first function passed to ``then()``, ``printCurrentTrack``, is the callback that will be called if the method call succeeds. The second function, ``console.error``, is the errback that will be called if anything goes wrong. If you don't hook up an errback, debugging will be hard as errors will silently go missing. For debugging, you may be interested in errors from function without interesting return values as well. In that case, you can pass ``null`` as the callback: .. code-block:: js mopidy.playback.next().then(null, console.error.bind(console)); The promise objects returned by Mopidy.js adheres to the `CommonJS Promises/A `_ standard. We use the implementation known as `when.js `_. Please refer to when.js' documentation or the standard for further details on how to work with promise objects. Cleaning up ----------- If you for some reason want to clean up after Mopidy.js before the web page is closed or navigated away from, you can close the WebSocket, unregister all event listeners, and delete the object like this: .. code-block:: js // Close the WebSocket without reconnecting. Letting the object be garbage // collected will have the same effect, so this isn't strictly necessary. mopidy.close(); // Unregister all event listeners. If you don't do this, you may have // lingering references to the object causing the garbage collector to not // clean up after it. mopidy.off(); // Delete your reference to the object, so it can be garbage collected. mopidy = null; Example to get started with --------------------------- 1. Make sure that you've installed all dependencies required by :ref:`ext-http`. 2. Create an empty directory for your web client. 3. Change the :confval:`http/static_dir` config value to point to your new directory. 4. Start/restart Mopidy. 5. Create a file in the directory named ``index.html`` containing e.g. "Hello, world!". 6. Visit http://localhost:6680/ to confirm that you can view your new HTML file there. 7. Include Mopidy.js in your web page: .. code-block:: html 8. Add one of the following Mopidy.js examples of how to queue and start playback of your first playlist either to your web page or a JavaScript file that you include in your web page. "Imperative" style: .. code-block:: js var consoleError = console.error.bind(console); var trackDesc = function (track) { return track.name + " by " + track.artists[0].name + " from " + track.album.name; }; var queueAndPlayFirstPlaylist = function () { mopidy.playlists.getPlaylists().then(function (playlists) { var playlist = playlists[0]; console.log("Loading playlist:", playlist.name); mopidy.tracklist.add(playlist.tracks).then(function (tlTracks) { mopidy.playback.play(tlTracks[0]).then(function () { mopidy.playback.getCurrentTrack().then(function (track) { console.log("Now playing:", trackDesc(track)); }, consoleError); }, consoleError); }, consoleError); }, consoleError); }; var mopidy = new Mopidy(); // Connect to server mopidy.on(console.log.bind(console)); // Log all events mopidy.on("state:online", queueAndPlayFirstPlaylist); Approximately the same behavior in a more functional style, using chaining of promisies. .. code-block:: js var consoleError = console.error.bind(console); var getFirst = function (list) { return list[0]; }; var extractTracks = function (playlist) { return playlist.tracks; }; var printTypeAndName = function (model) { console.log(model.__model__ + ": " + model.name); // By returning the playlist, this function can be inserted // anywhere a model with a name is piped in the chain. return model; }; var trackDesc = function (track) { return track.name + " by " + track.artists[0].name + " from " + track.album.name; }; var printNowPlaying = function () { // By returning any arguments we get, the function can be inserted // anywhere in the chain. var args = arguments; return mopidy.playback.getCurrentTrack().then(function (track) { console.log("Now playing:", trackDesc(track)); return args; }); }; var queueAndPlayFirstPlaylist = function () { mopidy.playlists.getPlaylists() // => list of Playlists .then(getFirst, consoleError) // => Playlist .then(printTypeAndName, consoleError) // => Playlist .then(extractTracks, consoleError) // => list of Tracks .then(mopidy.tracklist.add, consoleError) // => list of TlTracks .then(getFirst, consoleError) // => TlTrack .then(mopidy.playback.play, consoleError) // => null .then(printNowPlaying, consoleError); }; var mopidy = new Mopidy(); // Connect to server mopidy.on(console.log.bind(console)); // Log all events mopidy.on("state:online", queueAndPlayFirstPlaylist); 9. The web page should now queue and play your first playlist every time your load it. See the browser's console for output from the function, any errors, and all events that are emitted. mopidy-0.17.0/docs/api/index.rst000066400000000000000000000003011224420023200164230ustar00rootroot00000000000000.. _api-ref: ************* API reference ************* .. toctree:: :glob: concepts models backends core audio frontends commands ext config http mopidy-0.17.0/docs/api/models.rst000066400000000000000000000025571224420023200166160ustar00rootroot00000000000000*********** Data models *********** These immutable data models are used for all data transfer within the Mopidy backends and between the backends and the MPD frontend. All fields are optional and immutable. In other words, they can only be set through the class constructor during instance creation. If you want to modify a model, use the :meth:`~mopidy.models.ImmutableObject.copy` method. It accepts keyword arguments for the parts of the model you want to change, and copies the rest of the data from the model you call it on. Example:: >>> from mopidy.models import Track >>> track1 = Track(name='Christmas Carol', length=171) >>> track1 Track(artists=[], length=171, name='Christmas Carol') >>> track2 = track1.copy(length=37) >>> track2 Track(artists=[], length=37, name='Christmas Carol') >>> track1 Track(artists=[], length=171, name='Christmas Carol') Data model relations ==================== .. digraph:: model_relations Playlist -> Track [ label="has 0..n" ] Track -> Album [ label="has 0..1" ] Track -> Artist [ label="has 0..n" ] Album -> Artist [ label="has 0..n" ] SearchResult -> Artist [ label="has 0..n" ] SearchResult -> Album [ label="has 0..n" ] SearchResult -> Track [ label="has 0..n" ] Data model API ============== .. automodule:: mopidy.models :synopsis: Data model API :members: mopidy-0.17.0/docs/authors.rst000066400000000000000000000013121224420023200162330ustar00rootroot00000000000000.. _authors: ******* Authors ******* Mopidy is copyright 2009-2013 Stein Magnus Jodal and contributors. Mopidy is licensed under the `Apache License, Version 2.0 `_. The following persons have contributed to Mopidy. The list is in the order of first contribution. For details on who have contributed what, please refer to our Git repository. .. include:: ../AUTHORS If you already enjoy Mopidy, or don't enjoy it and want to help us making Mopidy better, the best way to do so is to contribute back to the community. You can contribute code, documentation, tests, bug reports, or help other users, spreading the word, etc. See :ref:`contributing` for a head start. mopidy-0.17.0/docs/changelog.rst000066400000000000000000002437311224420023200165120ustar00rootroot00000000000000********* Changelog ********* This changelog is used to track all major changes to Mopidy. v0.17.0 (2013-11-23) ==================== The focus of 0.17 has been on introducing subcommands to the ``mopidy`` command, making it possible for extensions to add subcommands of their own, and to improve the default config file when starting Mopidy the first time. In addition, we've grown support for Zeroconf publishing of the MPD and HTTP servers, and gotten a much faster scanner. The scanner now also scans some additional tags like composers and performers. Since the release of 0.16, we've closed or merged 22 issues and pull requests through about 200 commits by :ref:`five people `, including one new contributor. **Commands** - Switched to subcommands for the ``mopidy`` command , this implies the following changes: (Fixes: :issue:`437`) ===================== ================= Old command New command ===================== ================= mopidy --show-deps mopidy deps mopidy --show-config mopidy config mopidy-scan mopidy local scan ===================== ================= - Added hooks for extensions to create their own custom subcommands and converted ``mopidy-scan`` as a first user of the new API. (Fixes: :issue:`436`) **Configuration** - When ``mopidy`` is started for the first time we create an empty :file:`{$XDG_CONFIG_DIR}/mopidy/mopidy.conf` file. We now populate this file with the default config for all installed extensions so it'll be easier to set up Mopidy without looking through all the documentation for relevant config values. (Fixes: :issue:`467`) **Core API** - The :class:`~mopidy.models.Track` model has grown fields for ``composers``, ``performers``, ``genre``, and ``comment``. - The search field ``track`` has been renamed to ``track_name`` to avoid confusion with ``track_no``. (Fixes: :issue:`535`) - The signature of the tracklist's :meth:`~mopidy.core.TracklistController.filter` and :meth:`~mopidy.core.TracklistController.remove` methods have changed. Previously, they expected e.g. ``tracklist.filter(tlid=17)``. Now, the value must always be a list, e.g. ``tracklist.filter(tlid=[17])``. This change allows you to get or remove multiple tracks with a single call, e.g. ``tracklist.remove(tlid=[1, 2, 7])``. This is especially useful for web clients, as requests can be batched. This also brings the interface closer to the library's :meth:`~mopidy.core.LibraryController.find_exact` and :meth:`~mopidy.core.LibraryController.search` methods. **Audio** - Change default volume mixer from ``autoaudiomixer`` to ``software``. GStreamer 1.x does not support volume control, so we're changing to use software mixing by default, as that may be the only thing we'll support in the future when we upgrade to GStreamer 1.x. **Local backend** - Library scanning has been switched back from GStreamer's discoverer to our custom implementation due to various issues with GStreamer 0.10's built in scanner. This also fixes the scanner slowdown. (Fixes: :issue:`565`) - When scanning, we no longer default the album artist to be the same as the track artist. Album artist is now only populated if the scanned file got an explicit album artist set. - The scanner will now extract multiple artists from files with multiple artist tags. - The scanner will now extract composers and performers, as well as genre, bitrate, and comments. (Fixes: :issue:`577`) - Fix scanner so that time of last modification is respected when deciding which files can be skipped when scanning the music collection for changes. - The scanner now ignores the capitalization of file extensions in :confval:`local/excluded_file_extensions`, so you no longer need to list both ``.jpg`` and ``.JPG`` to ignore JPEG files when scanning. (Fixes: :issue:`525`) - The scanner now by default ignores ``*.nfo`` and ``*.html`` files too. **MPD frontend** - The MPD service is now published as a Zeroconf service if avahi-daemon is running on the system. Some MPD clients will use this to present Mopidy as an available server on the local network without needing any configuration. See the :confval:`mpd/zeroconf` config value to change the service name or disable the service. (Fixes: :issue:`39`) - Add support for ``composer``, ``performer``, ``comment``, ``genre``, and ``performer``. These tags can be used with ``list ...``, ``search ...``, and ``find ...`` and their variants, and are supported in the ``any`` tag also - The ``bitrate`` field in the ``status`` response is now always an integer. This follows the behavior of the original MPD server. (Fixes: :issue:`577`) **HTTP frontend** - The HTTP service is now published as a Zeroconf service if avahi-daemon is running on the system. Some browsers will present HTTP Zeroconf services on the local network as "local sites" bookmarks. See the :confval:`http/zeroconf` config value to change the service name or disable the service. (Fixes: :issue:`39`) **DBUS/MPRIS** - The ``mopidy`` process now registers it's GObject event loop as the default eventloop for dbus-python. (Fixes: :mpris:`2`) v0.16.1 (2013-11-02) ==================== This is very small release to get Mopidy's Debian package ready for inclusion in Debian. **Commands** - Fix removal of last dir level in paths to dependencies in ``mopidy --show-deps`` output. - Add manpages for all commands. **Local backend** - Fix search filtering by track number that was added in 0.16.0. **MPD frontend** - Add support for ``list "albumartist" ...`` which was missed when ``find`` and ``search`` learned to handle ``albumartist`` in 0.16.0. (Fixes: :issue:`553`) v0.16.0 (2013-10-27) ==================== The goals for 0.16 were to add support for queuing playlists of e.g. radio streams directly to Mopidy, without manually extracting the stream URLs from the playlist first, and to move the Spotify, Last.fm, and MPRIS support out to independent Mopidy extensions, living outside the main Mopidy repo. In addition, we've seen some cleanup to the playback vs tracklist part of the core API, which will require some changes for users of the HTTP/JavaScript APIs, as well as the addition of audio muting to the core API. To speed up the :ref:`development of new extensions `, we've added a cookiecutter project to get the skeleton of a Mopidy extension up and running in a matter of minutes. Read below for all the details and for links to issues with even more details. Since the release of 0.15, we've closed or merged 31 issues and pull requests through about 200 commits by :ref:`five people `, including three new contributors. **Dependencies** Parts of Mopidy have been moved to their own external extensions. If you want Mopidy to continue to work like it used to, you may have to install one or more of the following extensions as well: - The Spotify backend has been moved to `Mopidy-Spotify `_. - The Last.fm scrobbler has been moved to `Mopidy-Scrobbler `_. - The MPRIS frontend has been moved to `Mopidy-MPRIS `_. **Core** - Parts of the functionality in :class:`mopidy.core.PlaybackController` have been moved to :class:`mopidy.core.TracklistController`: =================================== ================================== Old location New location =================================== ================================== playback.get_consume() tracklist.get_consume() playback.set_consume(v) tracklist.set_consume(v) playback.consume tracklist.consume playback.get_random() tracklist.get_random() playback.set_random(v) tracklist.set_random(v) playback.random tracklist.random playback.get_repeat() tracklist.get_repeat() playback.set_repeat(v) tracklist.set_repeat(v) playback.repeat tracklist.repeat playback.get_single() tracklist.get_single() playback.set_single(v) tracklist.set_single(v) playback.single tracklist.single playback.get_tracklist_position() tracklist.index(tl_track) playback.tracklist_position tracklist.index(tl_track) playback.get_tl_track_at_eot() tracklist.eot_track(tl_track) playback.tl_track_at_eot tracklist.eot_track(tl_track) playback.get_tl_track_at_next() tracklist.next_track(tl_track) playback.tl_track_at_next tracklist.next_track(tl_track) playback.get_tl_track_at_previous() tracklist.previous_track(tl_track) playback.tl_track_at_previous tracklist.previous_track(tl_track) =================================== ================================== The ``tl_track`` argument to the last four new functions are used as the reference ``tl_track`` in the tracklist to find e.g. the next track. Usually, this will be :attr:`~mopidy.core.PlaybackController.current_tl_track`. - Added :attr:`mopidy.core.PlaybackController.mute` for muting and unmuting audio. (Fixes: :issue:`186`) - Added :meth:`mopidy.core.CoreListener.mute_changed` event that is triggered when the mute state changes. - In "random" mode, after a full playthrough of the tracklist, playback continued from the last track played to the end of the playlist in non-random order. It now stops when all tracks have been played once, unless "repeat" mode is enabled. (Fixes: :issue:`453`) - In "single" mode, after a track ended, playback continued with the next track in the tracklist. It now stops after playing a single track, unless "repeat" mode is enabled. (Fixes: :issue:`496`) **Audio** - Added support for parsing and playback of playlists in GStreamer. For end users this basically means that you can now add a radio playlist to Mopidy and we will automatically download it and play the stream inside it. Currently we support M3U, PLS, XSPF and ASX files. Also note that we can currently only play the first stream in the playlist. - We now handle the rare case where an audio track has max volume equal to min. This was causing divide by zero errors when scaling volumes to a zero to hundred scale. (Fixes: :issue:`525`) - Added support for muting audio without setting the volume to 0. This works both for the software and hardware mixers. (Fixes: :issue:`186`) **Local backend** - Replaced our custom media library scanner with GStreamer's builtin scanner. This should make scanning less error prone and faster as timeouts should be infrequent. (Fixes: :issue:`198`) - Media files with less than 100ms duration are now excluded from the library. - Media files with the file extensions ``.jpeg``, ``.jpg``, ``.png``, ``.txt``, and ``.log`` are now skipped by the media library scanner. You can change the list of excluded file extensions by setting the :confval:`local/excluded_file_extensions` config value. (Fixes: :issue:`516`) - Unknown URIs found in playlists are now made into track objects with the URI set instead of being ignored. This makes it possible to have playlists with e.g. HTTP radio streams and not just ``local:track:...`` URIs. This used to work, but was broken in Mopidy 0.15.0. (Fixes: :issue:`527`) - Fixed crash when playing ``local:track:...`` URIs which contained non-ASCII chars after uridecode. - Removed media files are now also removed from the in-memory media library when the media library is reloaded from disk. (Fixes: :issue:`500`) **MPD frontend** - Made the formerly unused commands ``outputs``, ``enableoutput``, and ``disableoutput`` mute/unmute audio. (Related to: :issue:`186`) - The MPD command ``list`` now works with ``"albumartist"`` as its second argument, e.g. ``list "album" "albumartist" "anartist"``. (Fixes: :issue:`468`) - The MPD commands ``find`` and ``search`` now accepts ``albumartist`` and ``track`` (this is the track number, not the track name) as field types to limit the search result with. - The MPD command ``count`` is now implemented. It accepts the same type of arguments as ``find`` and ``search``, but returns the number of tracks and their total playtime instead. **Extension support** - A cookiecutter project for quickly creating new Mopidy extensions have been created. You can find it at `cookiecutter-mopidy-ext `_. (Fixes: :issue:`522`) v0.15.0 (2013-09-19) ==================== A release with a number of small and medium fixes, with no specific focus. **Dependencies** - Mopidy no longer supports Python 2.6. Currently, the only Python version supported by Mopidy is Python 2.7. We're continuously working towards running Mopidy on Python 3. (Fixes: :issue:`344`) **Command line options** - Converted from the optparse to the argparse library for handling command line options. - :option:`mopidy --show-config` will now take into consideration any :option:`mopidy --option` arguments appearing later on the command line. This helps you see the effective configuration for runs with the same :option:`mopidy --options` arguments. **Audio** - Added support for audio visualization. :confval:`audio/visualizer` can now be set to GStreamer visualizers. - Properly encode localized mixer names before logging. **Local backend** - An album's number of discs and a track's disc number are now extracted when scanning your music collection. - The scanner now gives up scanning a file after a second, and continues with the next file. This fixes some hangs on non-media files, like logs. (Fixes: :issue:`476`, :issue:`483`) - Added support for pluggable library updaters. This allows extension writers to start providing their own custom libraries instead of being stuck with just our tag cache as the only option. - Converted local backend to use new ``local:playlist:path`` and ``local:track:path`` URI scheme. Also moves support of ``file://`` to streaming backend. **Spotify backend** - Prepend playlist folder names to the playlist name, so that the playlist hierarchy from your Spotify account is available in Mopidy. (Fixes: :issue:`62`) - Fix proxy config values that was broken with the config system change in 0.14. (Fixes: :issue:`472`) **MPD frontend** - Replace newline, carriage return and forward slash in playlist names. (Fixes: :issue:`474`, :issue:`480`) - Accept ``listall`` and ``listallinfo`` commands without the URI parameter. The methods are still not implemented, but now the commands are accepted as valid. **HTTP frontend** - Fix too broad truth test that caused :class:`mopidy.models.TlTrack` objects with ``tlid`` set to ``0`` to be sent to the HTTP client without the ``tlid`` field. (Fixes: :issue:`501`) - Upgrade Mopidy.js dependencies. This version has been released to NPM as Mopidy.js v0.1.1. **Extension support** - :class:`mopidy.config.Secret` is now deserialized to unicode instead of bytes. This may require modifications to extensions. v0.14.2 (2013-07-01) ==================== This is a maintenance release to make Mopidy 0.14 work with pyspotify 1.11. **Dependencies** - pyspotify >= 1.9, < 2 is now required for Spotify support. In other words, you're free to upgrade to pyspotify 1.11, but it isn't a requirement. v0.14.1 (2013-04-28) ==================== This release addresses an issue in v0.14.0 where the new :option:`mopidy-convert-config` tool and the new :option:`mopidy --option` command line option was broken because some string operations inadvertently converted some byte strings to unicode. v0.14.0 (2013-04-28) ==================== The 0.14 release has a clear focus on two things: the new configuration system and extension support. Mopidy's documentation has also been greatly extended and improved. Since the last release a month ago, we've closed or merged 53 issues and pull requests. A total of seven :ref:`authors ` have contributed, including one new. **Dependencies** - setuptools or distribute is now required. We've introduced this dependency to use setuptools' entry points functionality to find installed Mopidy extensions. **New configuration system** - Mopidy has a new configuration system based on ini-style files instead of a Python file. This makes configuration easier for users, and also makes it possible for Mopidy extensions to have their own config sections. As part of this change we have cleaned up the naming of our config values. To ease migration we've made a tool named :option:`mopidy-convert-config` for automatically converting the old ``settings.py`` to a new ``mopidy.conf`` file. This tool takes care of all the renamed config values as well. See :ref:`mopidy-convert-config` for details on how to use it. - A long wanted feature: You can now enable or disable specific frontends or backends without having to redefine :attr:`~mopidy.settings.FRONTENDS` or :attr:`~mopidy.settings.BACKENDS` in your config. Those config values are gone completely. **Extension support** - Mopidy now supports extensions. This means that any developer now easily can create a Mopidy extension to add new control interfaces or music backends. This helps spread the maintenance burden across more developers, and also makes it possible to extend Mopidy with new backends the core developers are unable to create and/or maintain because of geo restrictions, etc. If you're interested in creating an extension for Mopidy, read up on :ref:`extensiondev`. - All of Mopidy's existing frontends and backends are now plugged into Mopidy as extensions, but they are still distributed together with Mopidy and are enabled by default. - The NAD mixer have been moved out of Mopidy core to its own project, Mopidy-NAD. See :ref:`ext` for more information. - Janez Troha has made the first two external extensions for Mopidy: a backend for playing music from Soundcloud, and a backend for playing music from a Beets music library. See :ref:`ext` for more information. **Command line options** - The command option :option:`mopidy --list-settings` is now named :option:`mopidy --show-config`. - The command option :option:`mopidy --list-deps` is now named :option:`mopidy --show-deps`. - What configuration files to use can now be specified through the command option :option:`mopidy --config`, multiple files can be specified using colon as a separator. - Configuration values can now be overridden through the command option :option:`mopidy --option`. For example: ``mopidy --option spotify/enabled=false``. - The GStreamer command line options, :option:`mopidy --gst-*` and :option:`mopidy --help-gst` are no longer supported. To set GStreamer debug flags, you can use environment variables such as :envvar:`GST_DEBUG`. Refer to GStreamer's documentation for details. **Spotify backend** - Add support for starred playlists, both your own and those owned by other users. (Fixes: :issue:`326`) - Fix crash when a new playlist is added by another Spotify client. (Fixes: :issue:`387`, :issue:`425`) **MPD frontend** - Playlists with identical names are now handled properly by the MPD frontend by suffixing the duplicate names with e.g. ``[2]``. This is needed because MPD identify playlists by name only, while Mopidy and Spotify supports multiple playlists with the same name, and identify them using an URI. (Fixes: :issue:`114`) **MPRIS frontend** - The frontend is now disabled if the :envvar:`DISPLAY` environment variable is unset. This avoids some harmless error messages, that have been known to confuse new users debugging other problems. **Development** - Developers running Mopidy from a Git clone now need to run ``python setup.py develop`` to register the bundled extensions. If you don't do this, Mopidy will not find any frontends or backends. Note that we highly recomend you do this in a virtualenv, not system wide. As a bonus, the command also gives you a ``mopidy`` executable in your search path. v0.13.0 (2013-03-31) ==================== The 0.13 release brings small improvements and bugfixes throughout Mopidy. There are no major new features, just incremental improvement of what we already have. **Dependencies** - Pykka >= 1.1 is now required. **Core** - Removed the :attr:`mopidy.settings.DEBUG_THREAD` setting and the :option:`--debug-thread` command line option. Sending SIGUSR1 to the Mopidy process will now always make it log tracebacks for all alive threads. - Log a warning if a track isn't playable to make it more obvious that backend X needs backend Y to be present for playback to work. - :meth:`mopidy.core.TracklistController.add` now accepts an ``uri`` which it will lookup in the library and then add to the tracklist. This is helpful for e.g. web clients that doesn't want to transfer all track meta data back to the server just to add it to the tracklist when the server already got all the needed information easily available. (Fixes: :issue:`325`) - Change the following methods to accept an ``uris`` keyword argument: - :meth:`mopidy.core.LibraryController.find_exact` - :meth:`mopidy.core.LibraryController.search` Search queries will only be forwarded to backends handling the given URI roots, and the backends may use the URI roots to further limit what results are returned. For example, a search with ``uris=['file:']`` will only be processed by the local backend. A search with ``uris=['file:///media/music']`` will only be processed by the local backend, and, if such filtering is supported by the backend, will only return results with URIs within the given URI root. **Audio sub-system** - Make audio error logging handle log messages with non-ASCII chars. (Fixes: :issue:`347`) **Local backend** - Make ``mopidy-scan`` work with Ogg Vorbis files. (Fixes: :issue:`275`) - Fix playback of files with non-ASCII chars in their file path. (Fixes: :issue:`353`) **Spotify backend** - Let GStreamer handle time position tracking and seeks. (Fixes: :issue:`191`) - For all playlists owned by other Spotify users, we now append the owner's username to the playlist name. (Partly fixes: :issue:`114`) **HTTP frontend** - Mopidy.js now works both from browsers and from Node.js environments. This means that you now can make Mopidy clients in Node.js. Mopidy.js has been published to the `npm registry `_ for easy installation in Node.js projects. - Upgrade Mopidy.js' build system Grunt from 0.3 to 0.4. - Upgrade Mopidy.js' dependencies when.js from 1.6.1 to 2.0.0. - Expose :meth:`mopidy.core.Core.get_uri_schemes` to HTTP clients. It is available through Mopidy.js as ``mopidy.getUriSchemes()``. **MPRIS frontend** - Publish album art URIs if available. - Publish disc number of track if available. v0.12.0 (2013-03-12) ==================== The 0.12 release has been delayed for a while because of some issues related some ongoing GStreamer cleanup we didn't invest enough time to finish. Finally, we've come to our senses and have now cherry-picked the good parts to bring you a new release, while postponing the GStreamer changes to 0.13. The release adds a new backend for playing audio streams, as well as various minor improvements throughout Mopidy. - Make Mopidy work on early Python 2.6 versions. (Fixes: :issue:`302`) - ``optparse`` fails if the first argument to ``add_option`` is a unicode string on Python < 2.6.2rc1. - ``foo(**data)`` fails if the keys in ``data`` is unicode strings on Python < 2.6.5rc1. **Audio sub-system** - Improve selection of mixer tracks for volume control. (Fixes: :issue:`307`) **Local backend** - Make ``mopidy-scan`` support symlinks. **Stream backend** We've added a new backend for playing audio streams, the :mod:`stream backend `. It is activated by default. The stream backend supports the intersection of what your GStreamer installation supports and what protocols are included in the :attr:`mopidy.settings.STREAM_PROTOCOLS` setting. Current limitations: - No metadata about the current track in the stream is available. - Playlists are not parsed, so you can't play e.g. a M3U or PLS file which contains stream URIs. You need to extract the stream URL from the playlist yourself. See :issue:`303` for progress on this. **Core API** - :meth:`mopidy.core.PlaylistsController.get_playlists` now accepts an argument ``include_tracks``. This defaults to :class:`True`, which has the same old behavior. If set to :class:`False`, the tracks are stripped from the playlists before they are returned. This can be used to limit the amount of data returned if the response is to be passed out of the application, e.g. to a web client. (Fixes: :issue:`297`) **Models** - Add :attr:`mopidy.models.Album.images` field for including album art URIs. (Partly fixes :issue:`263`) - Add :attr:`mopidy.models.Track.disc_no` field. (Partly fixes: :issue:`286`) - Add :attr:`mopidy.models.Album.num_discs` field. (Partly fixes: :issue:`286`) v0.11.1 (2012-12-24) ==================== Spotify search was broken in 0.11.0 for users of Python 2.6. This release fixes it. If you're using Python 2.7, v0.11.0 and v0.11.1 should be equivalent. v0.11.0 (2012-12-24) ==================== In celebration of Mopidy's three year anniversary December 23, we're releasing Mopidy 0.11. This release brings several improvements, most notably better search which now includes matching artists and albums from Spotify in the search results. **Settings** - The settings validator now complains if a setting which expects a tuple of values (e.g. :attr:`mopidy.settings.BACKENDS`, :attr:`mopidy.settings.FRONTENDS`) has a non-iterable value. This typically happens because the setting value contains a single value and one has forgotten to add a comma after the string, making the value a tuple. (Fixes: :issue:`278`) **Spotify backend** - Add :attr:`mopidy.settings.SPOTIFY_TIMEOUT` setting which allows you to control how long we should wait before giving up on Spotify searches, etc. - Add support for looking up albums, artists, and playlists by URI in addition to tracks. (Fixes: :issue:`67`) As an example of how this can be used, you can try the the following MPD commands which now all adds one or more tracks to your tracklist:: add "spotify:track:1mwt9hzaH7idmC5UCoOUkz" add "spotify:album:3gpHG5MGwnipnap32lFYvI" add "spotify:artist:5TgQ66WuWkoQ2xYxaSTnVP" add "spotify:user:p3.no:playlist:0XX6tamRiqEgh3t6FPFEkw" - Increase max number of tracks returned by searches from 100 to 200, which seems to be Spotify's current max limit. **Local backend** - Load track dates from tag cache. - Add support for searching by track date. **MPD frontend** - Add :attr:`mopidy.settings.MPD_SERVER_CONNECTION_TIMEOUT` setting which controls how long an MPD client can stay inactive before the connection is closed by the server. - Add support for the ``findadd`` command. - Updated to match the MPD 0.17 protocol (Fixes: :issue:`228`): - Add support for ``seekcur`` command. - Add support for ``config`` command. - Add support for loading a range of tracks from a playlist to the ``load`` command. - Add support for ``searchadd`` command. - Add support for ``searchaddpl`` command. - Add empty stubs for channel commands for client to client communication. - Add support for search by date. - Make ``seek`` and ``seekid`` not restart the current track before seeking in it. - Include fake tracks representing albums and artists in the search results. When these are added to the tracklist, they expand to either all tracks in the album or all tracks by the artist. This makes it easy to play full albums in proper order, which is a feature that have been frequently requested. (Fixes: :issue:`67`, :issue:`148`) **Internal changes** *Models:* - Specified that :attr:`mopidy.models.Playlist.last_modified` should be in UTC. - Added :class:`mopidy.models.SearchResult` model to encapsulate search results consisting of more than just tracks. *Core API:* - Change the following methods to return :class:`mopidy.models.SearchResult` objects which can include both track results and other results: - :meth:`mopidy.core.LibraryController.find_exact` - :meth:`mopidy.core.LibraryController.search` - Change the following methods to accept either a dict with filters or kwargs. Previously they only accepted kwargs, which made them impossible to use from the Mopidy.js through JSON-RPC, which doesn't support kwargs. - :meth:`mopidy.core.LibraryController.find_exact` - :meth:`mopidy.core.LibraryController.search` - :meth:`mopidy.core.PlaylistsController.filter` - :meth:`mopidy.core.TracklistController.filter` - :meth:`mopidy.core.TracklistController.remove` - Actually trigger the :meth:`mopidy.core.CoreListener.volume_changed` event. - Include the new volume level in the :meth:`mopidy.core.CoreListener.volume_changed` event. - The ``track_playback_{paused,resumed,started,ended}`` events now include a :class:`mopidy.models.TlTrack` instead of a :class:`mopidy.models.Track`. *Audio:* - Mixers with fewer than 100 volume levels could report another volume level than what you just set due to the conversion between Mopidy's 0-100 range and the mixer's range. Now Mopidy returns the recently set volume if the mixer reports a volume level that matches the recently set volume, otherwise the mixer's volume level is rescaled to the 1-100 range and returned. v0.10.0 (2012-12-12) ==================== We've added an HTTP frontend for those wanting to build web clients for Mopidy! **Dependencies** - pyspotify >= 1.9, < 1.11 is now required for Spotify support. In other words, you're free to upgrade to pyspotify 1.10, but it isn't a requirement. **Documentation** - Added installation instructions for Fedora. **Spotify backend** - Save a lot of memory by reusing artist, album, and track models. - Make sure the playlist loading hack only runs once. **Local backend** - Change log level from error to warning on messages emitted when the tag cache isn't found and a couple of similar cases. - Make ``mopidy-scan`` ignore invalid dates, e.g. dates in years outside the range 1-9999. - Make ``mopidy-scan`` accept :option:`-q`/:option:`--quiet` and :option:`-v`/:option:`--verbose` options to control the amount of logging output when scanning. - The scanner can now handle files with other encodings than UTF-8. Rebuild your tag cache with ``mopidy-scan`` to include tracks that may have been ignored previously. **HTTP frontend** - Added new optional HTTP frontend which exposes Mopidy's core API through JSON-RPC 2.0 messages over a WebSocket. See :ref:`http-api` for further details. - Added a JavaScript library, Mopidy.js, to make it easier to develop web based Mopidy clients using the new HTTP frontend. **Bug fixes** - :issue:`256`: Fix crash caused by non-ASCII characters in paths returned from ``glib``. The bug can be worked around by overriding the settings that includes offending ``$XDG_`` variables. v0.9.0 (2012-11-21) =================== Support for using the local and Spotify backends simultaneously have for a very long time been our most requested feature. Finally, it's here! **Dependencies** - pyspotify >= 1.9, < 1.10 is now required for Spotify support. **Documentation** - New :ref:`installation` guides, organized by OS and distribution so that you can follow one concise list of instructions instead of jumping around the docs to look for instructions for each dependency. - Moved :ref:`raspberrypi-installation` howto from the wiki to the docs. - Updated :ref:`mpd-clients` overview. - Added :ref:`mpris-clients` and :ref:`upnp-clients` overview. **Multiple backends support** - Both the local backend and the Spotify backend are now turned on by default. The local backend is listed first in the :attr:`mopidy.settings.BACKENDS` setting, and are thus given the highest priority in e.g. search results, meaning that we're listing search hits from the local backend first. If you want to prioritize the backends in another way, simply set ``BACKENDS`` in your own settings file and reorder the backends. There are no other setting changes related to the local and Spotify backends. As always, see :mod:`mopidy.settings` for the full list of available settings. **Spotify backend** - The Spotify backend now includes release year and artist on albums. - :issue:`233`: The Spotify backend now returns the track if you search for the Spotify track URI. - Added support for connecting to the Spotify service through an HTTP or SOCKS proxy, which is supported by pyspotify >= 1.9. - Subscriptions to other Spotify user's "starred" playlists are ignored, as they currently isn't fully supported by pyspotify. **Local backend** - :issue:`236`: The ``mopidy-scan`` command failed to include tags from ALAC files (Apple lossless) because it didn't support multiple tag messages from GStreamer per track it scanned. - Added support for search by filename to local backend. **MPD frontend** - :issue:`218`: The MPD commands ``listplaylist`` and ``listplaylistinfo`` now accepts unquoted playlist names if they don't contain spaces. - :issue:`246`: The MPD command ``list album artist ""`` and similar ``search``, ``find``, and ``list`` commands with empty filter values caused a :exc:`LookupError`, but should have been ignored by the MPD server. - The MPD frontend no longer lowercases search queries. This broke e.g. search by URI, where casing may be essential. - The MPD command ``plchanges`` always returned the entire playlist. It now returns an empty response when the client has seen the latest version. - The MPD commands ``search`` and ``find`` now allows the key ``file``, which is used by ncmpcpp instead of ``filename``. - The MPD commands ``search`` and ``find`` now allow search query values to be empty strings. - The MPD command ``listplaylists`` will no longer return playlists without a name. This could crash ncmpcpp. - The MPD command ``list`` will no longer return artist names, album names, or dates that are blank. - The MPD command ``decoders`` will now return an empty response instead of a "not implemented" error to make the ncmpcpp browse view work the first time it is opened. **MPRIS frontend** - The MPRIS playlists interface is now supported by our MPRIS frontend. This means that you now can select playlists to queue and play from the Ubuntu Sound Menu. **Audio mixers** - Made the :mod:`NAD mixer ` responsive to interrupts during amplifier calibration. It will now quit immediately, while previously it completed the calibration first, and then quit, which could take more than 15 seconds. **Developer support** - Added optional background thread for debugging deadlocks. When the feature is enabled via the ``--debug-thread`` option or :attr:`mopidy.settings.DEBUG_THREAD` setting a ``SIGUSR1`` signal will dump the traceback for all running threads. - The settings validator will now allow any setting prefixed with ``CUSTOM_`` to exist in the settings file. **Internal changes** Internally, Mopidy have seen a lot of changes to pave the way for multiple backends and the future HTTP frontend. - A new layer and actor, "core", has been added to our stack, inbetween the frontends and the backends. The responsibility of the core layer and actor is to take requests from the frontends, pass them on to one or more backends, and combining the response from the backends into a single response to the requesting frontend. Frontends no longer know anything about the backends. They just use the :ref:`core-api`. - The dependency graph between the core controllers and the backend providers have been straightened out, so that we don't have any circular dependencies. The frontend, core, backend, and audio layers are now strictly separate. The frontend layer calls on the core layer, and the core layer calls on the backend layer. Both the core layer and the backends are allowed to call on the audio layer. Any data flow in the opposite direction is done by broadcasting of events to listeners, through e.g. :class:`mopidy.core.CoreListener` and :class:`mopidy.audio.AudioListener`. See :ref:`concepts` for more details and illustrations of all the relations. - All dependencies are now explicitly passed to the constructors of the frontends, core, and the backends. This makes testing each layer with dummy/mocked lower layers easier than with the old variant, where dependencies where looked up in Pykka's actor registry. - All properties in the core API now got getters, and setters if setting them is allowed. They are not explictly listed in the docs as they have the same behavior as the documented properties, but they are available and may be used. This is useful for the future HTTP frontend. *Models:* - Added :attr:`mopidy.models.Album.date` attribute. It has the same format as the existing :attr:`mopidy.models.Track.date`. - Added :class:`mopidy.models.ModelJSONEncoder` and :func:`mopidy.models.model_json_decoder` for automatic JSON serialization and deserialization of data structures which contains Mopidy models. This is useful for the future HTTP frontend. *Library:* - :meth:`mopidy.core.LibraryController.find_exact` and :meth:`mopidy.core.LibraryController.search` now returns plain lists of tracks instead of playlist objects. - :meth:`mopidy.core.LibraryController.lookup` now returns a list of tracks instead of a single track. This makes it possible to support lookup of artist or album URIs which then can expand to a list of tracks. *Playback:* - The base playback provider has been updated with sane default behavior instead of empty functions. By default, the playback provider now lets GStreamer keep track of the current track's time position. The local backend simply uses the base playback provider without any changes. Any future backend that just feeds URIs to GStreamer to play can also use the base playback provider without any changes. - Removed :attr:`mopidy.core.PlaybackController.track_at_previous`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_previous` instead. - Removed :attr:`mopidy.core.PlaybackController.track_at_next`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_next` instead. - Removed :attr:`mopidy.core.PlaybackController.track_at_eot`. Use :attr:`mopidy.core.PlaybackController.tl_track_at_eot` instead. - Removed :attr:`mopidy.core.PlaybackController.current_tlid`. Use :attr:`mopidy.core.PlaybackController.current_tl_track` instead. *Playlists:* The playlists part of the core API has been revised to be more focused around the playlist URI, and some redundant functionality has been removed: - Renamed "stored playlists" to "playlists" everywhere, including the core API used by frontends. - :attr:`mopidy.core.PlaylistsController.playlists` no longer supports assignment to it. The `playlists` property on the backend layer still does, and all functionality is maintained by assigning to the playlists collections at the backend level. - :meth:`mopidy.core.PlaylistsController.delete` now accepts an URI, and not a playlist object. - :meth:`mopidy.core.PlaylistsController.save` now returns the saved playlist. The returned playlist may differ from the saved playlist, and should thus be used instead of the playlist passed to :meth:`mopidy.core.PlaylistsController.save`. - :meth:`mopidy.core.PlaylistsController.rename` has been removed, since renaming can be done with :meth:`mopidy.core.PlaylistsController.save`. - :meth:`mopidy.core.PlaylistsController.get` has been replaced by :meth:`mopidy.core.PlaylistsController.filter`. - The event :meth:`mopidy.core.CoreListener.playlist_changed` has been changed to include the playlist that was changed. *Tracklist:* - Renamed "current playlist" to "tracklist" everywhere, including the core API used by frontends. - Removed :meth:`mopidy.core.TracklistController.append`. Use :meth:`mopidy.core.TracklistController.add` instead, which is now capable of adding multiple tracks. - :meth:`mopidy.core.TracklistController.get` has been replaced by :meth:`mopidy.core.TracklistController.filter`. - :meth:`mopidy.core.TracklistController.remove` can now remove multiple tracks, and returns the tracks it removed. - When the tracklist is changed, we now trigger the new :meth:`mopidy.core.CoreListener.tracklist_changed` event. Previously we triggered :meth:`mopidy.core.CoreListener.playlist_changed`, which is intended for stored playlists, not the tracklist. *Towards Python 3 support:* - Make the entire code base use unicode strings by default, and only fall back to bytestrings where it is required. Another step closer to Python 3. v0.8.1 (2012-10-30) =================== A small maintenance release to fix a bug introduced in 0.8.0 and update Mopidy to work with Pykka 1.0. **Dependencies** - Pykka >= 1.0 is now required. **Bug fixes** - :issue:`213`: Fix "streaming task paused, reason not-negotiated" errors observed by some users on some Spotify tracks due to a change introduced in 0.8.0. See the issue for a patch that applies to 0.8.0. - :issue:`216`: Volume returned by the MPD command `status` contained a floating point ``.0`` suffix. This bug was introduced with the large audio output and mixer changes in v0.8.0 and broke the MPDroid Android client. It now returns an integer again. v0.8.0 (2012-09-20) =================== This release does not include any major new features. We've done a major cleanup of how audio outputs and audio mixers work, and on the way we've resolved a bunch of related issues. **Audio output and mixer changes** - Removed multiple outputs support. Having this feature currently seems to be more trouble than what it is worth. The :attr:`mopidy.settings.OUTPUTS` setting is no longer supported, and has been replaced with :attr:`mopidy.settings.OUTPUT` which is a GStreamer bin description string in the same format as ``gst-launch`` expects. Default value is ``autoaudiosink``. (Fixes: :issue:`81`, :issue:`115`, :issue:`121`, :issue:`159`) - Switch to pure GStreamer based mixing. This implies that users setup a GStreamer bin with a mixer in it in :attr:`mopidy.settings.MIXER`. The default value is ``autoaudiomixer``, a custom mixer that attempts to find a mixer that will work on your system. If this picks the wrong mixer you can of course override it. Setting the mixer to :class:`None` is also supported. MPD protocol support for volume has also been updated to return -1 when we have no mixer set. ``software`` can be used to force software mixing. - Removed the Denon hardware mixer, as it is not maintained. - Updated the NAD hardware mixer to work in the new GStreamer based mixing regime. Settings are now passed as GStreamer element properties. In practice that means that the following old-style config:: MIXER = u'mopidy.mixers.nad.NadMixer' MIXER_EXT_PORT = u'/dev/ttyUSB0' MIXER_EXT_SOURCE = u'Aux' MIXER_EXT_SPEAKERS_A = u'On' MIXER_EXT_SPEAKERS_B = u'Off' Now is reduced to simply:: MIXER = u'nadmixer port=/dev/ttyUSB0 source=aux speakers-a=on speakers-b=off' The ``port`` property defaults to ``/dev/ttyUSB0``, and the rest of the properties may be left out if you don't want the mixer to adjust the settings on your NAD amplifier when Mopidy is started. **Changes** - When unknown settings are encountered, we now check if it's similar to a known setting, and suggests to the user what we think the setting should have been. - Added :option:`--list-deps` option to the ``mopidy`` command that lists required and optional dependencies, their current versions, and some other information useful for debugging. (Fixes: :issue:`74`) - Added ``tools/debug-proxy.py`` to tee client requests to two backends and diff responses. Intended as a developer tool for checking for MPD protocol changes and various client support. Requires gevent, which currently is not a dependency of Mopidy. - Support tracks with only release year, and not a full release date, like e.g. Spotify tracks. - Default value of ``LOCAL_MUSIC_PATH`` has been updated to be ``$XDG_MUSIC_DIR``, which on most systems this is set to ``$HOME``. Users of local backend that relied on the old default ``~/music`` need to update their settings. Note that the code responsible for finding this music now also ignores UNIX hidden files and folders. - File and path settings now support ``$XDG_CACHE_DIR``, ``$XDG_DATA_DIR`` and ``$XDG_MUSIC_DIR`` substitution. Defaults for such settings have been updated to use this instead of hidden away defaults. - Playback is now done using ``playbin2`` from GStreamer instead of rolling our own. This is the first step towards resolving :issue:`171`. **Bug fixes** - :issue:`72`: Created a Spotify track proxy that will switch to using loaded data as soon as it becomes available. - :issue:`150`: Fix bug which caused some clients to block Mopidy completely. The bug was caused by some clients sending ``close`` and then shutting down the connection right away. This trigged a situation in which the connection cleanup code would wait for an response that would never come inside the event loop, blocking everything else. - :issue:`162`: Fixed bug when the MPD command ``playlistinfo`` is used with a track position. Track position and CPID was intermixed, so it would cause a crash if a CPID matching the track position didn't exist. - Fixed crash on lookup of unknown path when using local backend. - :issue:`189`: ``LOCAL_MUSIC_PATH`` and path handling in rest of settings has been updated so all of the code now uses the correct value. - Fixed incorrect track URIs generated by M3U playlist parsing code. Generated tracks are now relative to ``LOCAL_MUSIC_PATH``. - :issue:`203`: Re-add support for software mixing. v0.7.3 (2012-08-11) =================== A small maintenance release to fix a crash affecting a few users, and a couple of small adjustments to the Spotify backend. **Changes** - Fixed crash when logging :exc:`IOError` exceptions on systems using languages with non-ASCII characters, like French. - Move the default location of the Spotify cache from `~/.cache/mopidy` to `~/.cache/mopidy/spotify`. You can change this by setting :attr:`mopidy.settings.SPOTIFY_CACHE_PATH`. - Reduce time required to update the Spotify cache on startup. One one system/Spotify account, the time from clean cache to ready for use was reduced from 35s to 12s. v0.7.2 (2012-05-07) =================== This is a maintenance release to make Mopidy 0.7 build on systems without all of Mopidy's runtime dependencies, like Launchpad PPAs. **Changes** - Change from version tuple at :attr:`mopidy.VERSION` to :pep:`386` compliant version string at :attr:`mopidy.__version__` to conform to :pep:`396`. v0.7.1 (2012-04-22) =================== This is a maintenance release to make Mopidy 0.7 work with pyspotify >= 1.7. **Changes** - Don't override pyspotify's ``notify_main_thread`` callback. The default implementation is sensible, while our override did nothing. v0.7.0 (2012-02-25) =================== Not a big release with regard to features, but this release got some performance improvements over v0.6, especially for slower Atom systems. It also fixes a couple of other bugs, including one which made Mopidy crash when using GStreamer from the prereleases of Ubuntu 12.04. **Changes** - The MPD command ``playlistinfo`` is now faster, thanks to John Bäckstrand. - Added the method :meth:`mopidy.backends.base.CurrentPlaylistController.length()`, :meth:`mopidy.backends.base.CurrentPlaylistController.index()`, and :meth:`mopidy.backends.base.CurrentPlaylistController.slice()` to reduce the need for copying the entire current playlist from one thread to another. Thanks to John Bäckstrand for pinpointing the issue. - Fix crash on creation of config and cache directories if intermediate directories does not exist. This was especially the case on OS X, where ``~/.config`` doesn't exist for most users. - Fix ``gst.LinkError`` which appeared when using newer versions of GStreamer, e.g. on Ubuntu 12.04 Alpha. (Fixes: :issue:`144`) - Fix crash on mismatching quotation in ``list`` MPD queries. (Fixes: :issue:`137`) - Volume is now reported to be the same as the volume was set to, also when internal rounding have been done due to :attr:`mopidy.settings.MIXER_MAX_VOLUME` has been set to cap the volume. This should make it possible to manage capped volume from clients that only increase volume with one step at a time, like ncmpcpp does. v0.6.1 (2011-12-28) =================== This is a maintenance release to make Mopidy 0.6 work with pyspotify >= 1.5, which Mopidy's develop branch have supported for a long time. This should also make the Debian packages work out of the box again. **Important changes** - pyspotify 1.5 or greater is required. **Changes** - Spotify playlist folder boundaries are now properly detected. In other words, if you use playlist folders, you will no longer get lots of log messages about bad playlists. v0.6.0 (2011-10-09) =================== The development of Mopidy have been quite slow for the last couple of months, but we do have some goodies to release which have been idling in the develop branch since the warmer days of the summer. This release brings support for the MPD ``idle`` command, which makes it possible for a client wait for updates from the server instead of polling every second. Also, we've added support for the MPRIS standard, so that Mopidy can be controlled over D-Bus from e.g. the Ubuntu Sound Menu. Please note that 0.6.0 requires some updated dependencies, as listed under *Important changes* below. **Important changes** - Pykka 0.12.3 or greater is required. - pyspotify 1.4 or greater is required. - All config, data, and cache locations are now based on the XDG spec. - This means that your settings file will need to be moved from ``~/.mopidy/settings.py`` to ``~/.config/mopidy/settings.py``. - Your Spotify cache will now be stored in ``~/.cache/mopidy`` instead of ``~/.mopidy/spotify_cache``. - The local backend's ``tag_cache`` should now be in ``~/.local/share/mopidy/tag_cache``, likewise your playlists will be in ``~/.local/share/mopidy/playlists``. - The local client now tries to lookup where your music is via XDG, it will fall-back to ``~/music`` or use whatever setting you set manually. - The MPD command ``idle`` is now supported by Mopidy for the following subsystems: player, playlist, options, and mixer. (Fixes: :issue:`32`) - A new frontend :mod:`mopidy.frontends.mpris` have been added. It exposes Mopidy through the `MPRIS interface `_ over D-Bus. In practice, this makes it possible to control Mopidy through the `Ubuntu Sound Menu `_. **Changes** - Replace :attr:`mopidy.backends.base.Backend.uri_handlers` with :attr:`mopidy.backends.base.Backend.uri_schemes`, which just takes the part up to the colon of an URI, and not any prefix. - Add Listener API, :mod:`mopidy.listeners`, to be implemented by actors wanting to receive events from the backend. This is a formalization of the ad hoc events the Last.fm scrobbler has already been using for some time. - Replaced all of the MPD network code that was provided by asyncore with custom stack. This change was made to facilitate support for the ``idle`` command, and to reduce the number of event loops being used. - Fix metadata update in Shoutcast streaming. (Fixes: :issue:`122`) - Unescape all incoming MPD requests. (Fixes: :issue:`113`) - Increase the maximum number of results returned by Spotify searches from 32 to 100. - Send Spotify search queries to pyspotify as unicode objects, as required by pyspotify 1.4. (Fixes: :issue:`129`) - Add setting :attr:`mopidy.settings.MPD_SERVER_MAX_CONNECTIONS`. (Fixes: :issue:`134`) - Remove `destroy()` methods from backend controller and provider APIs, as it was not in use and actually not called by any code. Will reintroduce when needed. v0.5.0 (2011-06-15) =================== Since last time we've added support for audio streaming to SHOUTcast servers and fixed the longstanding playlist loading issue in the Spotify backend. As always the release has a bunch of bug fixes and minor improvements. Please note that 0.5.0 requires some updated dependencies, as listed under *Important changes* below. **Important changes** - If you use the Spotify backend, you *must* upgrade to libspotify 0.0.8 and pyspotify 1.3. If you install from APT, libspotify and pyspotify will automatically be upgraded. If you are not installing from APT, follow the instructions at :ref:`installation`. - If you have explicitly set the :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` setting, you must update your settings file. The new setting is named :attr:`mopidy.settings.SPOTIFY_BITRATE` and accepts the integer values 96, 160, and 320. - Mopidy now supports running with 1 to N outputs at the same time. This feature was mainly added to facilitate SHOUTcast support, which Mopidy has also gained. In its current state outputs can not be toggled during runtime. **Changes** - Local backend: - Fix local backend time query errors that where coming from stopped pipeline. (Fixes: :issue:`87`) - Spotify backend: - Thanks to Antoine Pierlot-Garcin's recent work on updating and improving pyspotify, stored playlists will again load when Mopidy starts. The workaround of searching and reconnecting to make the playlists appear are no longer necessary. (Fixes: :issue:`59`) - Track's that are no longer available in Spotify's archives are now "autolinked" to corresponding tracks in other albums, just like the official Spotify clients do. (Fixes: :issue:`34`) - MPD frontend: - Refactoring and cleanup. Most notably, all request handlers now get an instance of :class:`mopidy.frontends.mpd.dispatcher.MpdContext` as the first argument. The new class contains reference to any object in Mopidy the MPD protocol implementation should need access to. - Close the client connection when the command ``close`` is received. - Do not allow access to the command ``kill``. - ``commands`` and ``notcommands`` now have correct output if password authentication is turned on, but the connected user has not been authenticated yet. - Command line usage: - Support passing options to GStreamer. See :option:`--help-gst` for a list of available options. (Fixes: :issue:`95`) - Improve :option:`--list-settings` output. (Fixes: :issue:`91`) - Added :option:`--interactive` for reading missing local settings from ``stdin``. (Fixes: :issue:`96`) - Improve shutdown procedure at CTRL+C. Add signal handler for ``SIGTERM``, which initiates the same shutdown procedure as CTRL+C does. - Tag cache generator: - Made it possible to abort :command:`mopidy-scan` with CTRL+C. - Fixed bug regarding handling of bad dates. - Use :mod:`logging` instead of ``print`` statements. - Found and worked around strange WMA metadata behaviour. - Backend API: - Calling on :meth:`mopidy.backends.base.playback.PlaybackController.next` and :meth:`mopidy.backends.base.playback.PlaybackController.previous` no longer implies that playback should be started. The playback state--whether playing, paused or stopped--will now be kept. - The method :meth:`mopidy.backends.base.playback.PlaybackController.change_track` has been added. Like ``next()``, and ``prev()``, it changes the current track without changing the playback state. v0.4.1 (2011-05-06) =================== This is a bug fix release fixing audio problems on older GStreamer and some minor bugs. **Bug fixes** - Fix broken audio on at least GStreamer 0.10.30, which affects Ubuntu 10.10. The GStreamer `appsrc` bin wasn't being linked due to lack of default caps. (Fixes: :issue:`85`) - Fix crash in :mod:`mopidy.mixers.nad` that occures at startup when the :mod:`io` module is available. We used an `eol` keyword argument which is supported by :meth:`serial.FileLike.readline`, but not by :meth:`io.RawBaseIO.readline`. When the :mod:`io` module is available, it is used by PySerial instead of the `FileLike` implementation. - Fix UnicodeDecodeError in MPD frontend on non-english locale. Thanks to Antoine Pierlot-Garcin for the patch. (Fixes: :issue:`88`) - Do not create Pykka proxies that are not going to be used in :mod:`mopidy.core`. The underlying actor may already intentionally be dead, and thus the program may crash on creating a proxy it doesn't need. Combined with the Pykka 0.12.2 release this fixes a crash in the Last.fm frontend which may occur when all dependencies are installed, but the frontend isn't configured. (Fixes: :issue:`84`) v0.4.0 (2011-04-27) =================== Mopidy 0.4.0 is another release without major feature additions. In 0.4.0 we've fixed a bunch of issues and bugs, with the help of several new contributors who are credited in the changelog below. The major change of 0.4.0 is an internal refactoring which clears way for future features, and which also make Mopidy work on Python 2.7. In other words, Mopidy 0.4.0 works on Ubuntu 11.04 and Arch Linux. Please note that 0.4.0 requires some updated dependencies, as listed under *Important changes* below. Also, the known bug in the Spotify playlist loading from Mopidy 0.3.0 is still present. .. warning:: Known bug in Spotify playlist loading There is a known bug in the loading of Spotify playlists. To avoid the bug, follow the simple workaround described at :issue:`59`. **Important changes** - Mopidy now depends on `Pykka `_ >=0.12. If you install from APT, Pykka will automatically be installed. If you are not installing from APT, you may install Pykka from PyPI:: sudo pip install -U Pykka - If you use the Spotify backend, you *should* upgrade to libspotify 0.0.7 and the latest pyspotify from the Mopidy developers. If you install from APT, libspotify and pyspotify will automatically be upgraded. If you are not installing from APT, follow the instructions at :ref:`installation`. **Changes** - Mopidy now use Pykka actors for thread management and inter-thread communication. The immediate advantage of this is that Mopidy now works on Python 2.7, which is the default on e.g. Ubuntu 11.04. (Fixes: :issue:`66`) - Spotify backend: - Fixed multiple segmentation faults due to bugs in Pyspotify. Thanks to Antoine Pierlot-Garcin and Jamie Kirkpatrick for patches to Pyspotify. - Better error messages on wrong login or network problems. Thanks to Antoine Pierlot-Garcin for patches to Mopidy and Pyspotify. (Fixes: :issue:`77`) - Reduce log level for trivial log messages from warning to info. (Fixes: :issue:`71`) - Pause playback on network connection errors. (Fixes: :issue:`65`) - Local backend: - Fix crash in :command:`mopidy-scan` if a track has no artist name. Thanks to Martins Grunskis for test and patch and "octe" for patch. - Fix crash in `tag_cache` parsing if a track has no total number of tracks in the album. Thanks to Martins Grunskis for the patch. - MPD frontend: - Add support for "date" queries to both the ``find`` and ``search`` commands. This makes media library browsing in ncmpcpp work, though very slow due to all the meta data requests to Spotify. - Add support for ``play "-1"`` when in playing or paused state, which fixes resume and addition of tracks to the current playlist while playing for the MPoD client. - Fix bug where ``status`` returned ``song: None``, which caused MPDroid to crash. (Fixes: :issue:`69`) - Gracefully fallback to IPv4 sockets on systems that supports IPv6, but has turned it off. (Fixes: :issue:`75`) - GStreamer output: - Use ``uridecodebin`` for playing audio from both Spotify and the local backend. This contributes to support for multiple backends simultaneously. - Settings: - Fix crash on ``--list-settings`` on clean installation. Thanks to Martins Grunskis for the bug report and patch. (Fixes: :issue:`63`) - Packaging: - Replace test data symlinks with real files to avoid symlink issues when installing with pip. (Fixes: :issue:`68`) - Debugging: - Include platform, architecture, Linux distribution, and Python version in the debug log, to ease debugging of issues with attached debug logs. v0.3.1 (2011-01-22) =================== A couple of fixes to the 0.3.0 release is needed to get a smooth installation. **Bug fixes** - The Spotify application key was missing from the Python package. - Installation of the Python package as a normal user failed because it did not have permissions to install ``mopidy.desktop``. The file is now only installed if the installation is executed as the root user. v0.3.0 (2011-01-22) =================== Mopidy 0.3.0 brings a bunch of small changes all over the place, but no large changes. The main features are support for high bitrate audio from Spotify, and MPD password authentication. Regarding the docs, we've improved the :ref:`installation instructions ` and done a bit of testing of the available :ref:`Android ` and :ref:`iOS clients ` for MPD. Please note that 0.3.0 requires some updated dependencies, as listed under *Important changes* below. Also, there is a known bug in the Spotify playlist loading, as described below. As the bug will take some time to fix and has a known workaround, we did not want to delay the release while waiting for a fix to this problem. .. warning:: Known bug in Spotify playlist loading There is a known bug in the loading of Spotify playlists. This bug affects both Mopidy 0.2.1 and 0.3.0, given that you use libspotify 0.0.6. To avoid the bug, either use Mopidy 0.2.1 with libspotify 0.0.4, or use either Mopidy version with libspotify 0.0.6 and follow the simple workaround described at :issue:`59`. **Important changes** - If you use the Spotify backend, you need to upgrade to libspotify 0.0.6 and the latest pyspotify from the Mopidy developers. Follow the instructions at :ref:`installation`. - If you use the Last.fm frontend, you need to upgrade to pylast 0.5.7. Run ``sudo pip install --upgrade pylast`` or install Mopidy from APT. **Changes** - Spotify backend: - Support high bitrate (320k) audio. Set the new setting :attr:`mopidy.settings.SPOTIFY_HIGH_BITRATE` to :class:`True` to switch to high bitrate audio. - Rename :mod:`mopidy.backends.libspotify` to :mod:`mopidy.backends.spotify`. If you have set :attr:`mopidy.settings.BACKENDS` explicitly, you may need to update the setting's value. - Catch and log error caused by playlist folder boundaries being threated as normal playlists. More permanent fix requires support for checking playlist types in pyspotify (see :issue:`62`). - Fix crash on failed lookup of track by URI. (Fixes: :issue:`60`) - Local backend: - Add :command:`mopidy-scan` command to generate ``tag_cache`` files without any help from the original MPD server. See :ref:`generating-a-tag-cache` for instructions on how to use it. - Fix support for UTF-8 encoding in tag caches. - MPD frontend: - Add support for password authentication. See :attr:`mopidy.settings.MPD_SERVER_PASSWORD` and :ref:`use-mpd-on-a-network` for details on how to use it. (Fixes: :issue:`41`) - Support ``setvol 50`` without quotes around the argument. Fixes volume control in Droid MPD. - Support ``seek 1 120`` without quotes around the arguments. Fixes seek in Droid MPD. - Last.fm frontend: - Update to use Last.fm's new Scrobbling 2.0 API, as the old Submissions Protocol 1.2.1 is deprecated. (Fixes: :issue:`33`) - Fix crash when track object does not contain all the expected meta data. - Fix crash when response from Last.fm cannot be decoded as UTF-8. (Fixes: :issue:`37`) - Fix crash when response from Last.fm contains invalid XML. - Fix crash when response from Last.fm has an invalid HTTP status line. - Mixers: - Support use of unicode strings for settings specific to :mod:`mopidy.mixers.nad`. - Settings: - Automatically expand the "~" characted to the user's home directory and make the path absolute for settings with names ending in ``_PATH`` or ``_FILE``. - Rename the following settings. The settings validator will warn you if you need to change your local settings. - ``LOCAL_MUSIC_FOLDER`` to :attr:`mopidy.settings.LOCAL_MUSIC_PATH` - ``LOCAL_PLAYLIST_FOLDER`` to :attr:`mopidy.settings.LOCAL_PLAYLIST_PATH` - ``LOCAL_TAG_CACHE`` to :attr:`mopidy.settings.LOCAL_TAG_CACHE_FILE` - ``SPOTIFY_LIB_CACHE`` to :attr:`mopidy.settings.SPOTIFY_CACHE_PATH` - Fix bug which made settings set to :class:`None` or 0 cause a :exc:`mopidy.SettingsError` to be raised. - Packaging and distribution: - Setup APT repository and crate Debian packages of Mopidy. See :ref:`installation` for instructions for how to install Mopidy, including all dependencies, from APT. - Install ``mopidy.desktop`` file that makes Mopidy available from e.g. Gnome application menus. - API: - Rename and generalize ``Playlist._with(**kwargs)`` to :meth:`mopidy.models.ImmutableObject.copy`. - Add ``musicbrainz_id`` field to :class:`mopidy.models.Artist`, :class:`mopidy.models.Album`, and :class:`mopidy.models.Track`. - Prepare for multi-backend support (see :issue:`40`) by introducing the :ref:`provider concept `. Split the backend API into a :ref:`backend controller API ` (for frontend use) and a :ref:`backend provider API ` (for backend implementation use), which includes the following changes: - Rename ``BaseBackend`` to :class:`mopidy.backends.base.Backend`. - Rename ``BaseCurrentPlaylistController`` to :class:`mopidy.backends.base.CurrentPlaylistController`. - Split ``BaseLibraryController`` to :class:`mopidy.backends.base.LibraryController` and :class:`mopidy.backends.base.BaseLibraryProvider`. - Split ``BasePlaybackController`` to :class:`mopidy.backends.base.PlaybackController` and :class:`mopidy.backends.base.BasePlaybackProvider`. - Split ``BaseStoredPlaylistsController`` to :class:`mopidy.backends.base.StoredPlaylistsController` and :class:`mopidy.backends.base.BaseStoredPlaylistsProvider`. - Move ``BaseMixer`` to :class:`mopidy.mixers.base.BaseMixer`. - Add docs for the current non-stable output API, :class:`mopidy.outputs.base.BaseOutput`. v0.2.1 (2011-01-07) =================== This is a maintenance release without any new features. **Bug fixes** - Fix crash in :mod:`mopidy.frontends.lastfm` which occurred at playback if either :mod:`pylast` was not installed or the Last.fm scrobbling was not correctly configured. The scrobbling thread now shuts properly down at failure. v0.2.0 (2010-10-24) =================== In Mopidy 0.2.0 we've added a `Last.fm `_ scrobbling support, which means that Mopidy now can submit meta data about the tracks you play to your Last.fm profile. See :mod:`mopidy.frontends.lastfm` for details on new dependencies and settings. If you use Mopidy's Last.fm support, please join the `Mopidy group at Last.fm `_. With the exception of the work on the Last.fm scrobbler, there has been a couple of quiet months in the Mopidy camp. About the only thing going on, has been stabilization work and bug fixing. All bugs reported on GitHub, plus some, have been fixed in 0.2.0. Thus, we hope this will be a great release! We've worked a bit on OS X support, but not all issues are completely solved yet. :issue:`25` is the one that is currently blocking OS X support. Any help solving it will be greatly appreciated! Finally, please :ref:`update your pyspotify installation ` when upgrading to Mopidy 0.2.0. The latest pyspotify got a fix for the segmentation fault that occurred when playing music and searching at the same time, thanks to Valentin David. **Important changes** - Added a Last.fm scrobbler. See :mod:`mopidy.frontends.lastfm` for details. **Changes** - Logging and command line options: - Simplify the default log format, :attr:`mopidy.settings.CONSOLE_LOG_FORMAT`. From a user's point of view: Less noise, more information. - Rename the :option:`--dump` command line option to :option:`--save-debug-log`. - Rename setting :attr:`mopidy.settings.DUMP_LOG_FORMAT` to :attr:`mopidy.settings.DEBUG_LOG_FORMAT` and use it for :option:`--verbose` too. - Rename setting :attr:`mopidy.settings.DUMP_LOG_FILENAME` to :attr:`mopidy.settings.DEBUG_LOG_FILENAME`. - MPD frontend: - MPD command ``list`` now supports queries by artist, album name, and date, as used by e.g. the Ario client. (Fixes: :issue:`20`) - MPD command ``add ""`` and ``addid ""`` now behaves as expected. (Fixes :issue:`16`) - MPD command ``playid "-1"`` now correctly resumes playback if paused. - Random mode: - Fix wrong behavior on end of track and next after random mode has been used. (Fixes: :issue:`18`) - Fix infinite recursion loop crash on playback of non-playable tracks when in random mode. (Fixes :issue:`17`) - Fix assertion error that happened if one removed tracks from the current playlist, while in random mode. (Fixes :issue:`22`) - Switched from using subprocesses to threads. (Fixes: :issue:`14`) - :mod:`mopidy.outputs.gstreamer`: Set ``caps`` on the ``appsrc`` bin before use. This makes sound output work with GStreamer >= 0.10.29, which includes the versions used in Ubuntu 10.10 and on OS X if using Homebrew. (Fixes: :issue:`21`, :issue:`24`, contributes to :issue:`14`) - Improved handling of uncaught exceptions in threads. The entire process should now exit immediately. v0.1.0 (2010-08-23) =================== After three weeks of long nights and sprints we're finally pleased enough with the state of Mopidy to remove the alpha label, and do a regular release. Mopidy 0.1.0 got important improvements in search functionality, working track position seeking, no known stability issues, and greatly improved MPD client support. There are lots of changes since 0.1.0a3, and we urge you to at least read the *important changes* below. This release does not support OS X. We're sorry about that, and are working on fixing the OS X issues for a future release. You can track the progress at :issue:`14`. **Important changes** - License changed from GPLv2 to Apache License, version 2.0. - GStreamer is now a required dependency. See our :ref:`GStreamer installation docs `. - :mod:`mopidy.backends.libspotify` is now the default backend. :mod:`mopidy.backends.despotify` is no longer available. This means that you need to install the :ref:`dependencies for libspotify `. - If you used :mod:`mopidy.backends.libspotify` previously, pyspotify must be updated when updating to this release, to get working seek functionality. - :attr:`mopidy.settings.SERVER_HOSTNAME` and :attr:`mopidy.settings.SERVER_PORT` has been renamed to :attr:`mopidy.settings.MPD_SERVER_HOSTNAME` and :attr:`mopidy.settings.MPD_SERVER_PORT` to allow for multiple frontends in the future. **Changes** - Exit early if not Python >= 2.6, < 3. - Validate settings at startup and print useful error messages if the settings has not been updated or anything is misspelled. - Add command line option :option:`--list-settings` to print the currently active settings. - Include Sphinx scripts for building docs, pylintrc, tests and test data in the packages created by ``setup.py`` for i.e. PyPI. - MPD frontend: - Search improvements, including support for multi-word search. - Fixed ``play "-1"`` and ``playid "-1"`` behaviour when playlist is empty or when a current track is set. - Support ``plchanges "-1"`` to work better with MPDroid. - Support ``pause`` without arguments to work better with MPDroid. - Support ``plchanges``, ``play``, ``consume``, ``random``, ``repeat``, and ``single`` without quotes to work better with BitMPC. - Fixed deletion of the currently playing track from the current playlist, which crashed several clients. - Implement ``seek`` and ``seekid``. - Fix ``playlistfind`` output so the correct song is played when playing songs directly from search results in GMPC. - Fix ``load`` so that one can append a playlist to the current playlist, and make it return the correct error message if the playlist is not found. - Support for single track repeat added. (Fixes: :issue:`4`) - Relocate from :mod:`mopidy.mpd` to :mod:`mopidy.frontends.mpd`. - Split gigantic protocol implementation into eleven modules. - Rename ``mopidy.frontends.mpd.{serializer => translator}`` to match naming in backends. - Remove setting :attr:`mopidy.settings.SERVER` and :attr:`mopidy.settings.FRONTEND` in favour of the new :attr:`mopidy.settings.FRONTENDS`. - Run MPD server in its own process. - Backends: - Rename :mod:`mopidy.backends.gstreamer` to :mod:`mopidy.backends.local`. - Remove :mod:`mopidy.backends.despotify`, as Despotify is little maintained and the Libspotify backend is working much better. (Fixes: :issue:`9`, :issue:`10`, :issue:`13`) - A Spotify application key is now bundled with the source. :attr:`mopidy.settings.SPOTIFY_LIB_APPKEY` is thus removed. - If failing to play a track, playback will skip to the next track. - Both :mod:`mopidy.backends.libspotify` and :mod:`mopidy.backends.local` have been rewritten to use the new common GStreamer audio output module, :mod:`mopidy.outputs.gstreamer`. - Mixers: - Added new :mod:`mopidy.mixers.gstreamer_software.GStreamerSoftwareMixer` which now is the default mixer on all platforms. - New setting :attr:`mopidy.settings.MIXER_MAX_VOLUME` for capping the maximum output volume. - Backend API: - Relocate from :mod:`mopidy.backends` to :mod:`mopidy.backends.base`. - The ``id`` field of :class:`mopidy.models.Track` has been removed, as it is no longer needed after the CPID refactoring. - :meth:`mopidy.backends.base.BaseBackend()` now accepts an ``output_queue`` which it can use to send messages (i.e. audio data) to the output process. - :meth:`mopidy.backends.base.BaseLibraryController.find_exact()` now accepts keyword arguments of the form ``find_exact(artist=['foo'], album=['bar'])``. - :meth:`mopidy.backends.base.BaseLibraryController.search()` now accepts keyword arguments of the form ``search(artist=['foo', 'fighters'], album=['bar', 'grooves'])``. - :meth:`mopidy.backends.base.BaseCurrentPlaylistController.append()` replaces :meth:`mopidy.backends.base.BaseCurrentPlaylistController.load()`. Use :meth:`mopidy.backends.base.BaseCurrentPlaylistController.clear()` if you want to clear the current playlist. - The following fields in :class:`mopidy.backends.base.BasePlaybackController` has been renamed to reflect their relation to methods called on the controller: - ``next_track`` to ``track_at_next`` - ``next_cp_track`` to ``cp_track_at_next`` - ``previous_track`` to ``track_at_previous`` - ``previous_cp_track`` to ``cp_track_at_previous`` - :attr:`mopidy.backends.base.BasePlaybackController.track_at_eot` and :attr:`mopidy.backends.base.BasePlaybackController.cp_track_at_eot` has been added to better handle the difference between the user pressing next and the current track ending. - Rename :meth:`mopidy.backends.base.BasePlaybackController.new_playlist_loaded_callback()` to :meth:`mopidy.backends.base.BasePlaybackController.on_current_playlist_change()`. - Rename :meth:`mopidy.backends.base.BasePlaybackController.end_of_track_callback()` to :meth:`mopidy.backends.base.BasePlaybackController.on_end_of_track()`. - Remove :meth:`mopidy.backends.base.BaseStoredPlaylistsController.search()` since it was barely used, untested, and we got no use case for non-exact search in stored playlists yet. Use :meth:`mopidy.backends.base.BaseStoredPlaylistsController.get()` instead. v0.1.0a3 (2010-08-03) ===================== In the last two months, Mopidy's MPD frontend has gotten lots of stability fixes and error handling improvements, proper support for having the same track multiple times in a playlist, and support for IPv6. We have also fixed the choppy playback on the libspotify backend. For the road ahead of us, we got an updated release roadmap with our goals for the 0.1 to 0.3 releases. Enjoy the best alpha relase of Mopidy ever :-) **Changes** - MPD frontend: - Support IPv6. - ``addid`` responds properly on errors instead of crashing. - ``commands`` support, which makes RelaXXPlayer work with Mopidy. (Fixes: :issue:`6`) - Does no longer crash on invalid data, i.e. non-UTF-8 data. - ``ACK`` error messages are now MPD-compliant, which should make clients handle errors from Mopidy better. - Requests to existing commands with wrong arguments are no longer reported as unknown commands. - ``command_list_end`` before ``command_list_start`` now returns unknown command error instead of crashing. - ``list`` accepts field argument without quotes and capitalized, to work with GMPC and ncmpc. - ``noidle`` command now returns ``OK`` instead of an error. Should make some clients work a bit better. - Having multiple identical tracks in a playlist is now working properly. (CPID refactoring) - Despotify backend: - Catch and log :exc:`spytify.SpytifyError`. (Fixes: :issue:`11`) - Libspotify backend: - Fix choppy playback using the Libspotify backend by using blocking ALSA mode. (Fixes: :issue:`7`) - Backend API: - A new data structure called ``cp_track`` is now used in the current playlist controller and the playback controller. A ``cp_track`` is a two-tuple of (CPID integer, :class:`mopidy.models.Track`), identifying an instance of a track uniquely within the current playlist. - :meth:`mopidy.backends.BaseCurrentPlaylistController.load()` now accepts lists of :class:`mopidy.models.Track` instead of :class:`mopidy.models.Playlist`, as none of the other fields on the ``Playlist`` model was in use. - :meth:`mopidy.backends.BaseCurrentPlaylistController.add()` now returns the ``cp_track`` added to the current playlist. - :meth:`mopidy.backends.BaseCurrentPlaylistController.remove()` now takes criterias, just like :meth:`mopidy.backends.BaseCurrentPlaylistController.get()`. - :meth:`mopidy.backends.BaseCurrentPlaylistController.get()` now returns a ``cp_track``. - :attr:`mopidy.backends.BaseCurrentPlaylistController.tracks` is now read-only. Use the methods to change its contents. - :attr:`mopidy.backends.BaseCurrentPlaylistController.cp_tracks` is a read-only list of ``cp_track``. Use the methods to change its contents. - :attr:`mopidy.backends.BasePlaybackController.current_track` is now just for convenience and read-only. To set the current track, assign a ``cp_track`` to :attr:`mopidy.backends.BasePlaybackController.current_cp_track`. - :attr:`mopidy.backends.BasePlaybackController.current_cpid` is the read-only CPID of the current track. - :attr:`mopidy.backends.BasePlaybackController.next_cp_track` is the next ``cp_track`` in the playlist. - :attr:`mopidy.backends.BasePlaybackController.previous_cp_track` is the previous ``cp_track`` in the playlist. - :meth:`mopidy.backends.BasePlaybackController.play()` now takes a ``cp_track``. v0.1.0a2 (2010-06-02) ===================== It has been a rather slow month for Mopidy, but we would like to keep up with the established pace of at least a release per month. **Changes** - Improvements to MPD protocol handling, making Mopidy work much better with a group of clients, including ncmpc, MPoD, and Theremin. - New command line flag :option:`--dump` for dumping debug log to ``dump.log`` in the current directory. - New setting :attr:`mopidy.settings.MIXER_ALSA_CONTROL` for forcing what ALSA control :class:`mopidy.mixers.alsa.AlsaMixer` should use. v0.1.0a1 (2010-05-04) ===================== Since the previous release Mopidy has seen about 300 commits, more than 200 new tests, a libspotify release, and major feature additions to Spotify. The new releases from Spotify have lead to updates to our dependencies, and also to new bugs in Mopidy. Thus, this is primarily a bugfix release, even though the not yet finished work on a GStreamer backend have been merged. All users are recommended to upgrade to 0.1.0a1, and should at the same time ensure that they have the latest versions of our dependencies: Despotify r508 if you are using DespotifyBackend, and pyspotify 1.1 with libspotify 0.0.4 if you are using LibspotifyBackend. As always, report problems at our IRC channel or our issue tracker. Thanks! **Changes** - Backend API changes: - Removed ``backend.playback.volume`` wrapper. Use ``backend.mixer.volume`` directly. - Renamed ``backend.playback.playlist_position`` to ``current_playlist_position`` to match naming of ``current_track``. - Replaced ``get_by_id()`` with a more flexible ``get(**criteria)``. - Merged the ``gstreamer`` branch from Thomas Adamcik: - More than 200 new tests, and thus several bug fixes to existing code. - Several new generic features, like shuffle, consume, and playlist repeat. (Fixes: :issue:`3`) - **[Work in Progress]** A new backend for playing music from a local music archive using the GStreamer library. - Made :class:`mopidy.mixers.alsa.AlsaMixer` work on machines without a mixer named "Master". - Make :class:`mopidy.backends.DespotifyBackend` ignore local files in playlists (feature added in Spotify 0.4.3). Reported by Richard Haugen Olsen. - And much more. v0.1.0a0 (2010-03-27) ===================== "*Release early. Release often. Listen to your customers.*" wrote Eric S. Raymond in *The Cathedral and the Bazaar*. Three months of development should be more than enough. We have more to do, but Mopidy is working and usable. 0.1.0a0 is an alpha release, which basicly means we will still change APIs, add features, etc. before the final 0.1.0 release. But the software is usable as is, so we release it. Please give it a try and give us feedback, either at our IRC channel or through the `issue tracker `_. Thanks! **Changes** - Initial version. No changelog available. mopidy-0.17.0/docs/clients/000077500000000000000000000000001224420023200154605ustar00rootroot00000000000000mopidy-0.17.0/docs/clients/http.rst000066400000000000000000000027271224420023200172010ustar00rootroot00000000000000.. _http-clients: ************ HTTP clients ************ Mopidy added an :ref:`HTTP frontend ` and an :ref:`HTTP API ` in 0.10 which together provides the building blocks needed for creating web clients for Mopidy with the help of a WebSocket and a JavaScript library provided by Mopidy. This page will list any Mopidy web clients using the HTTP frontend. If you've created one, please notify us so we can include your client on this page. See :ref:`http-api` for details on how to build your own web client. woutervanwijk/Mopidy-Webclient ============================== .. image:: /_static/woutervanwijk-mopidy-webclient.png :width: 382 :height: 621 The first web client for Mopidy is still under development, but is already very usable. It targets both desktop and mobile browsers. The web client used for the `Pi Musicbox `_ is also available for other users of Mopidy. See https://github.com/woutervanwijk/Mopidy-WebClient for details. Mopidy Lux ========== .. image:: /_static/dz0ny-mopidy-lux.png :width: 1000 :height: 645 New web client developed by Janez Troha. See https://github.com/dz0ny/mopidy-lux for details. JukePi ====== New web client developed by Meantime IT in the UK for their office jukebox. See https://github.com/meantimeit/jukepi for details. Other web clients ================= For Mopidy web clients using Mopidy's MPD frontend instead of HTTP, see :ref:`mpd-web-clients`. mopidy-0.17.0/docs/clients/index.rst000066400000000000000000000000711224420023200173170ustar00rootroot00000000000000******* Clients ******* .. toctree:: :glob: ** mopidy-0.17.0/docs/clients/mpd.rst000066400000000000000000000217551224420023200170040ustar00rootroot00000000000000.. _mpd-clients: *********** MPD clients *********** This is a list of MPD clients we either know works well with Mopidy, or that we know won't work well. For a more exhaustive list of MPD clients, see http://mpd.wikia.com/wiki/Clients. .. contents:: Contents :local: Test procedure ============== In some cases, we've used the following test procedure to compare the feature completeness of clients: #. Connect to Mopidy #. Search for "foo", with search type "any" if it can be selected #. Add "The Pretender" from the search results to the current playlist #. Start playback #. Pause and resume playback #. Adjust volume #. Find a playlist and append it to the current playlist #. Skip to next track #. Skip to previous track #. Select the last track from the current playlist #. Turn on repeat mode #. Seek to 10 seconds or so before the end of the track #. Wait for the end of the track and confirm that playback continues at the start of the playlist #. Turn off repeat mode #. Turn on random mode #. Skip to next track and confirm that it random mode works #. Turn off random mode #. Stop playback #. Check if the app got support for single mode and consume mode #. Kill Mopidy and confirm that the app handles it without crashing Console clients =============== ncmpcpp ------- A console client that works well with Mopidy, and is regularly used by Mopidy developers. .. image:: /_static/mpd-client-ncmpcpp.png :width: 575 :height: 426 Search does not work in the "Match if tag contains search phrase (regexes supported)" mode because the client tries to fetch all known metadata and do the search on the client side. The two other search modes works nicely, so this is not a problem. ncmpc ----- A console client. Works with Mopidy 0.6 and upwards. Uses the ``idle`` MPD command, but in a resource inefficient way. mpc --- A command line client. Version 0.16 and upwards seems to work nicely with Mopidy. Graphical clients ================= GMPC ---- `GMPC `_ is a graphical MPD client (GTK+) which works well with Mopidy. .. image:: /_static/mpd-client-gmpc.png :width: 1000 :height: 565 GMPC may sometimes requests a lot of meta data of related albums, artists, etc. This takes more time with Mopidy, which needs to query Spotify for the data, than with a normal MPD server, which has a local cache of meta data. Thus, GMPC may sometimes feel frozen, but usually you just need to give it a bit of slack before it will catch up. Sonata ------ `Sonata `_ is a graphical MPD client (GTK+). It generally works well with Mopidy, except for search. .. image:: /_static/mpd-client-sonata.png :width: 475 :height: 424 When you search in Sonata, it only sends the first to letters of the search query to Mopidy, and then does the rest of the filtering itself on the client side. Since Spotify has a collection of millions of tracks and they only return the first 100 hits for any search query, searching for two-letter combinations seldom returns any useful results. See :issue:`1` and the closed `Sonata bug`_ for details. .. _Sonata bug: http://developer.berlios.de/feature/?func=detailfeature&feature_id=5038&group_id=7323 Theremin -------- `Theremin `_ is a graphical MPD client for OS X. It is unmaintained, but generally works well with Mopidy. .. _android_mpd_clients: Android clients =============== We've tested all five MPD clients we could find for Android with Mopidy 0.8.1 on a Samsung Galaxy Nexus with Android 4.1.2, using our standard test procedure. MPDroid ------- Test date: 2012-11-06 Tested version: 1.03.1 (released 2012-10-16) .. image:: /_static/mpd-client-mpdroid.jpg :width: 288 :height: 512 You can get `MPDroid from Google Play `_. - MPDroid started out as a fork of PMix, and is now much better. - MPDroid's user interface looks nice. - Everything in the test procedure works. - In contrast to all other Android clients, MPDroid does support single mode or consume mode. - When Mopidy is killed, MPDroid handles it gracefully and asks if you want to try to reconnect. MPDroid is a good MPD client, and really the only one we can recommend. BitMPC ------ Test date: 2012-11-06 Tested version: 1.0.0 (released 2010-04-12) You can get `BitMPC from Google Play `_. - The user interface lacks some finishing touches. E.g. you can't enter a hostname for the server. Only IPv4 addresses are allowed. - When we last tested the same version of BitMPC using Android 2.1: - All features exercised in the test procedure worked. - BitMPC lacked support for single mode and consume mode. - BitMPC crashed if Mopidy was killed or crashed. - When we tried to test using Android 4.1.1, BitMPC started and connected to Mopidy without problems, but the app crashed as soon as we fired off our search, and continued to crash on startup after that. In conclusion, BitMPC is usable if you got an older Android phone and don't care about looks. For newer Android versions, BitMPC will probably not work as it hasn't been maintained for 2.5 years. Droid MPD Client ---------------- Test date: 2012-11-06 Tested version: 1.4.0 (released 2011-12-20) You can get `Droid MPD Client from Google Play `_. - No intutive way to ask the app to connect to the server after adding the server hostname to the settings. - To find the search functionality, you have to select the menu, then "Playlist manager", then the search tab. I do not understand why search is hidden inside "Playlist manager". - The tabs "Artists" and "Albums" did not contain anything, and did not cause any requests. - The tab "Folders" showed a spinner and said "Updating data..." but did not send any requests. - Searching for "foo" did nothing. No request was sent to the server. - Droid MPD client does not support single mode or consume mode. - Not able to complete the test procedure, due to the above problems. In conclusion, not a client we can recommend. PMix ---- Test date: 2012-11-06 Tested version: 0.4.0 (released 2010-03-06) You can get `PMix from Google Play `_. PMix haven't been updated for 2.5 years, and has less working features than it's fork MPDroid. Ignore PMix and use MPDroid instead. MPD Remote ---------- Test date: 2012-11-06 Tested version: 1.0 (released 2012-05-01) You can get `MPD Remote from Google Play `_. This app looks terrible in the screen shots, got just 100+ downloads, and got a terrible rating. I honestly didn't take the time to test it. .. _ios_mpd_clients: iOS clients =========== MPoD ---- Test date: 2012-11-06 Tested version: 1.7.1 .. image:: /_static/mpd-client-mpod.jpg :width: 320 :height: 480 The `MPoD `_ iPhone/iPod Touch app can be installed from `MPoD at iTunes Store `_. - The user interface looks nice. - All features exercised in the test procedure worked with MPaD, except seek, which I didn't figure out to do. - Search only works in the "Browse" tab, and not under in the "Artist", "Album", or "Song" tabs. For the tabs where search doesn't work, no queries are sent to Mopidy when searching. - Single mode and consume mode is supported. MPaD ---- Test date: 2012-11-06 Tested version: 1.7.1 .. image:: /_static/mpd-client-mpad.jpg :width: 480 :height: 360 The `MPaD `_ iPad app can be purchased from `MPaD at iTunes Store `_ - The user interface looks nice, though I would like to be able to view the current playlist in the large part of the split view. - All features exercised in the test procedure worked with MPaD. - Search only works in the "Browse" tab, and not under in the "Artist", "Album", or "Song" tabs. For the tabs where search doesn't work, no queries are sent to Mopidy when searching. - Single mode and consume mode is supported. - The server menu can be very slow top open, and there is no visible feedback when waiting for the connection to a server to succeed. .. _mpd-web-clients: Web clients =========== The following web clients use the MPD protocol to communicate with Mopidy. For other web clients, see :ref:`http-clients`. Rompr ----- .. image:: /_static/rompr.png :width: 557 :height: 600 `Rompr `_ is a web based MPD client. `mrvanes `_, a Mopidy and Rompr user, said: "These projects are a real match made in heaven." Partify ------- `Partify `_ is a web based MPD client focusing on making music playing collaborative and social. mopidy-0.17.0/docs/clients/mpris.rst000066400000000000000000000056661224420023200173610ustar00rootroot00000000000000.. _mpris-clients: ************* MPRIS clients ************* `MPRIS `_ is short for Media Player Remote Interfacing Specification. It's a spec that describes a standard D-Bus interface for making media players available to other applications on the same system. The MPRIS frontend provided by the `Mopidy-MPRIS extension `_ currently implements all required parts of the MPRIS spec, plus the optional playlist interface. It does not implement the optional tracklist interface. .. _ubuntu-sound-menu: Ubuntu Sound Menu ================= The `Ubuntu Sound Menu `_ is the default sound menu in Ubuntu since 10.10 or 11.04. By default, it only includes the Rhytmbox music player, but many other players can integrate with the sound menu, including the official Spotify player and Mopidy. .. image:: /_static/ubuntu-sound-menu.png :height: 480 :width: 955 If you install Mopidy from apt.mopidy.com, the sound menu should work out of the box. If you install Mopidy in any other way, you need to make sure that the file located at ``data/mopidy.desktop`` in the Mopidy git repo is installed as ``/usr/share/applications/mopidy.desktop``, and that the properties ``TryExec`` and ``Exec`` in the file points to an existing executable file, preferably your Mopidy executable. If this isn't in place, the sound menu will not detect that Mopidy is running. Next, Mopidy's MPRIS frontend must be running for the sound menu to be able to control Mopidy. The frontend is enabled by default, so as long as you have all its dependencies available, you should be good to go. Keep an eye out for warnings or errors from the MPRIS frontend when you start Mopidy, since it may fail because of missing dependencies or because Mopidy is started outside of X; the frontend won't work if ``$DISPLAY`` isn't set when Mopidy is started. Under normal use, if Mopidy isn't running and you open the menu and click on "Mopidy Music Server", a terminal window will open and automatically start Mopidy. If Mopidy is already running, you'll see that Mopidy is marked with an arrow to the left of its name, like in the screen shot above, and the player controls will be visible. Mopidy doesn't support the MPRIS spec's optional playlist interface yet, so you'll not be able to select what track to play from the sound menu. If you use an MPD client to queue a playlist, you can use the sound menu to check what you're currently playing, pause, resume, and skip to the next and previous track. In summary, Mopidy's sound menu integration is currently not a full featured client, but it's a convenient addition to an MPD client since it's always easily available on Unity's menu bar. Rygel ===== Rygel is an application that will translate between Mopidy's MPRIS interface and UPnP, and thus make Mopidy controllable from devices compatible with UPnP and/or DLNA. To read more about this, see :ref:`upnp-clients`. mopidy-0.17.0/docs/clients/upnp.rst000066400000000000000000000114061224420023200171760ustar00rootroot00000000000000.. _upnp-clients: ************ UPnP clients ************ `UPnP `_ is a set of specifications for media sharing, playing, remote control, etc, across a home network. The specs are supported by a lot of consumer devices (like smartphones, TVs, Xbox, and PlayStation) that are often labeled as being `DLNA `_ compatible or certified. The DLNA guidelines and UPnP specifications defines several device roles, of which Mopidy may play two: DLNA Digital Media Server (DMS) / UPnP AV MediaServer: A MediaServer provides a library of media and is capable of streaming that media to a MediaRenderer. If Mopidy was a MediaServer, you could browse and play Mopidy's music on a TV, smartphone, or tablet supporting UPnP. Mopidy does not currently support this, but we may in the future. :issue:`52` is the relevant wishlist issue. DLNA Digital Media Renderer (DMR) / UPnP AV MediaRenderer: A MediaRenderer is asked by some remote controller to play some given media, typically served by a MediaServer. If Mopidy was a MediaRenderer, you could use e.g. your smartphone or tablet to make Mopidy play media. Mopidy *does already* have experimental support for being a MediaRenderer with the help of Rygel, as you can read more about below. .. _rygel: How to make Mopidy available as an UPnP MediaRenderer ===================================================== With the help of `the Rygel project `_ Mopidy can be made available as an UPnP MediaRenderer. Rygel will interface with the MPRIS interface provided by the `Mopidy-MPRIS extension `_, and make Mopidy available as a MediaRenderer on the local network. Since this depends on the MPRIS frontend, which again depends on D-Bus being available, this will only work on Linux, and not OS X. MPRIS/D-Bus is only available to other applications on the same host, so Rygel must be running on the same machine as Mopidy. 1. Start Mopidy and make sure the MPRIS frontend is working. It is activated by default when the Mopidy-MPRIS extension is installed, but you may miss dependencies or be using OS X, in which case it will not work. Check the console output when Mopidy is started for any errors related to the MPRIS frontend. If you're unsure it is working, there are instructions for how to test it on in the `Mopidy-MPRIS readme `_. 2. Install Rygel. On Debian/Ubuntu:: sudo apt-get install rygel 3. Enable Rygel's MPRIS plugin. On Debian/Ubuntu, edit ``/etc/rygel.conf``, find the ``[MPRIS]`` section, and change ``enabled=false`` to ``enabled=true``. 4. Start Rygel by running:: rygel Example output:: $ rygel Rygel-Message: New plugin 'MediaExport' available Rygel-Message: New plugin 'org.mpris.MediaPlayer2.mopidy' available In the above example, you can see that Rygel found Mopidy, and it is now making Mopidy available through Rygel. The UPnP-Inspector client ========================= `UPnP-Inspector `_ is a graphical analyzer and debugging tool for UPnP services. It will detect any UPnP devices on your network, and show these in a tree structure. This is not a tool for your everyday music listening while relaxing on the couch, but it may be of use for testing that your setup works correctly. 1. Install UPnP-Inspector. On Debian/Ubuntu:: sudo apt-get install upnp-inspector 2. Run it:: upnp-inspector 3. Assuming that Mopidy is running with a working MPRIS frontend, and that Rygel is running on the same machine, Mopidy should now appear in UPnP-Inspector's device list. 4. If you expand the tree item saying ``Mopidy (MediaRenderer:2)`` or similiar, and then the sub element named ``AVTransport:2`` or similar, you'll find a list of commands you can invoke. E.g. if you double-click the ``Pause`` command, you'll get a new window where you can press an ``Invoke`` button, and then Mopidy should be paused. Note that if you have a firewall on the host running Mopidy and Rygel, and you want this to be exposed to the rest of your local network, you need to open up your firewall for UPnP traffic. UPnP use UDP port 1900 as well as some dynamically assigned ports. I've only verified that this procedure works across the network by temporarily disabling the firewall on the the two hosts involved, so I'll leave any firewall configuration as an exercise to the reader. Other clients ============= For a long list of UPnP clients for all possible platforms, see Wikipedia's `List of UPnP AV media servers and clients `_. mopidy-0.17.0/docs/codestyle.rst000066400000000000000000000033421224420023200165460ustar00rootroot00000000000000.. _codestyle: ********** Code style ********** - Always import ``unicode_literals`` and use unicode literals for everything except where you're explicitly working with bytes, which are marked with the ``b`` prefix. Do this:: from __future__ import unicode_literals foo = 'I am a unicode string, which is a sane default' bar = b'I am a bytestring' Not this:: foo = u'I am a unicode string' bar = 'I am a bytestring, but was it intentional?' - Follow :pep:`8` unless otherwise noted. `flake8 `_ should be used to check your code against the guidelines. - Use four spaces for indentation, *never* tabs. - Use CamelCase with initial caps for class names:: ClassNameWithCamelCase - Use underscore to split variable, function and method names for readability. Don't use CamelCase. :: lower_case_with_underscores - Use the fact that empty strings, lists and tuples are :class:`False` and don't compare boolean values using ``==`` and ``!=``. - Follow whitespace rules as described in :pep:`8`. Good examples:: spam(ham[1], {eggs: 2}) spam(1) dict['key'] = list[index] - Limit lines to 80 characters and avoid trailing whitespace. However note that wrapped lines should be *one* indentation level in from level above, except for ``if``, ``for``, ``with``, and ``while`` lines which should have two levels of indentation:: if (foo and bar ... baz and foobar): a = 1 from foobar import (foo, bar, ... baz) - For consistency, prefer ``'`` over ``"`` for strings, unless the string contains ``'``. - Take a look at :pep:`20` for a nice peek into a general mindset useful for Python coding. mopidy-0.17.0/docs/commands/000077500000000000000000000000001224420023200156205ustar00rootroot00000000000000mopidy-0.17.0/docs/commands/index.rst000066400000000000000000000002071224420023200174600ustar00rootroot00000000000000.. _commands: ******** Commands ******** Mopidy comes with the following commands: .. toctree:: :maxdepth: 1 :glob: ** mopidy-0.17.0/docs/commands/mopidy-convert-config.rst000066400000000000000000000042231224420023200225750ustar00rootroot00000000000000.. _mopidy-convert-config: ***************************** mopidy-convert-config command ***************************** Synopsis ======== mopidy-convert-config Description =========== Mopidy is a music server which can play music both from multiple sources, like your local hard drive, radio streams, and from Spotify and SoundCloud. Searches combines results from all music sources, and you can mix tracks from all sources in your play queue. Your playlists from Spotify or SoundCloud are also available for use. The ``mopidy-convert-config`` command is used to convert :file:`settings.py` configuration files used by ``mopidy`` < 0.14 to the :file:`mopidy.conf` config file used by ``mopidy`` >= 0.14. Options ======= .. program:: mopidy-convert-config This program does not take any options. It looks for the pre-0.14 settings file at :file:`{$XDG_CONFIG_DIR}/mopidy/settings.py`, and if it exists it converts it and ouputs a Mopidy 0.14 compatible ini-format configuration. If you don't already have a config file at :file:`{$XDG_CONFIG_DIR}/mopidy/mopidy.conf``, you're asked if you want to save the converted config to that file. Example ======= Given the following contents in :file:`~/.config/mopidy/settings.py`: :: LOCAL_MUSIC_PATH = u'~/music' MPD_SERVER_HOSTNAME = u'::' SPOTIFY_PASSWORD = u'secret' SPOTIFY_USERNAME = u'alice' Running ``mopidy-convert-config`` will convert the config and create a new :file:`mopidy.conf` config file: .. code-block:: none $ mopidy-convert-config Checking /home/alice/.config/mopidy/settings.py Converted config: [spotify] username = alice password = ******** [mpd] hostname = :: [local] media_dir = ~/music Write new config to /home/alice/.config/mopidy/mopidy.conf? [yN] y Done. Contents of :file:`~/.config/mopidy/mopidy.conf` after the conversion: .. code-block:: ini [spotify] username = alice password = secret [mpd] hostname = :: [local] media_dir = ~/music See also ======== :ref:`mopidy(1) ` Reporting bugs ============== Report bugs to Mopidy's issue tracker at mopidy-0.17.0/docs/commands/mopidy.rst000066400000000000000000000062221224420023200176550ustar00rootroot00000000000000.. _mopidy-cmd: ************** mopidy command ************** Synopsis ======== mopidy [-h] [--version] [-q] [-v] [--save-debug-log] [--config CONFIG_FILES] [-o CONFIG_OVERRIDES] [COMMAND] ... Description =========== Mopidy is a music server which can play music both from multiple sources, like your local hard drive, radio streams, and from Spotify and SoundCloud. Searches combines results from all music sources, and you can mix tracks from all sources in your play queue. Your playlists from Spotify or SoundCloud are also available for use. The ``mopidy`` command is used to start the server. Options ======= .. program:: mopidy .. cmdoption:: --help, -h Show help message and exit. .. cmdoption:: --version Show Mopidy's version number and exit. .. cmdoption:: --quiet, -q Show less output: warning level and higher. .. cmdoption:: --verbose, -v Show more output: debug level and higher. .. cmdoption:: --save-debug-log Save debug log to the file specified in the :confval:`logging/debug_file` config value, typically ``./mopidy.log``. .. cmdoption:: --config Specify config file to use. To use multiple config files, separate them with a colon. The later files override the earlier ones if there's a conflict. .. cmdoption:: --option

Static content serving

To see your own content instead of this placeholder page, change the setting HTTP_SERVER_STATIC_DIR to point to the directory containing your static files. This can be used to host e.g. a pure HTML/CSS/JavaScript Mopidy client.

If you replace this page with your own content, the Mopidy resources at /mopidy/ will still be available.

mopidy-0.17.0/mopidy/frontends/http/data/mopidy.css000066400000000000000000000024031224420023200222740ustar00rootroot00000000000000html { background: #e8ecef; color: #555; font-family: "Droid Serif", "Georgia", "Times New Roman", "Palatino", "Hoefler Text", "Baskerville", serif; font-size: 150%; line-height: 1.4em; } body { max-width: 20em; margin: 0 auto; } div.box { background: white; border-radius: 5px; box-shadow: 5px 5px 5px #d8dcdf; margin: 2em 0; padding: 1em; } div.box.focus { background: #465158; color: #e8ecef; } div.icon { float: right; } h1, h2 { font-family: "Ubuntu", "Arial", "Helvetica", "Lucida Grande", "Verdana", "Gill Sans", sans-serif; line-height: 1.1em; } h2 { margin: 0.2em 0 0; } p.next { text-align: right; } a { color: #555; text-decoration: none; border-bottom: 1px dotted; } img { border: 0; } code, pre { font-family: "Droid Sans Mono", Menlo, Courier New, Courier, Mono, monospace; font-size: 9pt; line-height: 1.2em; padding: 0.5em 1em; margin: 1em 0; white-space: pre; overflow: auto; } .box code, .box pre { background: #e8ecef; color: #555; } .box a { color: #465158; } .box a:hover { opacity: 0.8; } .box.focus a { color: #e8ecef; } .center { text-align: center; } #ws-console { height: 200px; overflow: auto; } mopidy-0.17.0/mopidy/frontends/http/data/mopidy.html000066400000000000000000000030261224420023200224520ustar00rootroot00000000000000 Mopidy HTTP frontend

Mopidy HTTP frontend

This web server is a part of the music server Mopidy. To learn more about Mopidy, please visit www.mopidy.com.

WebSocket endpoint

Mopidy has a WebSocket endpoint at /mopidy/ws/. You can use this end point to access Mopidy's full API, and to get notified about events happening in Mopidy.

Example

Here you can see events arriving from Mopidy in real time:



      

Nothing to see? Try playing a track using your MPD client.

Documentation

For more information, please refer to the Mopidy documentation at docs.mopidy.com.

mopidy-0.17.0/mopidy/frontends/http/data/mopidy.js000066400000000000000000001207621224420023200221310ustar00rootroot00000000000000/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ ((typeof define === "function" && define.amd && function (m) { define("bane", m); }) || (typeof module === "object" && function (m) { module.exports = m(); }) || function (m) { this.bane = m(); } )(function () { "use strict"; var slice = Array.prototype.slice; function handleError(event, error, errbacks) { var i, l = errbacks.length; if (l > 0) { for (i = 0; i < l; ++i) { errbacks[i](event, error); } return; } setTimeout(function () { error.message = event + " listener threw error: " + error.message; throw error; }, 0); } function assertFunction(fn) { if (typeof fn !== "function") { throw new TypeError("Listener is not function"); } return fn; } function supervisors(object) { if (!object.supervisors) { object.supervisors = []; } return object.supervisors; } function listeners(object, event) { if (!object.listeners) { object.listeners = {}; } if (event && !object.listeners[event]) { object.listeners[event] = []; } return event ? object.listeners[event] : object.listeners; } function errbacks(object) { if (!object.errbacks) { object.errbacks = []; } return object.errbacks; } /** * @signature var emitter = bane.createEmitter([object]); * * Create a new event emitter. If an object is passed, it will be modified * by adding the event emitter methods (see below). */ function createEventEmitter(object) { object = object || {}; function notifyListener(event, listener, args) { try { listener.listener.apply(listener.thisp || object, args); } catch (e) { handleError(event, e, errbacks(object)); } } object.on = function (event, listener, thisp) { if (typeof event === "function") { return supervisors(this).push({ listener: event, thisp: listener }); } listeners(this, event).push({ listener: assertFunction(listener), thisp: thisp }); }; object.off = function (event, listener) { var fns, events, i, l; if (!event) { fns = supervisors(this); fns.splice(0, fns.length); events = listeners(this); for (i in events) { if (events.hasOwnProperty(i)) { fns = listeners(this, i); fns.splice(0, fns.length); } } fns = errbacks(this); fns.splice(0, fns.length); return; } if (typeof event === "function") { fns = supervisors(this); listener = event; } else { fns = listeners(this, event); } if (!listener) { fns.splice(0, fns.length); return; } for (i = 0, l = fns.length; i < l; ++i) { if (fns[i].listener === listener) { fns.splice(i, 1); return; } } }; object.once = function (event, listener, thisp) { var wrapper = function () { object.off(event, wrapper); listener.apply(this, arguments); }; object.on(event, wrapper, thisp); }; object.bind = function (object, events) { var prop, i, l; if (!events) { for (prop in object) { if (typeof object[prop] === "function") { this.on(prop, object[prop], object); } } } else { for (i = 0, l = events.length; i < l; ++i) { if (typeof object[events[i]] === "function") { this.on(events[i], object[events[i]], object); } else { throw new Error("No such method " + events[i]); } } } return object; }; object.emit = function (event) { var toNotify = supervisors(this); var args = slice.call(arguments), i, l; for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); } toNotify = listeners(this, event).slice(); args = slice.call(arguments, 1); for (i = 0, l = toNotify.length; i < l; ++i) { notifyListener(event, toNotify[i], args); } }; object.errback = function (listener) { if (!this.errbacks) { this.errbacks = []; } this.errbacks.push(assertFunction(listener)); }; return object; } return { createEventEmitter: createEventEmitter }; }); if (typeof window !== "undefined") { window.define = function (factory) { try { delete window.define; } catch (e) { window.define = void 0; // IE } window.when = factory(); }; window.define.amd = {}; } /** * A lightweight CommonJS Promises/A and when() implementation * when is part of the cujo.js family of libraries (http://cujojs.com/) * * Licensed under the MIT License at: * http://www.opensource.org/licenses/mit-license.php * * @author Brian Cavalier * @author John Hann * @version 2.4.0 */ (function(define, global) { 'use strict'; define(function (require) { // Public API when.promise = promise; // Create a pending promise when.resolve = resolve; // Create a resolved promise when.reject = reject; // Create a rejected promise when.defer = defer; // Create a {promise, resolver} pair when.join = join; // Join 2 or more promises when.all = all; // Resolve a list of promises when.map = map; // Array.map() for promises when.reduce = reduce; // Array.reduce() for promises when.settle = settle; // Settle a list of promises when.any = any; // One-winner race when.some = some; // Multi-winner race when.isPromise = isPromiseLike; // DEPRECATED: use isPromiseLike when.isPromiseLike = isPromiseLike; // Is something promise-like, aka thenable /** * Register an observer for a promise or immediate value. * * @param {*} promiseOrValue * @param {function?} [onFulfilled] callback to be called when promiseOrValue is * successfully fulfilled. If promiseOrValue is an immediate value, callback * will be invoked immediately. * @param {function?} [onRejected] callback to be called when promiseOrValue is * rejected. * @param {function?} [onProgress] callback to be called when progress updates * are issued for promiseOrValue. * @returns {Promise} a new {@link Promise} that will complete with the return * value of callback or errback or the completion value of promiseOrValue if * callback and/or errback is not supplied. */ function when(promiseOrValue, onFulfilled, onRejected, onProgress) { // Get a trusted promise for the input promiseOrValue, and then // register promise handlers return resolve(promiseOrValue).then(onFulfilled, onRejected, onProgress); } /** * Trusted Promise constructor. A Promise created from this constructor is * a trusted when.js promise. Any other duck-typed promise is considered * untrusted. * @constructor * @param {function} sendMessage function to deliver messages to the promise's handler * @param {function?} inspect function that reports the promise's state * @name Promise */ function Promise(sendMessage, inspect) { this._message = sendMessage; this.inspect = inspect; } Promise.prototype = { /** * Register handlers for this promise. * @param [onFulfilled] {Function} fulfillment handler * @param [onRejected] {Function} rejection handler * @param [onProgress] {Function} progress handler * @return {Promise} new Promise */ then: function(onFulfilled, onRejected, onProgress) { /*jshint unused:false*/ var args, sendMessage; args = arguments; sendMessage = this._message; return _promise(function(resolve, reject, notify) { sendMessage('when', args, resolve, notify); }, this._status && this._status.observed()); }, /** * Register a rejection handler. Shortcut for .then(undefined, onRejected) * @param {function?} onRejected * @return {Promise} */ otherwise: function(onRejected) { return this.then(undef, onRejected); }, /** * Ensures that onFulfilledOrRejected will be called regardless of whether * this promise is fulfilled or rejected. onFulfilledOrRejected WILL NOT * receive the promises' value or reason. Any returned value will be disregarded. * onFulfilledOrRejected may throw or return a rejected promise to signal * an additional error. * @param {function} onFulfilledOrRejected handler to be called regardless of * fulfillment or rejection * @returns {Promise} */ ensure: function(onFulfilledOrRejected) { return this.then(injectHandler, injectHandler)['yield'](this); function injectHandler() { return resolve(onFulfilledOrRejected()); } }, /** * Shortcut for .then(function() { return value; }) * @param {*} value * @return {Promise} a promise that: * - is fulfilled if value is not a promise, or * - if value is a promise, will fulfill with its value, or reject * with its reason. */ 'yield': function(value) { return this.then(function() { return value; }); }, /** * Runs a side effect when this promise fulfills, without changing the * fulfillment value. * @param {function} onFulfilledSideEffect * @returns {Promise} */ tap: function(onFulfilledSideEffect) { return this.then(onFulfilledSideEffect)['yield'](this); }, /** * Assumes that this promise will fulfill with an array, and arranges * for the onFulfilled to be called with the array as its argument list * i.e. onFulfilled.apply(undefined, array). * @param {function} onFulfilled function to receive spread arguments * @return {Promise} */ spread: function(onFulfilled) { return this.then(function(array) { // array may contain promises, so resolve its contents. return all(array, function(array) { return onFulfilled.apply(undef, array); }); }); }, /** * Shortcut for .then(onFulfilledOrRejected, onFulfilledOrRejected) * @deprecated */ always: function(onFulfilledOrRejected, onProgress) { return this.then(onFulfilledOrRejected, onFulfilledOrRejected, onProgress); } }; /** * Returns a resolved promise. The returned promise will be * - fulfilled with promiseOrValue if it is a value, or * - if promiseOrValue is a promise * - fulfilled with promiseOrValue's value after it is fulfilled * - rejected with promiseOrValue's reason after it is rejected * @param {*} value * @return {Promise} */ function resolve(value) { return promise(function(resolve) { resolve(value); }); } /** * Returns a rejected promise for the supplied promiseOrValue. The returned * promise will be rejected with: * - promiseOrValue, if it is a value, or * - if promiseOrValue is a promise * - promiseOrValue's value after it is fulfilled * - promiseOrValue's reason after it is rejected * @param {*} promiseOrValue the rejected value of the returned {@link Promise} * @return {Promise} rejected {@link Promise} */ function reject(promiseOrValue) { return when(promiseOrValue, rejected); } /** * Creates a {promise, resolver} pair, either or both of which * may be given out safely to consumers. * The resolver has resolve, reject, and progress. The promise * has then plus extended promise API. * * @return {{ * promise: Promise, * resolve: function:Promise, * reject: function:Promise, * notify: function:Promise * resolver: { * resolve: function:Promise, * reject: function:Promise, * notify: function:Promise * }}} */ function defer() { var deferred, pending, resolved; // Optimize object shape deferred = { promise: undef, resolve: undef, reject: undef, notify: undef, resolver: { resolve: undef, reject: undef, notify: undef } }; deferred.promise = pending = promise(makeDeferred); return deferred; function makeDeferred(resolvePending, rejectPending, notifyPending) { deferred.resolve = deferred.resolver.resolve = function(value) { if(resolved) { return resolve(value); } resolved = true; resolvePending(value); return pending; }; deferred.reject = deferred.resolver.reject = function(reason) { if(resolved) { return resolve(rejected(reason)); } resolved = true; rejectPending(reason); return pending; }; deferred.notify = deferred.resolver.notify = function(update) { notifyPending(update); return update; }; } } /** * Creates a new promise whose fate is determined by resolver. * @param {function} resolver function(resolve, reject, notify) * @returns {Promise} promise whose fate is determine by resolver */ function promise(resolver) { return _promise(resolver, monitorApi.PromiseStatus && monitorApi.PromiseStatus()); } /** * Creates a new promise, linked to parent, whose fate is determined * by resolver. * @param {function} resolver function(resolve, reject, notify) * @param {Promise?} status promise from which the new promise is begotten * @returns {Promise} promise whose fate is determine by resolver * @private */ function _promise(resolver, status) { var self, value, consumers = []; self = new Promise(_message, inspect); self._status = status; // Call the provider resolver to seal the promise's fate try { resolver(promiseResolve, promiseReject, promiseNotify); } catch(e) { promiseReject(e); } // Return the promise return self; /** * Private message delivery. Queues and delivers messages to * the promise's ultimate fulfillment value or rejection reason. * @private * @param {String} type * @param {Array} args * @param {Function} resolve * @param {Function} notify */ function _message(type, args, resolve, notify) { consumers ? consumers.push(deliver) : enqueue(function() { deliver(value); }); function deliver(p) { p._message(type, args, resolve, notify); } } /** * Returns a snapshot of the promise's state at the instant inspect() * is called. The returned object is not live and will not update as * the promise's state changes. * @returns {{ state:String, value?:*, reason?:* }} status snapshot * of the promise. */ function inspect() { return value ? value.inspect() : toPendingState(); } /** * Transition from pre-resolution state to post-resolution state, notifying * all listeners of the ultimate fulfillment or rejection * @param {*|Promise} val resolution value */ function promiseResolve(val) { if(!consumers) { return; } value = coerce(val); scheduleConsumers(consumers, value); consumers = undef; if(status) { updateStatus(value, status); } } /** * Reject this promise with the supplied reason, which will be used verbatim. * @param {*} reason reason for the rejection */ function promiseReject(reason) { promiseResolve(rejected(reason)); } /** * Issue a progress event, notifying all progress listeners * @param {*} update progress event payload to pass to all listeners */ function promiseNotify(update) { if(consumers) { scheduleConsumers(consumers, progressed(update)); } } } /** * Creates a fulfilled, local promise as a proxy for a value * NOTE: must never be exposed * @param {*} value fulfillment value * @returns {Promise} */ function fulfilled(value) { return near( new NearFulfilledProxy(value), function() { return toFulfilledState(value); } ); } /** * Creates a rejected, local promise with the supplied reason * NOTE: must never be exposed * @param {*} reason rejection reason * @returns {Promise} */ function rejected(reason) { return near( new NearRejectedProxy(reason), function() { return toRejectedState(reason); } ); } /** * Creates a near promise using the provided proxy * NOTE: must never be exposed * @param {object} proxy proxy for the promise's ultimate value or reason * @param {function} inspect function that returns a snapshot of the * returned near promise's state * @returns {Promise} */ function near(proxy, inspect) { return new Promise(function (type, args, resolve) { try { resolve(proxy[type].apply(proxy, args)); } catch(e) { resolve(rejected(e)); } }, inspect); } /** * Create a progress promise with the supplied update. * @private * @param {*} update * @return {Promise} progress promise */ function progressed(update) { return new Promise(function (type, args, _, notify) { var onProgress = args[2]; try { notify(typeof onProgress === 'function' ? onProgress(update) : update); } catch(e) { notify(e); } }); } /** * Coerces x to a trusted Promise * * @private * @param {*} x thing to coerce * @returns {*} Guaranteed to return a trusted Promise. If x * is trusted, returns x, otherwise, returns a new, trusted, already-resolved * Promise whose resolution value is: * * the resolution value of x if it's a foreign promise, or * * x if it's a value */ function coerce(x) { if (x instanceof Promise) { return x; } if (!(x === Object(x) && 'then' in x)) { return fulfilled(x); } return promise(function(resolve, reject, notify) { enqueue(function() { try { // We must check and assimilate in the same tick, but not the // current tick, careful only to access promiseOrValue.then once. var untrustedThen = x.then; if(typeof untrustedThen === 'function') { fcall(untrustedThen, x, resolve, reject, notify); } else { // It's a value, create a fulfilled wrapper resolve(fulfilled(x)); } } catch(e) { // Something went wrong, reject reject(e); } }); }); } /** * Proxy for a near, fulfilled value * @param {*} value * @constructor */ function NearFulfilledProxy(value) { this.value = value; } NearFulfilledProxy.prototype.when = function(onResult) { return typeof onResult === 'function' ? onResult(this.value) : this.value; }; /** * Proxy for a near rejection * @param {*} reason * @constructor */ function NearRejectedProxy(reason) { this.reason = reason; } NearRejectedProxy.prototype.when = function(_, onError) { if(typeof onError === 'function') { return onError(this.reason); } else { throw this.reason; } }; /** * Schedule a task that will process a list of handlers * in the next queue drain run. * @private * @param {Array} handlers queue of handlers to execute * @param {*} value passed as the only arg to each handler */ function scheduleConsumers(handlers, value) { enqueue(function() { var handler, i = 0; while (handler = handlers[i++]) { handler(value); } }); } function updateStatus(value, status) { value.then(statusFulfilled, statusRejected); function statusFulfilled() { status.fulfilled(); } function statusRejected(r) { status.rejected(r); } } /** * Determines if x is promise-like, i.e. a thenable object * NOTE: Will return true for *any thenable object*, and isn't truly * safe, since it may attempt to access the `then` property of x (i.e. * clever/malicious getters may do weird things) * @param {*} x anything * @returns {boolean} true if x is promise-like */ function isPromiseLike(x) { return x && typeof x.then === 'function'; } /** * Initiates a competitive race, returning a promise that will resolve when * howMany of the supplied promisesOrValues have resolved, or will reject when * it becomes impossible for howMany to resolve, for example, when * (promisesOrValues.length - howMany) + 1 input promises reject. * * @param {Array} promisesOrValues array of anything, may contain a mix * of promises and values * @param howMany {number} number of promisesOrValues to resolve * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to an array of howMany values that * resolved first, or will reject with an array of * (promisesOrValues.length - howMany) + 1 rejection reasons. */ function some(promisesOrValues, howMany, onFulfilled, onRejected, onProgress) { return when(promisesOrValues, function(promisesOrValues) { return promise(resolveSome).then(onFulfilled, onRejected, onProgress); function resolveSome(resolve, reject, notify) { var toResolve, toReject, values, reasons, fulfillOne, rejectOne, len, i; len = promisesOrValues.length >>> 0; toResolve = Math.max(0, Math.min(howMany, len)); values = []; toReject = (len - toResolve) + 1; reasons = []; // No items in the input, resolve immediately if (!toResolve) { resolve(values); } else { rejectOne = function(reason) { reasons.push(reason); if(!--toReject) { fulfillOne = rejectOne = identity; reject(reasons); } }; fulfillOne = function(val) { // This orders the values based on promise resolution order values.push(val); if (!--toResolve) { fulfillOne = rejectOne = identity; resolve(values); } }; for(i = 0; i < len; ++i) { if(i in promisesOrValues) { when(promisesOrValues[i], fulfiller, rejecter, notify); } } } function rejecter(reason) { rejectOne(reason); } function fulfiller(val) { fulfillOne(val); } } }); } /** * Initiates a competitive race, returning a promise that will resolve when * any one of the supplied promisesOrValues has resolved or will reject when * *all* promisesOrValues have rejected. * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} promise that will resolve to the value that resolved first, or * will reject with an array of all rejected inputs. */ function any(promisesOrValues, onFulfilled, onRejected, onProgress) { function unwrapSingleResult(val) { return onFulfilled ? onFulfilled(val[0]) : val[0]; } return some(promisesOrValues, 1, unwrapSingleResult, onRejected, onProgress); } /** * Return a promise that will resolve only once all the supplied promisesOrValues * have resolved. The resolution value of the returned promise will be an array * containing the resolution values of each of the promisesOrValues. * @memberOf when * * @param {Array|Promise} promisesOrValues array of anything, may contain a mix * of {@link Promise}s and values * @param {function?} [onFulfilled] DEPRECATED, use returnedPromise.then() * @param {function?} [onRejected] DEPRECATED, use returnedPromise.then() * @param {function?} [onProgress] DEPRECATED, use returnedPromise.then() * @returns {Promise} */ function all(promisesOrValues, onFulfilled, onRejected, onProgress) { return _map(promisesOrValues, identity).then(onFulfilled, onRejected, onProgress); } /** * Joins multiple promises into a single returned promise. * @return {Promise} a promise that will fulfill when *all* the input promises * have fulfilled, or will reject when *any one* of the input promises rejects. */ function join(/* ...promises */) { return _map(arguments, identity); } /** * Settles all input promises such that they are guaranteed not to * be pending once the returned promise fulfills. The returned promise * will always fulfill, except in the case where `array` is a promise * that rejects. * @param {Array|Promise} array or promise for array of promises to settle * @returns {Promise} promise that always fulfills with an array of * outcome snapshots for each input promise. */ function settle(array) { return _map(array, toFulfilledState, toRejectedState); } /** * Promise-aware array map function, similar to `Array.prototype.map()`, * but input array may contain promises or values. * @param {Array|Promise} array array of anything, may contain promises and values * @param {function} mapFunc map function which may return a promise or value * @returns {Promise} promise that will fulfill with an array of mapped values * or reject if any input promise rejects. */ function map(array, mapFunc) { return _map(array, mapFunc); } /** * Internal map that allows a fallback to handle rejections * @param {Array|Promise} array array of anything, may contain promises and values * @param {function} mapFunc map function which may return a promise or value * @param {function?} fallback function to handle rejected promises * @returns {Promise} promise that will fulfill with an array of mapped values * or reject if any input promise rejects. */ function _map(array, mapFunc, fallback) { return when(array, function(array) { return _promise(resolveMap); function resolveMap(resolve, reject, notify) { var results, len, toResolve, i; // Since we know the resulting length, we can preallocate the results // array to avoid array expansions. toResolve = len = array.length >>> 0; results = []; if(!toResolve) { resolve(results); return; } // Since mapFunc may be async, get all invocations of it into flight for(i = 0; i < len; i++) { if(i in array) { resolveOne(array[i], i); } else { --toResolve; } } function resolveOne(item, i) { when(item, mapFunc, fallback).then(function(mapped) { results[i] = mapped; notify(mapped); if(!--toResolve) { resolve(results); } }, reject); } } }); } /** * Traditional reduce function, similar to `Array.prototype.reduce()`, but * input may contain promises and/or values, and reduceFunc * may return either a value or a promise, *and* initialValue may * be a promise for the starting value. * * @param {Array|Promise} promise array or promise for an array of anything, * may contain a mix of promises and values. * @param {function} reduceFunc reduce function reduce(currentValue, nextValue, index, total), * where total is the total number of items being reduced, and will be the same * in each call to reduceFunc. * @returns {Promise} that will resolve to the final reduced value */ function reduce(promise, reduceFunc /*, initialValue */) { var args = fcall(slice, arguments, 1); return when(promise, function(array) { var total; total = array.length; // Wrap the supplied reduceFunc with one that handles promises and then // delegates to the supplied. args[0] = function (current, val, i) { return when(current, function (c) { return when(val, function (value) { return reduceFunc(c, value, i, total); }); }); }; return reduceArray.apply(array, args); }); } // Snapshot states /** * Creates a fulfilled state snapshot * @private * @param {*} x any value * @returns {{state:'fulfilled',value:*}} */ function toFulfilledState(x) { return { state: 'fulfilled', value: x }; } /** * Creates a rejected state snapshot * @private * @param {*} x any reason * @returns {{state:'rejected',reason:*}} */ function toRejectedState(x) { return { state: 'rejected', reason: x }; } /** * Creates a pending state snapshot * @private * @returns {{state:'pending'}} */ function toPendingState() { return { state: 'pending' }; } // // Internals, utilities, etc. // var reduceArray, slice, fcall, nextTick, handlerQueue, setTimeout, funcProto, call, arrayProto, monitorApi, cjsRequire, undef; cjsRequire = require; // // Shared handler queue processing // // Credit to Twisol (https://github.com/Twisol) for suggesting // this type of extensible queue + trampoline approach for // next-tick conflation. handlerQueue = []; /** * Enqueue a task. If the queue is not currently scheduled to be * drained, schedule it. * @param {function} task */ function enqueue(task) { if(handlerQueue.push(task) === 1) { nextTick(drainQueue); } } /** * Drain the handler queue entirely, being careful to allow the * queue to be extended while it is being processed, and to continue * processing until it is truly empty. */ function drainQueue() { var task, i = 0; while(task = handlerQueue[i++]) { task(); } handlerQueue = []; } // capture setTimeout to avoid being caught by fake timers // used in time based tests setTimeout = global.setTimeout; // Allow attaching the monitor to when() if env has no console monitorApi = typeof console != 'undefined' ? console : when; // Prefer setImmediate or MessageChannel, cascade to node, // vertx and finally setTimeout /*global setImmediate,MessageChannel,process*/ if (typeof setImmediate === 'function') { nextTick = setImmediate.bind(global); } else if(typeof MessageChannel !== 'undefined') { var channel = new MessageChannel(); channel.port1.onmessage = drainQueue; nextTick = function() { channel.port2.postMessage(0); }; } else if (typeof process === 'object' && process.nextTick) { nextTick = process.nextTick; } else { try { // vert.x 1.x || 2.x nextTick = cjsRequire('vertx').runOnLoop || cjsRequire('vertx').runOnContext; } catch(ignore) { nextTick = function(t) { setTimeout(t, 0); }; } } // // Capture/polyfill function and array utils // // Safe function calls funcProto = Function.prototype; call = funcProto.call; fcall = funcProto.bind ? call.bind(call) : function(f, context) { return f.apply(context, slice.call(arguments, 2)); }; // Safe array ops arrayProto = []; slice = arrayProto.slice; // ES5 reduce implementation if native not available // See: http://es5.github.com/#x15.4.4.21 as there are many // specifics and edge cases. ES5 dictates that reduce.length === 1 // This implementation deviates from ES5 spec in the following ways: // 1. It does not check if reduceFunc is a Callable reduceArray = arrayProto.reduce || function(reduceFunc /*, initialValue */) { /*jshint maxcomplexity: 7*/ var arr, args, reduced, len, i; i = 0; arr = Object(this); len = arr.length >>> 0; args = arguments; // If no initialValue, use first item of array (we know length !== 0 here) // and adjust i to start at second item if(args.length <= 1) { // Skip to the first real element in the array for(;;) { if(i in arr) { reduced = arr[i++]; break; } // If we reached the end of the array without finding any real // elements, it's a TypeError if(++i >= len) { throw new TypeError(); } } } else { // If initialValue provided, use it reduced = args[1]; } // Do the actual reduce for(;i < len; ++i) { if(i in arr) { reduced = reduceFunc(reduced, arr[i], i, arr); } } return reduced; }; function identity(x) { return x; } return when; }); })(typeof define === 'function' && define.amd ? define : function (factory) { module.exports = factory(require); }, this); if (typeof module === "object" && typeof require === "function") { var bane = require("bane"); var websocket = require("faye-websocket"); var when = require("when"); } function Mopidy(settings) { if (!(this instanceof Mopidy)) { return new Mopidy(settings); } this._settings = this._configure(settings || {}); this._console = this._getConsole(); this._backoffDelay = this._settings.backoffDelayMin; this._pendingRequests = {}; this._webSocket = null; bane.createEventEmitter(this); this._delegateEvents(); if (this._settings.autoConnect) { this.connect(); } } if (typeof module === "object" && typeof require === "function") { Mopidy.WebSocket = websocket.Client; } else { Mopidy.WebSocket = window.WebSocket; } Mopidy.prototype._configure = function (settings) { var currentHost = (typeof document !== "undefined" && document.location.host) || "localhost"; settings.webSocketUrl = settings.webSocketUrl || "ws://" + currentHost + "/mopidy/ws/"; if (settings.autoConnect !== false) { settings.autoConnect = true; } settings.backoffDelayMin = settings.backoffDelayMin || 1000; settings.backoffDelayMax = settings.backoffDelayMax || 64000; return settings; }; Mopidy.prototype._getConsole = function () { var console = typeof console !== "undefined" && console || {}; console.log = console.log || function () {}; console.warn = console.warn || function () {}; console.error = console.error || function () {}; return console; }; Mopidy.prototype._delegateEvents = function () { // Remove existing event handlers this.off("websocket:close"); this.off("websocket:error"); this.off("websocket:incomingMessage"); this.off("websocket:open"); this.off("state:offline"); // Register basic set of event handlers this.on("websocket:close", this._cleanup); this.on("websocket:error", this._handleWebSocketError); this.on("websocket:incomingMessage", this._handleMessage); this.on("websocket:open", this._resetBackoffDelay); this.on("websocket:open", this._getApiSpec); this.on("state:offline", this._reconnect); }; Mopidy.prototype.connect = function () { if (this._webSocket) { if (this._webSocket.readyState === Mopidy.WebSocket.OPEN) { return; } else { this._webSocket.close(); } } this._webSocket = this._settings.webSocket || new Mopidy.WebSocket(this._settings.webSocketUrl); this._webSocket.onclose = function (close) { this.emit("websocket:close", close); }.bind(this); this._webSocket.onerror = function (error) { this.emit("websocket:error", error); }.bind(this); this._webSocket.onopen = function () { this.emit("websocket:open"); }.bind(this); this._webSocket.onmessage = function (message) { this.emit("websocket:incomingMessage", message); }.bind(this); }; Mopidy.prototype._cleanup = function (closeEvent) { Object.keys(this._pendingRequests).forEach(function (requestId) { var resolver = this._pendingRequests[requestId]; delete this._pendingRequests[requestId]; resolver.reject({ message: "WebSocket closed", closeEvent: closeEvent }); }.bind(this)); this.emit("state:offline"); }; Mopidy.prototype._reconnect = function () { this.emit("reconnectionPending", { timeToAttempt: this._backoffDelay }); setTimeout(function () { this.emit("reconnecting"); this.connect(); }.bind(this), this._backoffDelay); this._backoffDelay = this._backoffDelay * 2; if (this._backoffDelay > this._settings.backoffDelayMax) { this._backoffDelay = this._settings.backoffDelayMax; } }; Mopidy.prototype._resetBackoffDelay = function () { this._backoffDelay = this._settings.backoffDelayMin; }; Mopidy.prototype.close = function () { this.off("state:offline", this._reconnect); this._webSocket.close(); }; Mopidy.prototype._handleWebSocketError = function (error) { this._console.warn("WebSocket error:", error.stack || error); }; Mopidy.prototype._send = function (message) { var deferred = when.defer(); switch (this._webSocket.readyState) { case Mopidy.WebSocket.CONNECTING: deferred.resolver.reject({ message: "WebSocket is still connecting" }); break; case Mopidy.WebSocket.CLOSING: deferred.resolver.reject({ message: "WebSocket is closing" }); break; case Mopidy.WebSocket.CLOSED: deferred.resolver.reject({ message: "WebSocket is closed" }); break; default: message.jsonrpc = "2.0"; message.id = this._nextRequestId(); this._pendingRequests[message.id] = deferred.resolver; this._webSocket.send(JSON.stringify(message)); this.emit("websocket:outgoingMessage", message); } return deferred.promise; }; Mopidy.prototype._nextRequestId = (function () { var lastUsed = -1; return function () { lastUsed += 1; return lastUsed; }; }()); Mopidy.prototype._handleMessage = function (message) { try { var data = JSON.parse(message.data); if (data.hasOwnProperty("id")) { this._handleResponse(data); } else if (data.hasOwnProperty("event")) { this._handleEvent(data); } else { this._console.warn( "Unknown message type received. Message was: " + message.data); } } catch (error) { if (error instanceof SyntaxError) { this._console.warn( "WebSocket message parsing failed. Message was: " + message.data); } else { throw error; } } }; Mopidy.prototype._handleResponse = function (responseMessage) { if (!this._pendingRequests.hasOwnProperty(responseMessage.id)) { this._console.warn( "Unexpected response received. Message was:", responseMessage); return; } var resolver = this._pendingRequests[responseMessage.id]; delete this._pendingRequests[responseMessage.id]; if (responseMessage.hasOwnProperty("result")) { resolver.resolve(responseMessage.result); } else if (responseMessage.hasOwnProperty("error")) { resolver.reject(responseMessage.error); this._console.warn("Server returned error:", responseMessage.error); } else { resolver.reject({ message: "Response without 'result' or 'error' received", data: {response: responseMessage} }); this._console.warn( "Response without 'result' or 'error' received. Message was:", responseMessage); } }; Mopidy.prototype._handleEvent = function (eventMessage) { var type = eventMessage.event; var data = eventMessage; delete data.event; this.emit("event:" + this._snakeToCamel(type), data); }; Mopidy.prototype._getApiSpec = function () { return this._send({method: "core.describe"}) .then(this._createApi.bind(this), this._handleWebSocketError) .then(null, this._handleWebSocketError); }; Mopidy.prototype._createApi = function (methods) { var caller = function (method) { return function () { var params = Array.prototype.slice.call(arguments); return this._send({ method: method, params: params }); }.bind(this); }.bind(this); var getPath = function (fullName) { var path = fullName.split("."); if (path.length >= 1 && path[0] === "core") { path = path.slice(1); } return path; }; var createObjects = function (objPath) { var parentObj = this; objPath.forEach(function (objName) { objName = this._snakeToCamel(objName); parentObj[objName] = parentObj[objName] || {}; parentObj = parentObj[objName]; }.bind(this)); return parentObj; }.bind(this); var createMethod = function (fullMethodName) { var methodPath = getPath(fullMethodName); var methodName = this._snakeToCamel(methodPath.slice(-1)[0]); var object = createObjects(methodPath.slice(0, -1)); object[methodName] = caller(fullMethodName); object[methodName].description = methods[fullMethodName].description; object[methodName].params = methods[fullMethodName].params; }.bind(this); Object.keys(methods).forEach(createMethod); this.emit("state:online"); }; Mopidy.prototype._snakeToCamel = function (name) { return name.replace(/(_[a-z])/g, function (match) { return match.toUpperCase().replace("_", ""); }); }; if (typeof exports === "object") { exports.Mopidy = Mopidy; } mopidy-0.17.0/mopidy/frontends/http/data/mopidy.min.js000066400000000000000000000302601224420023200227040ustar00rootroot00000000000000/*! Mopidy.js - built 2013-09-17 * http://www.mopidy.com/ * Copyright (c) 2013 Stein Magnus Jodal and contributors * Licensed under the Apache License, Version 2.0 */ function Mopidy(a){return this instanceof Mopidy?(this._settings=this._configure(a||{}),this._console=this._getConsole(),this._backoffDelay=this._settings.backoffDelayMin,this._pendingRequests={},this._webSocket=null,bane.createEventEmitter(this),this._delegateEvents(),this._settings.autoConnect&&this.connect(),void 0):new Mopidy(a)}if(("function"==typeof define&&define.amd&&function(a){define("bane",a)}||"object"==typeof module&&function(a){module.exports=a()}||function(a){this.bane=a()})(function(){"use strict";function a(a,b,c){var d,e=c.length;if(e>0)for(d=0;e>d;++d)c[d](a,b);else setTimeout(function(){throw b.message=a+" listener threw error: "+b.message,b},0)}function b(a){if("function"!=typeof a)throw new TypeError("Listener is not function");return a}function c(a){return a.supervisors||(a.supervisors=[]),a.supervisors}function d(a,b){return a.listeners||(a.listeners={}),b&&!a.listeners[b]&&(a.listeners[b]=[]),b?a.listeners[b]:a.listeners}function e(a){return a.errbacks||(a.errbacks=[]),a.errbacks}function f(f){function h(b,c,d){try{c.listener.apply(c.thisp||f,d)}catch(g){a(b,g,e(f))}}return f=f||{},f.on=function(a,e,f){return"function"==typeof a?c(this).push({listener:a,thisp:e}):(d(this,a).push({listener:b(e),thisp:f}),void 0)},f.off=function(a,b){var f,g,h,i;if(!a){f=c(this),f.splice(0,f.length),g=d(this);for(h in g)g.hasOwnProperty(h)&&(f=d(this,h),f.splice(0,f.length));return f=e(this),f.splice(0,f.length),void 0}if("function"==typeof a?(f=c(this),b=a):f=d(this,a),!b)return f.splice(0,f.length),void 0;for(h=0,i=f.length;i>h;++h)if(f[h].listener===b)return f.splice(h,1),void 0},f.once=function(a,b,c){var d=function(){f.off(a,d),b.apply(this,arguments)};f.on(a,d,c)},f.bind=function(a,b){var c,d,e;if(b)for(d=0,e=b.length;e>d;++d){if("function"!=typeof a[b[d]])throw new Error("No such method "+b[d]);this.on(b[d],a[b[d]],a)}else for(c in a)"function"==typeof a[c]&&this.on(c,a[c],a);return a},f.emit=function(a){var b,e,f=c(this),i=g.call(arguments);for(b=0,e=f.length;e>b;++b)h(a,f[b],i);for(f=d(this,a).slice(),i=g.call(arguments,1),b=0,e=f.length;e>b;++b)h(a,f[b],i)},f.errback=function(a){this.errbacks||(this.errbacks=[]),this.errbacks.push(b(a))},f}var g=Array.prototype.slice;return{createEventEmitter:f}}),"undefined"!=typeof window&&(window.define=function(a){try{delete window.define}catch(b){window.define=void 0}window.when=a()},window.define.amd={}),function(a,b){"use strict";a(function(a){function c(a,b,c,d){return e(a).then(b,c,d)}function d(a,b){this._message=a,this.inspect=b}function e(a){return h(function(b){b(a)})}function f(a){return c(a,k)}function g(){function a(a,f,g){b.resolve=b.resolver.resolve=function(b){return d?e(b):(d=!0,a(b),c)},b.reject=b.resolver.reject=function(a){return d?e(k(a)):(d=!0,f(a),c)},b.notify=b.resolver.notify=function(a){return g(a),a}}var b,c,d;return b={promise:S,resolve:S,reject:S,notify:S,resolver:{resolve:S,reject:S,notify:S}},b.promise=c=h(a),b}function h(a){return i(a,Q.PromiseStatus&&Q.PromiseStatus())}function i(a,b){function c(a,b,c,d){function e(e){e._message(a,b,c,d)}l?l.push(e):E(function(){e(j)})}function e(){return j?j.inspect():D()}function f(a){l&&(j=n(a),q(l,j),l=S,b&&r(j,b))}function g(a){f(k(a))}function h(a){l&&q(l,m(a))}var i,j,l=[];i=new d(c,e),i._status=b;try{a(f,g,h)}catch(o){g(o)}return i}function j(a){return l(new o(a),function(){return B(a)})}function k(a){return l(new p(a),function(){return C(a)})}function l(a,b){return new d(function(b,c,d){try{d(a[b].apply(a,c))}catch(e){d(k(e))}},b)}function m(a){return new d(function(b,c,d,e){var f=c[2];try{e("function"==typeof f?f(a):a)}catch(g){e(g)}})}function n(a){return a instanceof d?a:a===Object(a)&&"then"in a?h(function(b,c,d){E(function(){try{var e=a.then;"function"==typeof e?J(e,a,b,c,d):b(j(a))}catch(f){c(f)}})}):j(a)}function o(a){this.value=a}function p(a){this.reason=a}function q(a,b){E(function(){for(var c,d=0;c=a[d++];)c(b)})}function r(a,b){function c(){b.fulfilled()}function d(a){b.rejected(a)}a.then(c,d)}function s(a){return a&&"function"==typeof a.then}function t(a,b,d,e,f){return c(a,function(a){function g(d,e,f){function g(a){n(a)}function h(a){m(a)}var i,j,k,l,m,n,o,p;if(o=a.length>>>0,i=Math.max(0,Math.min(b,o)),k=[],j=o-i+1,l=[],i)for(n=function(a){l.push(a),--j||(m=n=G,e(l))},m=function(a){k.push(a),--i||(m=n=G,d(k))},p=0;o>p;++p)p in a&&c(a[p],h,g,f);else d(k)}return h(g).then(d,e,f)})}function u(a,b,c,d){function e(a){return b?b(a[0]):a[0]}return t(a,1,e,c,d)}function v(a,b,c,d){return z(a,G).then(b,c,d)}function w(){return z(arguments,G)}function x(a){return z(a,B,C)}function y(a,b){return z(a,b)}function z(a,b,d){return c(a,function(a){function e(e,f,g){function h(a,h){c(a,b,d).then(function(a){i[h]=a,g(a),--k||e(i)},f)}var i,j,k,l;if(k=j=a.length>>>0,i=[],!k)return e(i),void 0;for(l=0;j>l;l++)l in a?h(a[l],l):--k}return i(e)})}function A(a,b){var d=J(I,arguments,1);return c(a,function(a){var e;return e=a.length,d[0]=function(a,d,f){return c(a,function(a){return c(d,function(c){return b(a,c,f,e)})})},H.apply(a,d)})}function B(a){return{state:"fulfilled",value:a}}function C(a){return{state:"rejected",reason:a}}function D(){return{state:"pending"}}function E(a){1===L.push(a)&&K(F)}function F(){for(var a,b=0;a=L[b++];)a();L=[]}function G(a){return a}c.promise=h,c.resolve=e,c.reject=f,c.defer=g,c.join=w,c.all=v,c.map=y,c.reduce=A,c.settle=x,c.any=u,c.some=t,c.isPromise=s,c.isPromiseLike=s,d.prototype={then:function(){var a,b;return a=arguments,b=this._message,i(function(c,d,e){b("when",a,c,e)},this._status&&this._status.observed())},otherwise:function(a){return this.then(S,a)},ensure:function(a){function b(){return e(a())}return this.then(b,b).yield(this)},yield:function(a){return this.then(function(){return a})},tap:function(a){return this.then(a).yield(this)},spread:function(a){return this.then(function(b){return v(b,function(b){return a.apply(S,b)})})},always:function(a,b){return this.then(a,a,b)}},o.prototype.when=function(a){return"function"==typeof a?a(this.value):this.value},p.prototype.when=function(a,b){if("function"==typeof b)return b(this.reason);throw this.reason};var H,I,J,K,L,M,N,O,P,Q,R,S;if(R=a,L=[],M=b.setTimeout,Q="undefined"!=typeof console?console:c,"function"==typeof setImmediate)K=setImmediate.bind(b);else if("undefined"!=typeof MessageChannel){var T=new MessageChannel;T.port1.onmessage=F,K=function(){T.port2.postMessage(0)}}else if("object"==typeof process&&process.nextTick)K=process.nextTick;else try{K=R("vertx").runOnLoop||R("vertx").runOnContext}catch(U){K=function(a){M(a,0)}}return N=Function.prototype,O=N.call,J=N.bind?O.bind(O):function(a,b){return a.apply(b,I.call(arguments,2))},P=[],I=P.slice,H=P.reduce||function(a){var b,c,d,e,f;if(f=0,b=Object(this),e=b.length>>>0,c=arguments,c.length<=1)for(;;){if(f in b){d=b[f++];break}if(++f>=e)throw new TypeError}else d=c[1];for(;e>f;++f)f in b&&(d=a(d,b[f],f,b));return d},c})}("function"==typeof define&&define.amd?define:function(a){module.exports=a(require)},this),"object"==typeof module&&"function"==typeof require)var bane=require("bane"),websocket=require("faye-websocket"),when=require("when");Mopidy.WebSocket="object"==typeof module&&"function"==typeof require?websocket.Client:window.WebSocket,Mopidy.prototype._configure=function(a){var b="undefined"!=typeof document&&document.location.host||"localhost";return a.webSocketUrl=a.webSocketUrl||"ws://"+b+"/mopidy/ws/",a.autoConnect!==!1&&(a.autoConnect=!0),a.backoffDelayMin=a.backoffDelayMin||1e3,a.backoffDelayMax=a.backoffDelayMax||64e3,a},Mopidy.prototype._getConsole=function(){var a="undefined"!=typeof a&&a||{};return a.log=a.log||function(){},a.warn=a.warn||function(){},a.error=a.error||function(){},a},Mopidy.prototype._delegateEvents=function(){this.off("websocket:close"),this.off("websocket:error"),this.off("websocket:incomingMessage"),this.off("websocket:open"),this.off("state:offline"),this.on("websocket:close",this._cleanup),this.on("websocket:error",this._handleWebSocketError),this.on("websocket:incomingMessage",this._handleMessage),this.on("websocket:open",this._resetBackoffDelay),this.on("websocket:open",this._getApiSpec),this.on("state:offline",this._reconnect)},Mopidy.prototype.connect=function(){if(this._webSocket){if(this._webSocket.readyState===Mopidy.WebSocket.OPEN)return;this._webSocket.close()}this._webSocket=this._settings.webSocket||new Mopidy.WebSocket(this._settings.webSocketUrl),this._webSocket.onclose=function(a){this.emit("websocket:close",a)}.bind(this),this._webSocket.onerror=function(a){this.emit("websocket:error",a)}.bind(this),this._webSocket.onopen=function(){this.emit("websocket:open")}.bind(this),this._webSocket.onmessage=function(a){this.emit("websocket:incomingMessage",a)}.bind(this)},Mopidy.prototype._cleanup=function(a){Object.keys(this._pendingRequests).forEach(function(b){var c=this._pendingRequests[b];delete this._pendingRequests[b],c.reject({message:"WebSocket closed",closeEvent:a})}.bind(this)),this.emit("state:offline")},Mopidy.prototype._reconnect=function(){this.emit("reconnectionPending",{timeToAttempt:this._backoffDelay}),setTimeout(function(){this.emit("reconnecting"),this.connect()}.bind(this),this._backoffDelay),this._backoffDelay=2*this._backoffDelay,this._backoffDelay>this._settings.backoffDelayMax&&(this._backoffDelay=this._settings.backoffDelayMax)},Mopidy.prototype._resetBackoffDelay=function(){this._backoffDelay=this._settings.backoffDelayMin},Mopidy.prototype.close=function(){this.off("state:offline",this._reconnect),this._webSocket.close()},Mopidy.prototype._handleWebSocketError=function(a){this._console.warn("WebSocket error:",a.stack||a)},Mopidy.prototype._send=function(a){var b=when.defer();switch(this._webSocket.readyState){case Mopidy.WebSocket.CONNECTING:b.resolver.reject({message:"WebSocket is still connecting"});break;case Mopidy.WebSocket.CLOSING:b.resolver.reject({message:"WebSocket is closing"});break;case Mopidy.WebSocket.CLOSED:b.resolver.reject({message:"WebSocket is closed"});break;default:a.jsonrpc="2.0",a.id=this._nextRequestId(),this._pendingRequests[a.id]=b.resolver,this._webSocket.send(JSON.stringify(a)),this.emit("websocket:outgoingMessage",a)}return b.promise},Mopidy.prototype._nextRequestId=function(){var a=-1;return function(){return a+=1}}(),Mopidy.prototype._handleMessage=function(a){try{var b=JSON.parse(a.data);b.hasOwnProperty("id")?this._handleResponse(b):b.hasOwnProperty("event")?this._handleEvent(b):this._console.warn("Unknown message type received. Message was: "+a.data)}catch(c){if(!(c instanceof SyntaxError))throw c;this._console.warn("WebSocket message parsing failed. Message was: "+a.data)}},Mopidy.prototype._handleResponse=function(a){if(!this._pendingRequests.hasOwnProperty(a.id))return this._console.warn("Unexpected response received. Message was:",a),void 0;var b=this._pendingRequests[a.id];delete this._pendingRequests[a.id],a.hasOwnProperty("result")?b.resolve(a.result):a.hasOwnProperty("error")?(b.reject(a.error),this._console.warn("Server returned error:",a.error)):(b.reject({message:"Response without 'result' or 'error' received",data:{response:a}}),this._console.warn("Response without 'result' or 'error' received. Message was:",a))},Mopidy.prototype._handleEvent=function(a){var b=a.event,c=a;delete c.event,this.emit("event:"+this._snakeToCamel(b),c)},Mopidy.prototype._getApiSpec=function(){return this._send({method:"core.describe"}).then(this._createApi.bind(this),this._handleWebSocketError).then(null,this._handleWebSocketError)},Mopidy.prototype._createApi=function(a){var b=function(a){return function(){var b=Array.prototype.slice.call(arguments);return this._send({method:a,params:b})}.bind(this)}.bind(this),c=function(a){var b=a.split(".");return b.length>=1&&"core"===b[0]&&(b=b.slice(1)),b},d=function(a){var b=this;return a.forEach(function(a){a=this._snakeToCamel(a),b[a]=b[a]||{},b=b[a]}.bind(this)),b}.bind(this),e=function(e){var f=c(e),g=this._snakeToCamel(f.slice(-1)[0]),h=d(f.slice(0,-1));h[g]=b(e),h[g].description=a[e].description,h[g].params=a[e].params}.bind(this);Object.keys(a).forEach(e),this.emit("state:online")},Mopidy.prototype._snakeToCamel=function(a){return a.replace(/(_[a-z])/g,function(a){return a.toUpperCase().replace("_","")})},"object"==typeof exports&&(exports.Mopidy=Mopidy);mopidy-0.17.0/mopidy/frontends/http/ext.conf000066400000000000000000000002171224420023200210200ustar00rootroot00000000000000[http] enabled = true hostname = 127.0.0.1 port = 6680 static_dir = zeroconf = Mopidy HTTP server on $hostname [loglevels] cherrypy = warning mopidy-0.17.0/mopidy/frontends/http/ws.py000066400000000000000000000044301224420023200203550ustar00rootroot00000000000000from __future__ import unicode_literals import logging import cherrypy from ws4py.websocket import WebSocket from mopidy import core, models from mopidy.utils import jsonrpc logger = logging.getLogger('mopidy.frontends.http') class WebSocketResource(object): def __init__(self, core_proxy): self._core = core_proxy inspector = jsonrpc.JsonRpcInspector( objects={ 'core.get_uri_schemes': core.Core.get_uri_schemes, 'core.library': core.LibraryController, 'core.playback': core.PlaybackController, 'core.playlists': core.PlaylistsController, 'core.tracklist': core.TracklistController, }) self.jsonrpc = jsonrpc.JsonRpcWrapper( objects={ 'core.describe': inspector.describe, 'core.get_uri_schemes': self._core.get_uri_schemes, 'core.library': self._core.library, 'core.playback': self._core.playback, 'core.playlists': self._core.playlists, 'core.tracklist': self._core.tracklist, }, decoders=[models.model_json_decoder], encoders=[models.ModelJSONEncoder]) @cherrypy.expose def index(self): logger.debug('WebSocket handler created') cherrypy.request.ws_handler.jsonrpc = self.jsonrpc class WebSocketHandler(WebSocket): def opened(self): remote = cherrypy.request.remote logger.debug( 'New WebSocket connection from %s:%d', remote.ip, remote.port) def closed(self, code, reason=None): remote = cherrypy.request.remote logger.debug( 'Closed WebSocket connection from %s:%d ' 'with code %s and reason %r', remote.ip, remote.port, code, reason) def received_message(self, request): remote = cherrypy.request.remote request = str(request) logger.debug( 'Received WebSocket message from %s:%d: %r', remote.ip, remote.port, request) response = self.jsonrpc.handle_json(request) if response: self.send(response) logger.debug( 'Sent WebSocket message to %s:%d: %r', remote.ip, remote.port, response) mopidy-0.17.0/mopidy/frontends/mpd/000077500000000000000000000000001224420023200171525ustar00rootroot00000000000000mopidy-0.17.0/mopidy/frontends/mpd/__init__.py000066400000000000000000000017001224420023200212610ustar00rootroot00000000000000from __future__ import unicode_literals import os import mopidy from mopidy import config, ext class Extension(ext.Extension): dist_name = 'Mopidy-MPD' ext_name = 'mpd' version = mopidy.__version__ def get_default_config(self): conf_file = os.path.join(os.path.dirname(__file__), 'ext.conf') return config.read(conf_file) def get_config_schema(self): schema = super(Extension, self).get_config_schema() schema['hostname'] = config.Hostname() schema['port'] = config.Port() schema['password'] = config.Secret(optional=True) schema['max_connections'] = config.Integer(minimum=1) schema['connection_timeout'] = config.Integer(minimum=1) schema['zeroconf'] = config.String(optional=True) return schema def validate_environment(self): pass def get_frontend_classes(self): from .actor import MpdFrontend return [MpdFrontend] mopidy-0.17.0/mopidy/frontends/mpd/actor.py000066400000000000000000000047201224420023200206370ustar00rootroot00000000000000from __future__ import unicode_literals import logging import sys import pykka from mopidy.core import CoreListener from mopidy.frontends.mpd import session from mopidy.utils import encoding, network, process, zeroconf logger = logging.getLogger('mopidy.frontends.mpd') class MpdFrontend(pykka.ThreadingActor, CoreListener): def __init__(self, config, core): super(MpdFrontend, self).__init__() hostname = network.format_hostname(config['mpd']['hostname']) self.hostname = hostname self.port = config['mpd']['port'] self.zeroconf_name = config['mpd']['zeroconf'] self.zeroconf_service = None try: network.Server( self.hostname, self.port, protocol=session.MpdSession, protocol_kwargs={ 'config': config, 'core': core, }, max_connections=config['mpd']['max_connections'], timeout=config['mpd']['connection_timeout']) except IOError as error: logger.error( 'MPD server startup failed: %s', encoding.locale_decode(error)) sys.exit(1) logger.info('MPD server running at [%s]:%s', self.hostname, self.port) def on_start(self): if self.zeroconf_name: self.zeroconf_service = zeroconf.Zeroconf( stype='_mpd._tcp', name=self.zeroconf_name, host=self.hostname, port=self.port) if self.zeroconf_service.publish(): logger.info('Registered MPD with Zeroconf as "%s"', self.zeroconf_service.name) else: logger.warning('Registering MPD with Zeroconf failed.') def on_stop(self): if self.zeroconf_service: self.zeroconf_service.unpublish() process.stop_actors_by_class(session.MpdSession) def send_idle(self, subsystem): listeners = pykka.ActorRegistry.get_by_class(session.MpdSession) for listener in listeners: getattr(listener.proxy(), 'on_idle')(subsystem) def playback_state_changed(self, old_state, new_state): self.send_idle('player') def tracklist_changed(self): self.send_idle('playlist') def options_changed(self): self.send_idle('options') def volume_changed(self, volume): self.send_idle('mixer') def mute_changed(self, mute): self.send_idle('output') mopidy-0.17.0/mopidy/frontends/mpd/dispatcher.py000066400000000000000000000242511224420023200216560ustar00rootroot00000000000000from __future__ import unicode_literals import logging import re import pykka from mopidy.frontends.mpd import exceptions, protocol logger = logging.getLogger('mopidy.frontends.mpd.dispatcher') protocol.load_protocol_modules() class MpdDispatcher(object): """ The MPD session feeds the MPD dispatcher with requests. The dispatcher finds the correct handler, processes the request and sends the response back to the MPD session. """ _noidle = re.compile(r'^noidle$') def __init__(self, session=None, config=None, core=None): self.config = config self.authenticated = False self.command_list_receiving = False self.command_list_ok = False self.command_list = [] self.command_list_index = None self.context = MpdContext( self, session=session, config=config, core=core) def handle_request(self, request, current_command_list_index=None): """Dispatch incoming requests to the correct handler.""" self.command_list_index = current_command_list_index response = [] filter_chain = [ self._catch_mpd_ack_errors_filter, self._authenticate_filter, self._command_list_filter, self._idle_filter, self._add_ok_filter, self._call_handler_filter, ] return self._call_next_filter(request, response, filter_chain) def handle_idle(self, subsystem): self.context.events.add(subsystem) subsystems = self.context.subscriptions.intersection( self.context.events) if not subsystems: return response = [] for subsystem in subsystems: response.append('changed: %s' % subsystem) response.append('OK') self.context.subscriptions = set() self.context.events = set() self.context.session.send_lines(response) def _call_next_filter(self, request, response, filter_chain): if filter_chain: next_filter = filter_chain.pop(0) return next_filter(request, response, filter_chain) else: return response ### Filter: catch MPD ACK errors def _catch_mpd_ack_errors_filter(self, request, response, filter_chain): try: return self._call_next_filter(request, response, filter_chain) except exceptions.MpdAckError as mpd_ack_error: if self.command_list_index is not None: mpd_ack_error.index = self.command_list_index return [mpd_ack_error.get_mpd_ack()] ### Filter: authenticate def _authenticate_filter(self, request, response, filter_chain): if self.authenticated: return self._call_next_filter(request, response, filter_chain) elif self.config['mpd']['password'] is None: self.authenticated = True return self._call_next_filter(request, response, filter_chain) else: command_name = request.split(' ')[0] command_names_not_requiring_auth = [ command.name for command in protocol.mpd_commands if not command.auth_required] if command_name in command_names_not_requiring_auth: return self._call_next_filter(request, response, filter_chain) else: raise exceptions.MpdPermissionError(command=command_name) ### Filter: command list def _command_list_filter(self, request, response, filter_chain): if self._is_receiving_command_list(request): self.command_list.append(request) return [] else: response = self._call_next_filter(request, response, filter_chain) if (self._is_receiving_command_list(request) or self._is_processing_command_list(request)): if response and response[-1] == 'OK': response = response[:-1] return response def _is_receiving_command_list(self, request): return ( self.command_list_receiving and request != 'command_list_end') def _is_processing_command_list(self, request): return ( self.command_list_index is not None and request != 'command_list_end') ### Filter: idle def _idle_filter(self, request, response, filter_chain): if self._is_currently_idle() and not self._noidle.match(request): logger.debug( 'Client sent us %s, only %s is allowed while in ' 'the idle state', repr(request), repr('noidle')) self.context.session.close() return [] if not self._is_currently_idle() and self._noidle.match(request): return [] # noidle was called before idle response = self._call_next_filter(request, response, filter_chain) if self._is_currently_idle(): return [] else: return response def _is_currently_idle(self): return bool(self.context.subscriptions) ### Filter: add OK def _add_ok_filter(self, request, response, filter_chain): response = self._call_next_filter(request, response, filter_chain) if not self._has_error(response): response.append('OK') return response def _has_error(self, response): return response and response[-1].startswith('ACK') ### Filter: call handler def _call_handler_filter(self, request, response, filter_chain): try: response = self._format_response(self._call_handler(request)) return self._call_next_filter(request, response, filter_chain) except pykka.ActorDeadError as e: logger.warning('Tried to communicate with dead actor.') raise exceptions.MpdSystemError(e) def _call_handler(self, request): (handler, kwargs) = self._find_handler(request) return handler(self.context, **kwargs) def _find_handler(self, request): for pattern in protocol.request_handlers: matches = re.match(pattern, request) if matches is not None: return ( protocol.request_handlers[pattern], matches.groupdict()) command_name = request.split(' ')[0] if command_name in [command.name for command in protocol.mpd_commands]: raise exceptions.MpdArgError( 'incorrect arguments', command=command_name) raise exceptions.MpdUnknownCommand(command=command_name) def _format_response(self, response): formatted_response = [] for element in self._listify_result(response): formatted_response.extend(self._format_lines(element)) return formatted_response def _listify_result(self, result): if result is None: return [] if isinstance(result, set): return self._flatten(list(result)) if not isinstance(result, list): return [result] return self._flatten(result) def _flatten(self, the_list): result = [] for element in the_list: if isinstance(element, list): result.extend(self._flatten(element)) else: result.append(element) return result def _format_lines(self, line): if isinstance(line, dict): return ['%s: %s' % (key, value) for (key, value) in line.items()] if isinstance(line, tuple): (key, value) = line return ['%s: %s' % (key, value)] return [line] class MpdContext(object): """ This object is passed as the first argument to all MPD command handlers to give the command handlers access to important parts of Mopidy. """ #: The current :class:`MpdDispatcher`. dispatcher = None #: The current :class:`mopidy.frontends.mpd.MpdSession`. session = None #: The Mopidy configuration. config = None #: The Mopidy core API. An instance of :class:`mopidy.core.Core`. core = None #: The active subsystems that have pending events. events = None #: The subsytems that we want to be notified about in idle mode. subscriptions = None _invalid_playlist_chars = re.compile(r'[\n\r/]') def __init__(self, dispatcher, session=None, config=None, core=None): self.dispatcher = dispatcher self.session = session self.config = config self.core = core self.events = set() self.subscriptions = set() self._playlist_uri_from_name = {} self._playlist_name_from_uri = {} self.refresh_playlists_mapping() def create_unique_name(self, playlist_name): stripped_name = self._invalid_playlist_chars.sub(' ', playlist_name) name = stripped_name i = 2 while name in self._playlist_uri_from_name: name = '%s [%d]' % (stripped_name, i) i += 1 return name def refresh_playlists_mapping(self): """ Maintain map between playlists and unique playlist names to be used by MPD """ if self.core is not None: self._playlist_uri_from_name.clear() self._playlist_name_from_uri.clear() for playlist in self.core.playlists.playlists.get(): if not playlist.name: continue # TODO: add scheme to name perhaps 'foo (spotify)' etc. name = self.create_unique_name(playlist.name) self._playlist_uri_from_name[name] = playlist.uri self._playlist_name_from_uri[playlist.uri] = name def lookup_playlist_from_name(self, name): """ Helper function to retrieve a playlist from its unique MPD name. """ if not self._playlist_uri_from_name: self.refresh_playlists_mapping() if name not in self._playlist_uri_from_name: return None uri = self._playlist_uri_from_name[name] return self.core.playlists.lookup(uri).get() def lookup_playlist_name_from_uri(self, uri): """ Helper function to retrieve the unique MPD playlist name from its uri. """ if uri not in self._playlist_name_from_uri: self.refresh_playlists_mapping() return self._playlist_name_from_uri[uri] mopidy-0.17.0/mopidy/frontends/mpd/exceptions.py000066400000000000000000000041271224420023200217110ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.exceptions import MopidyException class MpdAckError(MopidyException): """See fields on this class for available MPD error codes""" ACK_ERROR_NOT_LIST = 1 ACK_ERROR_ARG = 2 ACK_ERROR_PASSWORD = 3 ACK_ERROR_PERMISSION = 4 ACK_ERROR_UNKNOWN = 5 ACK_ERROR_NO_EXIST = 50 ACK_ERROR_PLAYLIST_MAX = 51 ACK_ERROR_SYSTEM = 52 ACK_ERROR_PLAYLIST_LOAD = 53 ACK_ERROR_UPDATE_ALREADY = 54 ACK_ERROR_PLAYER_SYNC = 55 ACK_ERROR_EXIST = 56 error_code = 0 def __init__(self, message='', index=0, command=''): super(MpdAckError, self).__init__(message, index, command) self.message = message self.index = index self.command = command def get_mpd_ack(self): """ MPD error code format:: ACK [%(error_code)i@%(index)i] {%(command)s} description """ return 'ACK [%i@%i] {%s} %s' % ( self.__class__.error_code, self.index, self.command, self.message) class MpdArgError(MpdAckError): error_code = MpdAckError.ACK_ERROR_ARG class MpdPasswordError(MpdAckError): error_code = MpdAckError.ACK_ERROR_PASSWORD class MpdPermissionError(MpdAckError): error_code = MpdAckError.ACK_ERROR_PERMISSION def __init__(self, *args, **kwargs): super(MpdPermissionError, self).__init__(*args, **kwargs) self.message = 'you don\'t have permission for "%s"' % self.command class MpdUnknownCommand(MpdAckError): error_code = MpdAckError.ACK_ERROR_UNKNOWN def __init__(self, *args, **kwargs): super(MpdUnknownCommand, self).__init__(*args, **kwargs) self.message = 'unknown command "%s"' % self.command self.command = '' class MpdNoExistError(MpdAckError): error_code = MpdAckError.ACK_ERROR_NO_EXIST class MpdSystemError(MpdAckError): error_code = MpdAckError.ACK_ERROR_SYSTEM class MpdNotImplemented(MpdAckError): error_code = 0 def __init__(self, *args, **kwargs): super(MpdNotImplemented, self).__init__(*args, **kwargs) self.message = 'Not implemented' mopidy-0.17.0/mopidy/frontends/mpd/ext.conf000066400000000000000000000002301224420023200206140ustar00rootroot00000000000000[mpd] enabled = true hostname = 127.0.0.1 port = 6600 password = max_connections = 20 connection_timeout = 60 zeroconf = Mopidy MPD server on $hostname mopidy-0.17.0/mopidy/frontends/mpd/protocol/000077500000000000000000000000001224420023200210135ustar00rootroot00000000000000mopidy-0.17.0/mopidy/frontends/mpd/protocol/__init__.py000066400000000000000000000055651224420023200231370ustar00rootroot00000000000000""" This is Mopidy's MPD protocol implementation. This is partly based upon the `MPD protocol documentation `_, which is a useful resource, but it is rather incomplete with regards to data formats, both for requests and responses. Thus, we have had to talk a great deal with the the original `MPD server `_ using telnet to get the details we need to implement our own MPD server which is compatible with the numerous existing `MPD clients `_. """ from __future__ import unicode_literals from collections import namedtuple import re #: The MPD protocol uses UTF-8 for encoding all data. ENCODING = 'UTF-8' #: The MPD protocol uses ``\n`` as line terminator. LINE_TERMINATOR = '\n' #: The MPD protocol version is 0.17.0. VERSION = '0.17.0' MpdCommand = namedtuple('MpdCommand', ['name', 'auth_required']) #: Set of all available commands, represented as :class:`MpdCommand` objects. mpd_commands = set() #: Map between request matchers and request handler functions. request_handlers = {} def handle_request(pattern, auth_required=True): """ Decorator for connecting command handlers to command requests. If you use named groups in the pattern, the decorated method will get the groups as keyword arguments. If the group is optional, remember to give the argument a default value. For example, if the command is ``do that thing`` the ``what`` argument will be ``this thing``:: @handle_request('do\ (?P.+)$') def do(what): ... Note that the patterns are compiled with the :attr:`re.VERBOSE` flag. Thus, you must escape any space characters you want to match, but you're also free to add non-escaped whitespace to format the pattern for easier reading. :param pattern: regexp pattern for matching commands :type pattern: string """ def decorator(func): match = re.search('([a-z_]+)', pattern) if match is not None: mpd_commands.add( MpdCommand(name=match.group(), auth_required=auth_required)) compiled_pattern = re.compile(pattern, flags=(re.UNICODE | re.VERBOSE)) if compiled_pattern in request_handlers: raise ValueError('Tried to redefine handler for %s with %s' % ( pattern, func)) request_handlers[compiled_pattern] = func func.__doc__ = ' - *Pattern:* ``%s``\n\n%s' % ( pattern, func.__doc__ or '') return func return decorator def load_protocol_modules(): """ The protocol modules must be imported to get them registered in :attr:`request_handlers` and :attr:`mpd_commands`. """ from . import ( # noqa audio_output, channels, command_list, connection, current_playlist, empty, music_db, playback, reflection, status, stickers, stored_playlists) mopidy-0.17.0/mopidy/frontends/mpd/protocol/audio_output.py000066400000000000000000000023361224420023200241120ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.exceptions import MpdNoExistError from mopidy.frontends.mpd.protocol import handle_request @handle_request(r'disableoutput\ "(?P\d+)"$') def disableoutput(context, outputid): """ *musicpd.org, audio output section:* ``disableoutput`` Turns an output off. """ if int(outputid) == 0: context.core.playback.set_mute(False) else: raise MpdNoExistError('No such audio output', command='disableoutput') @handle_request(r'enableoutput\ "(?P\d+)"$') def enableoutput(context, outputid): """ *musicpd.org, audio output section:* ``enableoutput`` Turns an output on. """ if int(outputid) == 0: context.core.playback.set_mute(True) else: raise MpdNoExistError('No such audio output', command='enableoutput') @handle_request(r'outputs$') def outputs(context): """ *musicpd.org, audio output section:* ``outputs`` Shows information about all outputs. """ muted = 1 if context.core.playback.get_mute().get() else 0 return [ ('outputid', 0), ('outputname', 'Mute'), ('outputenabled', muted), ] mopidy-0.17.0/mopidy/frontends/mpd/protocol/channels.py000066400000000000000000000033031224420023200231570ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_request(r'subscribe\ "(?P[A-Za-z0-9:._-]+)"$') def subscribe(context, channel): """ *musicpd.org, client to client section:* ``subscribe {NAME}`` Subscribe to a channel. The channel is created if it does not exist already. The name may consist of alphanumeric ASCII characters plus underscore, dash, dot and colon. """ raise MpdNotImplemented # TODO @handle_request(r'unsubscribe\ "(?P[A-Za-z0-9:._-]+)"$') def unsubscribe(context, channel): """ *musicpd.org, client to client section:* ``unsubscribe {NAME}`` Unsubscribe from a channel. """ raise MpdNotImplemented # TODO @handle_request(r'channels$') def channels(context): """ *musicpd.org, client to client section:* ``channels`` Obtain a list of all channels. The response is a list of "channel:" lines. """ raise MpdNotImplemented # TODO @handle_request(r'readmessages$') def readmessages(context): """ *musicpd.org, client to client section:* ``readmessages`` Reads messages for this client. The response is a list of "channel:" and "message:" lines. """ raise MpdNotImplemented # TODO @handle_request( r'sendmessage\ "(?P[A-Za-z0-9:._-]+)"\ "(?P[^"]*)"$') def sendmessage(context, channel, text): """ *musicpd.org, client to client section:* ``sendmessage {CHANNEL} {TEXT}`` Send a message to the specified channel. """ raise MpdNotImplemented # TODO mopidy-0.17.0/mopidy/frontends/mpd/protocol/command_list.py000066400000000000000000000045221224420023200240410ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdUnknownCommand @handle_request(r'command_list_begin$') def command_list_begin(context): """ *musicpd.org, command list section:* To facilitate faster adding of files etc. you can pass a list of commands all at once using a command list. The command list begins with ``command_list_begin`` or ``command_list_ok_begin`` and ends with ``command_list_end``. It does not execute any commands until the list has ended. The return value is whatever the return for a list of commands is. On success for all commands, ``OK`` is returned. If a command fails, no more commands are executed and the appropriate ``ACK`` error is returned. If ``command_list_ok_begin`` is used, ``list_OK`` is returned for each successful command executed in the command list. """ context.dispatcher.command_list_receiving = True context.dispatcher.command_list_ok = False context.dispatcher.command_list = [] @handle_request(r'command_list_end$') def command_list_end(context): """See :meth:`command_list_begin()`.""" if not context.dispatcher.command_list_receiving: raise MpdUnknownCommand(command='command_list_end') context.dispatcher.command_list_receiving = False (command_list, context.dispatcher.command_list) = ( context.dispatcher.command_list, []) (command_list_ok, context.dispatcher.command_list_ok) = ( context.dispatcher.command_list_ok, False) command_list_response = [] for index, command in enumerate(command_list): response = context.dispatcher.handle_request( command, current_command_list_index=index) command_list_response.extend(response) if (command_list_response and command_list_response[-1].startswith('ACK')): return command_list_response if command_list_ok: command_list_response.append('list_OK') return command_list_response @handle_request(r'command_list_ok_begin$') def command_list_ok_begin(context): """See :meth:`command_list_begin()`.""" context.dispatcher.command_list_receiving = True context.dispatcher.command_list_ok = True context.dispatcher.command_list = [] mopidy-0.17.0/mopidy/frontends/mpd/protocol/connection.py000066400000000000000000000023651224420023200235320ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import ( MpdPasswordError, MpdPermissionError) @handle_request(r'close$', auth_required=False) def close(context): """ *musicpd.org, connection section:* ``close`` Closes the connection to MPD. """ context.session.close() @handle_request(r'kill$') def kill(context): """ *musicpd.org, connection section:* ``kill`` Kills MPD. """ raise MpdPermissionError(command='kill') @handle_request(r'password\ "(?P[^"]+)"$', auth_required=False) def password_(context, password): """ *musicpd.org, connection section:* ``password {PASSWORD}`` This is used for authentication with the server. ``PASSWORD`` is simply the plaintext password. """ if password == context.config['mpd']['password']: context.dispatcher.authenticated = True else: raise MpdPasswordError('incorrect password', command='password') @handle_request(r'ping$', auth_required=False) def ping(context): """ *musicpd.org, connection section:* ``ping`` Does nothing but return ``OK``. """ pass mopidy-0.17.0/mopidy/frontends/mpd/protocol/current_playlist.py000066400000000000000000000271461224420023200250020ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd import translator from mopidy.frontends.mpd.exceptions import ( MpdArgError, MpdNoExistError, MpdNotImplemented) from mopidy.frontends.mpd.protocol import handle_request @handle_request(r'add\ "(?P[^"]*)"$') def add(context, uri): """ *musicpd.org, current playlist section:* ``add {URI}`` Adds the file ``URI`` to the playlist (directories add recursively). ``URI`` can also be a single file. *Clarifications:* - ``add ""`` should add all tracks in the library to the current playlist. """ if not uri: return tl_tracks = context.core.tracklist.add(uri=uri).get() if not tl_tracks: raise MpdNoExistError('directory or file not found', command='add') @handle_request(r'addid\ "(?P[^"]*)"(\ "(?P\d+)")*$') def addid(context, uri, songpos=None): """ *musicpd.org, current playlist section:* ``addid {URI} [POSITION]`` Adds a song to the playlist (non-recursive) and returns the song id. ``URI`` is always a single file or URL. For example:: addid "foo.mp3" Id: 999 OK *Clarifications:* - ``addid ""`` should return an error. """ if not uri: raise MpdNoExistError('No such song', command='addid') if songpos is not None: songpos = int(songpos) if songpos and songpos > context.core.tracklist.length.get(): raise MpdArgError('Bad song index', command='addid') tl_tracks = context.core.tracklist.add(uri=uri, at_position=songpos).get() if not tl_tracks: raise MpdNoExistError('No such song', command='addid') return ('Id', tl_tracks[0].tlid) @handle_request(r'delete\ "(?P\d+):(?P\d+)*"$') def delete_range(context, start, end=None): """ *musicpd.org, current playlist section:* ``delete [{POS} | {START:END}]`` Deletes a song from the playlist. """ start = int(start) if end is not None: end = int(end) else: end = context.core.tracklist.length.get() tl_tracks = context.core.tracklist.slice(start, end).get() if not tl_tracks: raise MpdArgError('Bad song index', command='delete') for (tlid, _) in tl_tracks: context.core.tracklist.remove(tlid=[tlid]) @handle_request(r'delete\ "(?P\d+)"$') def delete_songpos(context, songpos): """See :meth:`delete_range`""" try: songpos = int(songpos) (tlid, _) = context.core.tracklist.slice( songpos, songpos + 1).get()[0] context.core.tracklist.remove(tlid=[tlid]) except IndexError: raise MpdArgError('Bad song index', command='delete') @handle_request(r'deleteid\ "(?P\d+)"$') def deleteid(context, tlid): """ *musicpd.org, current playlist section:* ``deleteid {SONGID}`` Deletes the song ``SONGID`` from the playlist """ tlid = int(tlid) tl_tracks = context.core.tracklist.remove(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='deleteid') @handle_request(r'clear$') def clear(context): """ *musicpd.org, current playlist section:* ``clear`` Clears the current playlist. """ context.core.tracklist.clear() @handle_request(r'move\ "(?P\d+):(?P\d+)*"\ "(?P\d+)"$') def move_range(context, start, to, end=None): """ *musicpd.org, current playlist section:* ``move [{FROM} | {START:END}] {TO}`` Moves the song at ``FROM`` or range of songs at ``START:END`` to ``TO`` in the playlist. """ if end is None: end = context.core.tracklist.length.get() start = int(start) end = int(end) to = int(to) context.core.tracklist.move(start, end, to) @handle_request(r'move\ "(?P\d+)"\ "(?P\d+)"$') def move_songpos(context, songpos, to): """See :meth:`move_range`.""" songpos = int(songpos) to = int(to) context.core.tracklist.move(songpos, songpos + 1, to) @handle_request(r'moveid\ "(?P\d+)"\ "(?P\d+)"$') def moveid(context, tlid, to): """ *musicpd.org, current playlist section:* ``moveid {FROM} {TO}`` Moves the song with ``FROM`` (songid) to ``TO`` (playlist index) in the playlist. If ``TO`` is negative, it is relative to the current song in the playlist (if there is one). """ tlid = int(tlid) to = int(to) tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='moveid') position = context.core.tracklist.index(tl_tracks[0]).get() context.core.tracklist.move(position, position + 1, to) @handle_request(r'playlist$') def playlist(context): """ *musicpd.org, current playlist section:* ``playlist`` Displays the current playlist. .. note:: Do not use this, instead use ``playlistinfo``. """ return playlistinfo(context) @handle_request(r'playlistfind\ ("?)(?P[^"]+)\1\ "(?P[^"]+)"$') def playlistfind(context, tag, needle): """ *musicpd.org, current playlist section:* ``playlistfind {TAG} {NEEDLE}`` Finds songs in the current playlist with strict matching. *GMPC:* - does not add quotes around the tag. """ if tag == 'filename': tl_tracks = context.core.tracklist.filter(uri=[needle]).get() if not tl_tracks: return None position = context.core.tracklist.index(tl_tracks[0]).get() return translator.track_to_mpd_format(tl_tracks[0], position=position) raise MpdNotImplemented # TODO @handle_request(r'playlistid$') @handle_request(r'playlistid\ "(?P\d+)"$') def playlistid(context, tlid=None): """ *musicpd.org, current playlist section:* ``playlistid {SONGID}`` Displays a list of songs in the playlist. ``SONGID`` is optional and specifies a single song to display info for. """ if tlid is not None: tlid = int(tlid) tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='playlistid') position = context.core.tracklist.index(tl_tracks[0]).get() return translator.track_to_mpd_format(tl_tracks[0], position=position) else: return translator.tracks_to_mpd_format( context.core.tracklist.tl_tracks.get()) @handle_request(r'playlistinfo$') @handle_request(r'playlistinfo\ "(?P-?\d+)"$') @handle_request(r'playlistinfo\ "(?P\d+):(?P\d+)*"$') def playlistinfo(context, songpos=None, start=None, end=None): """ *musicpd.org, current playlist section:* ``playlistinfo [[SONGPOS] | [START:END]]`` Displays a list of all songs in the playlist, or if the optional argument is given, displays information only for the song ``SONGPOS`` or the range of songs ``START:END``. *ncmpc and mpc:* - uses negative indexes, like ``playlistinfo "-1"``, to request the entire playlist """ if songpos == '-1': songpos = None if songpos is not None: songpos = int(songpos) tl_track = context.core.tracklist.tl_tracks.get()[songpos] return translator.track_to_mpd_format(tl_track, position=songpos) else: if start is None: start = 0 start = int(start) if not (0 <= start <= context.core.tracklist.length.get()): raise MpdArgError('Bad song index', command='playlistinfo') if end is not None: end = int(end) if end > context.core.tracklist.length.get(): end = None tl_tracks = context.core.tracklist.tl_tracks.get() return translator.tracks_to_mpd_format(tl_tracks, start, end) @handle_request(r'playlistsearch\ ("?)(?P\w+)\1\ "(?P[^"]+)"$') def playlistsearch(context, tag, needle): """ *musicpd.org, current playlist section:* ``playlistsearch {TAG} {NEEDLE}`` Searches case-sensitively for partial matches in the current playlist. *GMPC:* - does not add quotes around the tag - uses ``filename`` and ``any`` as tags """ raise MpdNotImplemented # TODO @handle_request(r'plchanges\ ("?)(?P-?\d+)\1$') def plchanges(context, version): """ *musicpd.org, current playlist section:* ``plchanges {VERSION}`` Displays changed songs currently in the playlist since ``VERSION``. To detect songs that were deleted at the end of the playlist, use ``playlistlength`` returned by status command. *MPDroid:* - Calls ``plchanges "-1"`` two times per second to get the entire playlist. """ # XXX Naive implementation that returns all tracks as changed if int(version) < context.core.tracklist.version.get(): return translator.tracks_to_mpd_format( context.core.tracklist.tl_tracks.get()) @handle_request(r'plchangesposid\ "(?P\d+)"$') def plchangesposid(context, version): """ *musicpd.org, current playlist section:* ``plchangesposid {VERSION}`` Displays changed songs currently in the playlist since ``VERSION``. This function only returns the position and the id of the changed song, not the complete metadata. This is more bandwidth efficient. To detect songs that were deleted at the end of the playlist, use ``playlistlength`` returned by status command. """ # XXX Naive implementation that returns all tracks as changed if int(version) != context.core.tracklist.version.get(): result = [] for (position, (tlid, _)) in enumerate( context.core.tracklist.tl_tracks.get()): result.append(('cpos', position)) result.append(('Id', tlid)) return result @handle_request(r'shuffle$') @handle_request(r'shuffle\ "(?P\d+):(?P\d+)*"$') def shuffle(context, start=None, end=None): """ *musicpd.org, current playlist section:* ``shuffle [START:END]`` Shuffles the current playlist. ``START:END`` is optional and specifies a range of songs. """ if start is not None: start = int(start) if end is not None: end = int(end) context.core.tracklist.shuffle(start, end) @handle_request(r'swap\ "(?P\d+)"\ "(?P\d+)"$') def swap(context, songpos1, songpos2): """ *musicpd.org, current playlist section:* ``swap {SONG1} {SONG2}`` Swaps the positions of ``SONG1`` and ``SONG2``. """ songpos1 = int(songpos1) songpos2 = int(songpos2) tracks = context.core.tracklist.tracks.get() song1 = tracks[songpos1] song2 = tracks[songpos2] del tracks[songpos1] tracks.insert(songpos1, song2) del tracks[songpos2] tracks.insert(songpos2, song1) context.core.tracklist.clear() context.core.tracklist.add(tracks) @handle_request(r'swapid\ "(?P\d+)"\ "(?P\d+)"$') def swapid(context, tlid1, tlid2): """ *musicpd.org, current playlist section:* ``swapid {SONG1} {SONG2}`` Swaps the positions of ``SONG1`` and ``SONG2`` (both song ids). """ tlid1 = int(tlid1) tlid2 = int(tlid2) tl_tracks1 = context.core.tracklist.filter(tlid=[tlid1]).get() tl_tracks2 = context.core.tracklist.filter(tlid=[tlid2]).get() if not tl_tracks1 or not tl_tracks2: raise MpdNoExistError('No such song', command='swapid') position1 = context.core.tracklist.index(tl_tracks1[0]).get() position2 = context.core.tracklist.index(tl_tracks2[0]).get() swap(context, position1, position2) mopidy-0.17.0/mopidy/frontends/mpd/protocol/empty.py000066400000000000000000000003421224420023200225220ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request @handle_request(r'[\ ]*$') def empty(context): """The original MPD server returns ``OK`` on an empty request.""" pass mopidy-0.17.0/mopidy/frontends/mpd/protocol/music_db.py000066400000000000000000000416231224420023200231600ustar00rootroot00000000000000from __future__ import unicode_literals import functools import itertools import re from mopidy.models import Track from mopidy.frontends.mpd import translator from mopidy.frontends.mpd.exceptions import MpdArgError, MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request, stored_playlists LIST_QUERY = r""" ("?) # Optional quote around the field type (?P( # Field to list in the response [Aa]lbum | [Aa]lbumartist | [Aa]rtist | [Cc]omposer | [Dd]ate | [Gg]enre | [Pp]erformer )) \1 # End of optional quote around the field type (?: # Non-capturing group for optional search query \ # A single space (?P.*) )? $ """ SEARCH_FIELDS = r""" [Aa]lbum | [Aa]lbumartist | [Aa]ny | [Aa]rtist | [Cc]omment | [Cc]omposer | [Dd]ate | [Ff]ile | [Ff]ilename | [Gg]enre | [Pp]erformer | [Tt]itle | [Tt]rack """ # TODO Would be nice to get ("?)...\1 working for the quotes here SEARCH_QUERY = r""" (?P (?: # Non-capturing group for repeating query pairs "? # Optional quote around the field type (?: """ + SEARCH_FIELDS + r""" ) "? # End of optional quote around the field type \ # A single space "[^"]*" # Matching a quoted search string \s? )+ ) $ """ # TODO Would be nice to get ("?)...\1 working for the quotes here SEARCH_PAIR_WITHOUT_GROUPS = r""" \b # Only begin matching at word bundaries "? # Optional quote around the field type (?: # A non-capturing group for the field type """ + SEARCH_FIELDS + """ ) "? # End of optional quote around the field type \ # A single space "[^"]+" # Matching a quoted search string """ SEARCH_PAIR_WITHOUT_GROUPS_RE = re.compile( SEARCH_PAIR_WITHOUT_GROUPS, flags=(re.UNICODE | re.VERBOSE)) # TODO Would be nice to get ("?)...\1 working for the quotes here SEARCH_PAIR_WITH_GROUPS = r""" \b # Only begin matching at word bundaries "? # Optional quote around the field type (?P( # A capturing group for the field type """ + SEARCH_FIELDS + """ )) "? # End of optional quote around the field type \ # A single space "(?P[^"]+)" # Capturing a quoted search string """ SEARCH_PAIR_WITH_GROUPS_RE = re.compile( SEARCH_PAIR_WITH_GROUPS, flags=(re.UNICODE | re.VERBOSE)) def _query_from_mpd_search_format(mpd_query): """ Parses an MPD ``search`` or ``find`` query and converts it to the Mopidy query format. :param mpd_query: the MPD search query :type mpd_query: string """ pairs = SEARCH_PAIR_WITHOUT_GROUPS_RE.findall(mpd_query) query = {} for pair in pairs: m = SEARCH_PAIR_WITH_GROUPS_RE.match(pair) field = m.groupdict()['field'].lower() if field == 'title': field = 'track_name' elif field == 'track': field = 'track_no' elif field in ('file', 'filename'): field = 'uri' what = m.groupdict()['what'] if not what: raise ValueError if field in query: query[field].append(what) else: query[field] = [what] return query def _get_field(field, search_results): return list(itertools.chain(*[getattr(r, field) for r in search_results])) _get_albums = functools.partial(_get_field, 'albums') _get_artists = functools.partial(_get_field, 'artists') _get_tracks = functools.partial(_get_field, 'tracks') def _album_as_track(album): return Track( uri=album.uri, name='Album: ' + album.name, artists=album.artists, album=album, date=album.date) def _artist_as_track(artist): return Track( uri=artist.uri, name='Artist: ' + artist.name, artists=[artist]) @handle_request(r'count\ ' + SEARCH_QUERY) def count(context, mpd_query): """ *musicpd.org, music database section:* ``count {TAG} {NEEDLE}`` Counts the number of songs and their total playtime in the db matching ``TAG`` exactly. *GMPC:* - does not add quotes around the tag argument. - use multiple tag-needle pairs to make more specific searches. """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: raise MpdArgError('incorrect arguments', command='count') results = context.core.library.find_exact(**query).get() result_tracks = _get_tracks(results) return [ ('songs', len(result_tracks)), ('playtime', sum(track.length for track in result_tracks) / 1000), ] @handle_request(r'find\ ' + SEARCH_QUERY) def find(context, mpd_query): """ *musicpd.org, music database section:* ``find {TYPE} {WHAT}`` Finds songs in the db that are exactly ``WHAT``. ``TYPE`` can be any tag supported by MPD, or one of the two special parameters - ``file`` to search by full path (relative to database root), and ``any`` to match against all available tags. ``WHAT`` is what to find. *GMPC:* - does not add quotes around the field argument. - also uses ``find album "[ALBUM]" artist "[ARTIST]"`` to list album tracks. *ncmpc:* - does not add quotes around the field argument. - capitalizes the type argument. *ncmpcpp:* - also uses the search type "date". - uses "file" instead of "filename". """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: return results = context.core.library.find_exact(**query).get() result_tracks = [] if ('artist' not in query and 'albumartist' not in query and 'composer' not in query and 'performer' not in query): result_tracks += [_artist_as_track(a) for a in _get_artists(results)] if 'album' not in query: result_tracks += [_album_as_track(a) for a in _get_albums(results)] result_tracks += _get_tracks(results) return translator.tracks_to_mpd_format(result_tracks) @handle_request(r'findadd\ ' + SEARCH_QUERY) def findadd(context, mpd_query): """ *musicpd.org, music database section:* ``findadd {TYPE} {WHAT}`` Finds songs in the db that are exactly ``WHAT`` and adds them to current playlist. Parameters have the same meaning as for ``find``. """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: return results = context.core.library.find_exact(**query).get() context.core.tracklist.add(_get_tracks(results)) @handle_request(r'list\ ' + LIST_QUERY) def list_(context, field, mpd_query=None): """ *musicpd.org, music database section:* ``list {TYPE} [ARTIST]`` Lists all tags of the specified type. ``TYPE`` should be ``album``, ``artist``, ``albumartist``, ``date``, or ``genre``. ``ARTIST`` is an optional parameter when type is ``album``, ``date``, or ``genre``. This filters the result list by an artist. *Clarifications:* The musicpd.org documentation for ``list`` is far from complete. The command also supports the following variant: ``list {TYPE} {QUERY}`` Where ``QUERY`` applies to all ``TYPE``. ``QUERY`` is one or more pairs of a field name and a value. If the ``QUERY`` consists of more than one pair, the pairs are AND-ed together to find the result. Examples of valid queries and what they should return: ``list "artist" "artist" "ABBA"`` List artists where the artist name is "ABBA". Response:: Artist: ABBA OK ``list "album" "artist" "ABBA"`` Lists albums where the artist name is "ABBA". Response:: Album: More ABBA Gold: More ABBA Hits Album: Absolute More Christmas Album: Gold: Greatest Hits OK ``list "artist" "album" "Gold: Greatest Hits"`` Lists artists where the album name is "Gold: Greatest Hits". Response:: Artist: ABBA OK ``list "artist" "artist" "ABBA" "artist" "TLC"`` Lists artists where the artist name is "ABBA" *and* "TLC". Should never match anything. Response:: OK ``list "date" "artist" "ABBA"`` Lists dates where artist name is "ABBA". Response:: Date: Date: 1992 Date: 1993 OK ``list "date" "artist" "ABBA" "album" "Gold: Greatest Hits"`` Lists dates where artist name is "ABBA" and album name is "Gold: Greatest Hits". Response:: Date: 1992 OK ``list "genre" "artist" "The Rolling Stones"`` Lists genres where artist name is "The Rolling Stones". Response:: Genre: Genre: Rock OK *GMPC:* - does not add quotes around the field argument. *ncmpc:* - does not add quotes around the field argument. - capitalizes the field argument. """ field = field.lower() try: query = translator.query_from_mpd_list_format(field, mpd_query) except ValueError: return if field == 'artist': return _list_artist(context, query) if field == 'albumartist': return _list_albumartist(context, query) elif field == 'album': return _list_album(context, query) elif field == 'composer': return _list_composer(context, query) elif field == 'performer': return _list_performer(context, query) elif field == 'date': return _list_date(context, query) elif field == 'genre': return _list_genre(context, query) def _list_artist(context, query): artists = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): for artist in track.artists: if artist.name: artists.add(('Artist', artist.name)) return artists def _list_albumartist(context, query): albumartists = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): if track.album: for artist in track.album.artists: if artist.name: albumartists.add(('AlbumArtist', artist.name)) return albumartists def _list_album(context, query): albums = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): if track.album and track.album.name: albums.add(('Album', track.album.name)) return albums def _list_composer(context, query): composers = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): for composer in track.composers: if composer.name: composers.add(('Composer', composer.name)) return composers def _list_performer(context, query): performers = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): for performer in track.performers: if performer.name: performers.add(('Performer', performer.name)) return performers def _list_date(context, query): dates = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): if track.date: dates.add(('Date', track.date)) return dates def _list_genre(context, query): genres = set() results = context.core.library.find_exact(**query).get() for track in _get_tracks(results): if track.genre: genres.add(('Genre', track.genre)) return genres @handle_request(r'listall$') @handle_request(r'listall\ "(?P[^"]+)"$') def listall(context, uri=None): """ *musicpd.org, music database section:* ``listall [URI]`` Lists all songs and directories in ``URI``. """ raise MpdNotImplemented # TODO @handle_request(r'listallinfo$') @handle_request(r'listallinfo\ "(?P[^"]+)"$') def listallinfo(context, uri=None): """ *musicpd.org, music database section:* ``listallinfo [URI]`` Same as ``listall``, except it also returns metadata info in the same format as ``lsinfo``. """ raise MpdNotImplemented # TODO @handle_request(r'lsinfo$') @handle_request(r'lsinfo\ "(?P[^"]*)"$') def lsinfo(context, uri=None): """ *musicpd.org, music database section:* ``lsinfo [URI]`` Lists the contents of the directory ``URI``. When listing the root directory, this currently returns the list of stored playlists. This behavior is deprecated; use ``listplaylists`` instead. MPD returns the same result, including both playlists and the files and directories located at the root level, for both ``lsinfo``, ``lsinfo ""``, and ``lsinfo "/"``. """ if uri is None or uri == '/' or uri == '': return stored_playlists.listplaylists(context) raise MpdNotImplemented # TODO @handle_request(r'rescan$') @handle_request(r'rescan\ "(?P[^"]+)"$') def rescan(context, uri=None): """ *musicpd.org, music database section:* ``rescan [URI]`` Same as ``update``, but also rescans unmodified files. """ return update(context, uri, rescan_unmodified_files=True) @handle_request(r'search\ ' + SEARCH_QUERY) def search(context, mpd_query): """ *musicpd.org, music database section:* ``search {TYPE} {WHAT} [...]`` Searches for any song that contains ``WHAT``. Parameters have the same meaning as for ``find``, except that search is not case sensitive. *GMPC:* - does not add quotes around the field argument. - uses the undocumented field ``any``. - searches for multiple words like this:: search any "foo" any "bar" any "baz" *ncmpc:* - does not add quotes around the field argument. - capitalizes the field argument. *ncmpcpp:* - also uses the search type "date". - uses "file" instead of "filename". """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: return results = context.core.library.search(**query).get() artists = [_artist_as_track(a) for a in _get_artists(results)] albums = [_album_as_track(a) for a in _get_albums(results)] tracks = _get_tracks(results) return translator.tracks_to_mpd_format(artists + albums + tracks) @handle_request(r'searchadd\ ' + SEARCH_QUERY) def searchadd(context, mpd_query): """ *musicpd.org, music database section:* ``searchadd {TYPE} {WHAT} [...]`` Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds them to current playlist. Parameters have the same meaning as for ``find``, except that search is not case sensitive. """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: return results = context.core.library.search(**query).get() context.core.tracklist.add(_get_tracks(results)) @handle_request(r'searchaddpl\ "(?P[^"]+)"\ ' + SEARCH_QUERY) def searchaddpl(context, playlist_name, mpd_query): """ *musicpd.org, music database section:* ``searchaddpl {NAME} {TYPE} {WHAT} [...]`` Searches for any song that contains ``WHAT`` in tag ``TYPE`` and adds them to the playlist named ``NAME``. If a playlist by that name doesn't exist it is created. Parameters have the same meaning as for ``find``, except that search is not case sensitive. """ try: query = _query_from_mpd_search_format(mpd_query) except ValueError: return results = context.core.library.search(**query).get() playlist = context.lookup_playlist_from_name(playlist_name) if not playlist: playlist = context.core.playlists.create(playlist_name).get() tracks = list(playlist.tracks) + _get_tracks(results) playlist = playlist.copy(tracks=tracks) context.core.playlists.save(playlist) @handle_request(r'update$') @handle_request(r'update\ "(?P[^"]+)"$') def update(context, uri=None, rescan_unmodified_files=False): """ *musicpd.org, music database section:* ``update [URI]`` Updates the music database: find new files, remove deleted files, update modified files. ``URI`` is a particular directory or song/file to update. If you do not specify it, everything is updated. Prints ``updating_db: JOBID`` where ``JOBID`` is a positive number identifying the update job. You can read the current job id in the ``status`` response. """ return {'updating_db': 0} # TODO mopidy-0.17.0/mopidy/frontends/mpd/protocol/playback.py000066400000000000000000000320121224420023200231510ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.core import PlaybackState from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import ( MpdArgError, MpdNoExistError, MpdNotImplemented) @handle_request(r'consume\ ("?)(?P[01])\1$') def consume(context, state): """ *musicpd.org, playback section:* ``consume {STATE}`` Sets consume state to ``STATE``, ``STATE`` should be 0 or 1. When consume is activated, each song played is removed from playlist. """ if int(state): context.core.tracklist.consume = True else: context.core.tracklist.consume = False @handle_request(r'crossfade\ "(?P\d+)"$') def crossfade(context, seconds): """ *musicpd.org, playback section:* ``crossfade {SECONDS}`` Sets crossfading between songs. """ seconds = int(seconds) raise MpdNotImplemented # TODO @handle_request(r'next$') def next_(context): """ *musicpd.org, playback section:* ``next`` Plays next song in the playlist. *MPD's behaviour when affected by repeat/random/single/consume:* Given a playlist of three tracks numbered 1, 2, 3, and a currently playing track ``c``. ``next_track`` is defined at the track that will be played upon calls to ``next``. Tests performed on MPD 0.15.4-1ubuntu3. ====== ====== ====== ======= ===== ===== ===== ===== Inputs next_track ------------------------------- ------------------- ----- repeat random single consume c = 1 c = 2 c = 3 Notes ====== ====== ====== ======= ===== ===== ===== ===== T T T T 2 3 EOPL T T T . Rand Rand Rand [1] T T . T Rand Rand Rand [4] T T . . Rand Rand Rand [4] T . T T 2 3 EOPL T . T . 2 3 1 T . . T 3 3 EOPL T . . . 2 3 1 . T T T Rand Rand Rand [3] . T T . Rand Rand Rand [3] . T . T Rand Rand Rand [2] . T . . Rand Rand Rand [2] . . T T 2 3 EOPL . . T . 2 3 EOPL . . . T 2 3 EOPL . . . . 2 3 EOPL ====== ====== ====== ======= ===== ===== ===== ===== - When end of playlist (EOPL) is reached, the current track is unset. - [1] When *random* and *single* is combined, ``next`` selects a track randomly at each invocation, and not just the next track in an internal prerandomized playlist. - [2] When *random* is active, ``next`` will skip through all tracks in the playlist in random order, and finally EOPL is reached. - [3] *single* has no effect in combination with *random* alone, or *random* and *consume*. - [4] When *random* and *repeat* is active, EOPL is never reached, but the playlist is played again, in the same random order as the first time. """ return context.core.playback.next().get() @handle_request(r'pause$') @handle_request(r'pause\ "(?P[01])"$') def pause(context, state=None): """ *musicpd.org, playback section:* ``pause {PAUSE}`` Toggles pause/resumes playing, ``PAUSE`` is 0 or 1. *MPDroid:* - Calls ``pause`` without any arguments to toogle pause. """ if state is None: if (context.core.playback.state.get() == PlaybackState.PLAYING): context.core.playback.pause() elif (context.core.playback.state.get() == PlaybackState.PAUSED): context.core.playback.resume() elif int(state): context.core.playback.pause() else: context.core.playback.resume() @handle_request(r'play$') def play(context): """ The original MPD server resumes from the paused state on ``play`` without arguments. """ return context.core.playback.play().get() @handle_request(r'playid\ ("?)(?P-?\d+)\1$') def playid(context, tlid): """ *musicpd.org, playback section:* ``playid [SONGID]`` Begins playing the playlist at song ``SONGID``. *Clarifications:* - ``playid "-1"`` when playing is ignored. - ``playid "-1"`` when paused resumes playback. - ``playid "-1"`` when stopped with a current track starts playback at the current track. - ``playid "-1"`` when stopped without a current track, e.g. after playlist replacement, starts playback at the first track. """ tlid = int(tlid) if tlid == -1: return _play_minus_one(context) tl_tracks = context.core.tracklist.filter(tlid=[tlid]).get() if not tl_tracks: raise MpdNoExistError('No such song', command='playid') return context.core.playback.play(tl_tracks[0]).get() @handle_request(r'play\ ("?)(?P-?\d+)\1$') def playpos(context, songpos): """ *musicpd.org, playback section:* ``play [SONGPOS]`` Begins playing the playlist at song number ``SONGPOS``. *Clarifications:* - ``play "-1"`` when playing is ignored. - ``play "-1"`` when paused resumes playback. - ``play "-1"`` when stopped with a current track starts playback at the current track. - ``play "-1"`` when stopped without a current track, e.g. after playlist replacement, starts playback at the first track. *BitMPC:* - issues ``play 6`` without quotes around the argument. """ songpos = int(songpos) if songpos == -1: return _play_minus_one(context) try: tl_track = context.core.tracklist.slice(songpos, songpos + 1).get()[0] return context.core.playback.play(tl_track).get() except IndexError: raise MpdArgError('Bad song index', command='play') def _play_minus_one(context): if (context.core.playback.state.get() == PlaybackState.PLAYING): return # Nothing to do elif (context.core.playback.state.get() == PlaybackState.PAUSED): return context.core.playback.resume().get() elif context.core.playback.current_tl_track.get() is not None: tl_track = context.core.playback.current_tl_track.get() return context.core.playback.play(tl_track).get() elif context.core.tracklist.slice(0, 1).get(): tl_track = context.core.tracklist.slice(0, 1).get()[0] return context.core.playback.play(tl_track).get() else: return # Fail silently @handle_request(r'previous$') def previous(context): """ *musicpd.org, playback section:* ``previous`` Plays previous song in the playlist. *MPD's behaviour when affected by repeat/random/single/consume:* Given a playlist of three tracks numbered 1, 2, 3, and a currently playing track ``c``. ``previous_track`` is defined at the track that will be played upon ``previous`` calls. Tests performed on MPD 0.15.4-1ubuntu3. ====== ====== ====== ======= ===== ===== ===== Inputs previous_track ------------------------------- ------------------- repeat random single consume c = 1 c = 2 c = 3 ====== ====== ====== ======= ===== ===== ===== T T T T Rand? Rand? Rand? T T T . 3 1 2 T T . T Rand? Rand? Rand? T T . . 3 1 2 T . T T 3 1 2 T . T . 3 1 2 T . . T 3 1 2 T . . . 3 1 2 . T T T c c c . T T . c c c . T . T c c c . T . . c c c . . T T 1 1 2 . . T . 1 1 2 . . . T 1 1 2 . . . . 1 1 2 ====== ====== ====== ======= ===== ===== ===== - If :attr:`time_position` of the current track is 15s or more, ``previous`` should do a seek to time position 0. """ return context.core.playback.previous().get() @handle_request(r'random\ ("?)(?P[01])\1$') def random(context, state): """ *musicpd.org, playback section:* ``random {STATE}`` Sets random state to ``STATE``, ``STATE`` should be 0 or 1. """ if int(state): context.core.tracklist.random = True else: context.core.tracklist.random = False @handle_request(r'repeat\ ("?)(?P[01])\1$') def repeat(context, state): """ *musicpd.org, playback section:* ``repeat {STATE}`` Sets repeat state to ``STATE``, ``STATE`` should be 0 or 1. """ if int(state): context.core.tracklist.repeat = True else: context.core.tracklist.repeat = False @handle_request(r'replay_gain_mode\ "(?P(off|track|album))"$') def replay_gain_mode(context, mode): """ *musicpd.org, playback section:* ``replay_gain_mode {MODE}`` Sets the replay gain mode. One of ``off``, ``track``, ``album``. Changing the mode during playback may take several seconds, because the new settings does not affect the buffered data. This command triggers the options idle event. """ raise MpdNotImplemented # TODO @handle_request(r'replay_gain_status$') def replay_gain_status(context): """ *musicpd.org, playback section:* ``replay_gain_status`` Prints replay gain options. Currently, only the variable ``replay_gain_mode`` is returned. """ return 'off' # TODO @handle_request(r'seek\ ("?)(?P\d+)\1\ ("?)(?P\d+)\3$') def seek(context, songpos, seconds): """ *musicpd.org, playback section:* ``seek {SONGPOS} {TIME}`` Seeks to the position ``TIME`` (in seconds) of entry ``SONGPOS`` in the playlist. *Droid MPD:* - issues ``seek 1 120`` without quotes around the arguments. """ tl_track = context.core.playback.current_tl_track.get() if context.core.tracklist.index(tl_track).get() != int(songpos): playpos(context, songpos) context.core.playback.seek(int(seconds) * 1000).get() @handle_request(r'seekid\ "(?P\d+)"\ "(?P\d+)"$') def seekid(context, tlid, seconds): """ *musicpd.org, playback section:* ``seekid {SONGID} {TIME}`` Seeks to the position ``TIME`` (in seconds) of song ``SONGID``. """ tl_track = context.core.playback.current_tl_track.get() if not tl_track or tl_track.tlid != int(tlid): playid(context, tlid) context.core.playback.seek(int(seconds) * 1000).get() @handle_request(r'seekcur\ "(?P\d+)"$') @handle_request(r'seekcur\ "(?P[-+]\d+)"$') def seekcur(context, position=None, diff=None): """ *musicpd.org, playback section:* ``seekcur {TIME}`` Seeks to the position ``TIME`` within the current song. If prefixed by '+' or '-', then the time is relative to the current playing position. """ if position is not None: position = int(position) * 1000 context.core.playback.seek(position).get() elif diff is not None: position = context.core.playback.time_position.get() position += int(diff) * 1000 context.core.playback.seek(position).get() @handle_request(r'setvol\ ("?)(?P[-+]*\d+)\1$') def setvol(context, volume): """ *musicpd.org, playback section:* ``setvol {VOL}`` Sets volume to ``VOL``, the range of volume is 0-100. *Droid MPD:* - issues ``setvol 50`` without quotes around the argument. """ volume = int(volume) if volume < 0: volume = 0 if volume > 100: volume = 100 context.core.playback.volume = volume @handle_request(r'single\ ("?)(?P[01])\1$') def single(context, state): """ *musicpd.org, playback section:* ``single {STATE}`` Sets single state to ``STATE``, ``STATE`` should be 0 or 1. When single is activated, playback is stopped after current song, or song is repeated if the ``repeat`` mode is enabled. """ if int(state): context.core.tracklist.single = True else: context.core.tracklist.single = False @handle_request(r'stop$') def stop(context): """ *musicpd.org, playback section:* ``stop`` Stops playing. """ context.core.playback.stop() mopidy-0.17.0/mopidy/frontends/mpd/protocol/reflection.py000066400000000000000000000063261224420023200235260ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.exceptions import MpdPermissionError from mopidy.frontends.mpd.protocol import handle_request, mpd_commands @handle_request(r'config$', auth_required=False) def config(context): """ *musicpd.org, reflection section:* ``config`` Dumps configuration values that may be interesting for the client. This command is only permitted to "local" clients (connected via UNIX domain socket). """ raise MpdPermissionError(command='config') @handle_request(r'commands$', auth_required=False) def commands(context): """ *musicpd.org, reflection section:* ``commands`` Shows which commands the current user has access to. """ if context.dispatcher.authenticated: command_names = set([command.name for command in mpd_commands]) else: command_names = set([ command.name for command in mpd_commands if not command.auth_required]) # No one is permited to use 'config' or 'kill', rest of commands are not # listed by MPD, so we shouldn't either. command_names = command_names - set([ 'config', 'kill', 'command_list_begin', 'command_list_ok_begin', 'command_list_ok_begin', 'command_list_end', 'idle', 'noidle', 'sticker']) return [ ('command', command_name) for command_name in sorted(command_names)] @handle_request(r'decoders$') def decoders(context): """ *musicpd.org, reflection section:* ``decoders`` Print a list of decoder plugins, followed by their supported suffixes and MIME types. Example response:: plugin: mad suffix: mp3 suffix: mp2 mime_type: audio/mpeg plugin: mpcdec suffix: mpc *Clarifications:* - ncmpcpp asks for decoders the first time you open the browse view. By returning nothing and OK instead of an not implemented error, we avoid "Not implemented" showing up in the ncmpcpp interface, and we get the list of playlists without having to enter the browse interface twice. """ return # TODO @handle_request(r'notcommands$', auth_required=False) def notcommands(context): """ *musicpd.org, reflection section:* ``notcommands`` Shows which commands the current user does not have access to. """ if context.dispatcher.authenticated: command_names = [] else: command_names = [ command.name for command in mpd_commands if command.auth_required] # No permission to use command_names.append('config') command_names.append('kill') return [ ('command', command_name) for command_name in sorted(command_names)] @handle_request(r'tagtypes$') def tagtypes(context): """ *musicpd.org, reflection section:* ``tagtypes`` Shows a list of available song metadata. """ pass # TODO @handle_request(r'urlhandlers$') def urlhandlers(context): """ *musicpd.org, reflection section:* ``urlhandlers`` Gets a list of available URL handlers. """ return [ ('handler', uri_scheme) for uri_scheme in context.core.uri_schemes.get()] mopidy-0.17.0/mopidy/frontends/mpd/protocol/status.py000066400000000000000000000226201224420023200227120ustar00rootroot00000000000000from __future__ import unicode_literals import pykka from mopidy.core import PlaybackState from mopidy.frontends.mpd.exceptions import MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import track_to_mpd_format #: Subsystems that can be registered with idle command. SUBSYSTEMS = [ 'database', 'mixer', 'options', 'output', 'player', 'playlist', 'stored_playlist', 'update'] @handle_request(r'clearerror$') def clearerror(context): """ *musicpd.org, status section:* ``clearerror`` Clears the current error message in status (this is also accomplished by any command that starts playback). """ raise MpdNotImplemented # TODO @handle_request(r'currentsong$') def currentsong(context): """ *musicpd.org, status section:* ``currentsong`` Displays the song info of the current song (same song that is identified in status). """ tl_track = context.core.playback.current_tl_track.get() if tl_track is not None: position = context.core.tracklist.index(tl_track).get() return track_to_mpd_format(tl_track, position=position) @handle_request(r'idle$') @handle_request(r'idle\ (?P.+)$') def idle(context, subsystems=None): """ *musicpd.org, status section:* ``idle [SUBSYSTEMS...]`` Waits until there is a noteworthy change in one or more of MPD's subsystems. As soon as there is one, it lists all changed systems in a line in the format ``changed: SUBSYSTEM``, where ``SUBSYSTEM`` is one of the following: - ``database``: the song database has been modified after update. - ``update``: a database update has started or finished. If the database was modified during the update, the database event is also emitted. - ``stored_playlist``: a stored playlist has been modified, renamed, created or deleted - ``playlist``: the current playlist has been modified - ``player``: the player has been started, stopped or seeked - ``mixer``: the volume has been changed - ``output``: an audio output has been enabled or disabled - ``options``: options like repeat, random, crossfade, replay gain While a client is waiting for idle results, the server disables timeouts, allowing a client to wait for events as long as MPD runs. The idle command can be canceled by sending the command ``noidle`` (no other commands are allowed). MPD will then leave idle mode and print results immediately; might be empty at this time. If the optional ``SUBSYSTEMS`` argument is used, MPD will only send notifications when something changed in one of the specified subsystems. """ if subsystems: subsystems = subsystems.split() else: subsystems = SUBSYSTEMS for subsystem in subsystems: context.subscriptions.add(subsystem) active = context.subscriptions.intersection(context.events) if not active: context.session.prevent_timeout = True return response = [] context.events = set() context.subscriptions = set() for subsystem in active: response.append('changed: %s' % subsystem) return response @handle_request(r'noidle$') def noidle(context): """See :meth:`_status_idle`.""" if not context.subscriptions: return context.subscriptions = set() context.events = set() context.session.prevent_timeout = False @handle_request(r'stats$') def stats(context): """ *musicpd.org, status section:* ``stats`` Displays statistics. - ``artists``: number of artists - ``songs``: number of albums - ``uptime``: daemon uptime in seconds - ``db_playtime``: sum of all song times in the db - ``db_update``: last db update in UNIX time - ``playtime``: time length of music played """ return { 'artists': 0, # TODO 'albums': 0, # TODO 'songs': 0, # TODO 'uptime': 0, # TODO 'db_playtime': 0, # TODO 'db_update': 0, # TODO 'playtime': 0, # TODO } @handle_request(r'status$') def status(context): """ *musicpd.org, status section:* ``status`` Reports the current status of the player and the volume level. - ``volume``: 0-100 or -1 - ``repeat``: 0 or 1 - ``single``: 0 or 1 - ``consume``: 0 or 1 - ``playlist``: 31-bit unsigned integer, the playlist version number - ``playlistlength``: integer, the length of the playlist - ``state``: play, stop, or pause - ``song``: playlist song number of the current song stopped on or playing - ``songid``: playlist songid of the current song stopped on or playing - ``nextsong``: playlist song number of the next song to be played - ``nextsongid``: playlist songid of the next song to be played - ``time``: total time elapsed (of current playing/paused song) - ``elapsed``: Total time elapsed within the current song, but with higher resolution. - ``bitrate``: instantaneous bitrate in kbps - ``xfade``: crossfade in seconds - ``audio``: sampleRate``:bits``:channels - ``updatings_db``: job id - ``error``: if there is an error, returns message here *Clarifications based on experience implementing* - ``volume``: can also be -1 if no output is set. - ``elapsed``: Higher resolution means time in seconds with three decimal places for millisecond precision. """ futures = { 'tracklist.length': context.core.tracklist.length, 'tracklist.version': context.core.tracklist.version, 'playback.volume': context.core.playback.volume, 'tracklist.consume': context.core.tracklist.consume, 'tracklist.random': context.core.tracklist.random, 'tracklist.repeat': context.core.tracklist.repeat, 'tracklist.single': context.core.tracklist.single, 'playback.state': context.core.playback.state, 'playback.current_tl_track': context.core.playback.current_tl_track, 'tracklist.index': ( context.core.tracklist.index( context.core.playback.current_tl_track.get())), 'playback.time_position': context.core.playback.time_position, } pykka.get_all(futures.values()) result = [ ('volume', _status_volume(futures)), ('repeat', _status_repeat(futures)), ('random', _status_random(futures)), ('single', _status_single(futures)), ('consume', _status_consume(futures)), ('playlist', _status_playlist_version(futures)), ('playlistlength', _status_playlist_length(futures)), ('xfade', _status_xfade(futures)), ('state', _status_state(futures)), ] if futures['playback.current_tl_track'].get() is not None: result.append(('song', _status_songpos(futures))) result.append(('songid', _status_songid(futures))) if futures['playback.state'].get() in ( PlaybackState.PLAYING, PlaybackState.PAUSED): result.append(('time', _status_time(futures))) result.append(('elapsed', _status_time_elapsed(futures))) result.append(('bitrate', _status_bitrate(futures))) return result def _status_bitrate(futures): current_tl_track = futures['playback.current_tl_track'].get() if current_tl_track is None: return 0 if current_tl_track.track.bitrate is None: return 0 return current_tl_track.track.bitrate def _status_consume(futures): if futures['tracklist.consume'].get(): return 1 else: return 0 def _status_playlist_length(futures): return futures['tracklist.length'].get() def _status_playlist_version(futures): return futures['tracklist.version'].get() def _status_random(futures): return int(futures['tracklist.random'].get()) def _status_repeat(futures): return int(futures['tracklist.repeat'].get()) def _status_single(futures): return int(futures['tracklist.single'].get()) def _status_songid(futures): current_tl_track = futures['playback.current_tl_track'].get() if current_tl_track is not None: return current_tl_track.tlid else: return _status_songpos(futures) def _status_songpos(futures): return futures['tracklist.index'].get() def _status_state(futures): state = futures['playback.state'].get() if state == PlaybackState.PLAYING: return 'play' elif state == PlaybackState.STOPPED: return 'stop' elif state == PlaybackState.PAUSED: return 'pause' def _status_time(futures): return '%d:%d' % ( futures['playback.time_position'].get() // 1000, _status_time_total(futures) // 1000) def _status_time_elapsed(futures): return '%.3f' % (futures['playback.time_position'].get() / 1000.0) def _status_time_total(futures): current_tl_track = futures['playback.current_tl_track'].get() if current_tl_track is None: return 0 elif current_tl_track.track.length is None: return 0 else: return current_tl_track.track.length def _status_volume(futures): volume = futures['playback.volume'].get() if volume is not None: return volume else: return -1 def _status_xfade(futures): return 0 # Not supported mopidy-0.17.0/mopidy/frontends/mpd/protocol/stickers.py000066400000000000000000000041771224420023200232250ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.exceptions import MpdNotImplemented @handle_request( r'sticker\ delete\ "(?P[^"]+)"\ ' r'"(?P[^"]+)"(\ "(?P[^"]+)")*$') def sticker_delete(context, field, uri, name=None): """ *musicpd.org, sticker section:* ``sticker delete {TYPE} {URI} [NAME]`` Deletes a sticker value from the specified object. If you do not specify a sticker name, all sticker values are deleted. """ raise MpdNotImplemented # TODO @handle_request( r'sticker\ find\ "(?P[^"]+)"\ "(?P[^"]+)"\ ' r'"(?P[^"]+)"$') def sticker_find(context, field, uri, name): """ *musicpd.org, sticker section:* ``sticker find {TYPE} {URI} {NAME}`` Searches the sticker database for stickers with the specified name, below the specified directory (``URI``). For each matching song, it prints the ``URI`` and that one sticker's value. """ raise MpdNotImplemented # TODO @handle_request( r'sticker\ get\ "(?P[^"]+)"\ "(?P[^"]+)"\ ' r'"(?P[^"]+)"$') def sticker_get(context, field, uri, name): """ *musicpd.org, sticker section:* ``sticker get {TYPE} {URI} {NAME}`` Reads a sticker value for the specified object. """ raise MpdNotImplemented # TODO @handle_request(r'sticker\ list\ "(?P[^"]+)"\ "(?P[^"]+)"$') def sticker_list(context, field, uri): """ *musicpd.org, sticker section:* ``sticker list {TYPE} {URI}`` Lists the stickers for the specified object. """ raise MpdNotImplemented # TODO @handle_request( r'sticker\ set\ "(?P[^"]+)"\ "(?P[^"]+)"\ ' r'"(?P[^"]+)"\ "(?P[^"]+)"$') def sticker_set(context, field, uri, name, value): """ *musicpd.org, sticker section:* ``sticker set {TYPE} {URI} {NAME} {VALUE}`` Adds a sticker value to the specified object. If a sticker item with that name already exists, it is replaced. """ raise MpdNotImplemented # TODO mopidy-0.17.0/mopidy/frontends/mpd/protocol/stored_playlists.py000066400000000000000000000142641224420023200250000ustar00rootroot00000000000000from __future__ import unicode_literals import datetime as dt from mopidy.frontends.mpd.exceptions import MpdNoExistError, MpdNotImplemented from mopidy.frontends.mpd.protocol import handle_request from mopidy.frontends.mpd.translator import playlist_to_mpd_format @handle_request(r'listplaylist\ ("?)(?P[^"]+)\1$') def listplaylist(context, name): """ *musicpd.org, stored playlists section:* ``listplaylist {NAME}`` Lists the files in the playlist ``NAME.m3u``. Output format:: file: relative/path/to/file1.flac file: relative/path/to/file2.ogg file: relative/path/to/file3.mp3 """ playlist = context.lookup_playlist_from_name(name) if not playlist: raise MpdNoExistError('No such playlist', command='listplaylist') return ['file: %s' % t.uri for t in playlist.tracks] @handle_request(r'listplaylistinfo\ ("?)(?P[^"]+)\1$') def listplaylistinfo(context, name): """ *musicpd.org, stored playlists section:* ``listplaylistinfo {NAME}`` Lists songs in the playlist ``NAME.m3u``. Output format: Standard track listing, with fields: file, Time, Title, Date, Album, Artist, Track """ playlist = context.lookup_playlist_from_name(name) if not playlist: raise MpdNoExistError('No such playlist', command='listplaylistinfo') return playlist_to_mpd_format(playlist) @handle_request(r'listplaylists$') def listplaylists(context): """ *musicpd.org, stored playlists section:* ``listplaylists`` Prints a list of the playlist directory. After each playlist name the server sends its last modification time as attribute ``Last-Modified`` in ISO 8601 format. To avoid problems due to clock differences between clients and the server, clients should not compare this value with their local clock. Output format:: playlist: a Last-Modified: 2010-02-06T02:10:25Z playlist: b Last-Modified: 2010-02-06T02:11:08Z *Clarifications:* - ncmpcpp 0.5.10 segfaults if we return 'playlist: ' on a line, so we must ignore playlists without names, which isn't very useful anyway. """ result = [] for playlist in context.core.playlists.playlists.get(): if not playlist.name: continue name = context.lookup_playlist_name_from_uri(playlist.uri) result.append(('playlist', name)) last_modified = ( playlist.last_modified or dt.datetime.utcnow()).isoformat() # Remove microseconds last_modified = last_modified.split('.')[0] # Add time zone information last_modified = last_modified + 'Z' result.append(('Last-Modified', last_modified)) return result @handle_request( r'load\ "(?P[^"]+)"(\ "(?P\d+):(?P\d+)*")*$') def load(context, name, start=None, end=None): """ *musicpd.org, stored playlists section:* ``load {NAME} [START:END]`` Loads the playlist into the current queue. Playlist plugins are supported. A range may be specified to load only a part of the playlist. *Clarifications:* - ``load`` appends the given playlist to the current playlist. - MPD 0.17.1 does not support open-ended ranges, i.e. without end specified, for the ``load`` command, even though MPD's general range docs allows open-ended ranges. - MPD 0.17.1 does not fail if the specified range is outside the playlist, in either or both ends. """ playlist = context.lookup_playlist_from_name(name) if not playlist: raise MpdNoExistError('No such playlist', command='load') if start is not None: start = int(start) if end is not None: end = int(end) context.core.tracklist.add(playlist.tracks[start:end]) @handle_request(r'playlistadd\ "(?P[^"]+)"\ "(?P[^"]+)"$') def playlistadd(context, name, uri): """ *musicpd.org, stored playlists section:* ``playlistadd {NAME} {URI}`` Adds ``URI`` to the playlist ``NAME.m3u``. ``NAME.m3u`` will be created if it does not exist. """ raise MpdNotImplemented # TODO @handle_request(r'playlistclear\ "(?P[^"]+)"$') def playlistclear(context, name): """ *musicpd.org, stored playlists section:* ``playlistclear {NAME}`` Clears the playlist ``NAME.m3u``. """ raise MpdNotImplemented # TODO @handle_request(r'playlistdelete\ "(?P[^"]+)"\ "(?P\d+)"$') def playlistdelete(context, name, songpos): """ *musicpd.org, stored playlists section:* ``playlistdelete {NAME} {SONGPOS}`` Deletes ``SONGPOS`` from the playlist ``NAME.m3u``. """ raise MpdNotImplemented # TODO @handle_request( r'playlistmove\ "(?P[^"]+)"\ ' r'"(?P\d+)"\ "(?P\d+)"$') def playlistmove(context, name, from_pos, to_pos): """ *musicpd.org, stored playlists section:* ``playlistmove {NAME} {SONGID} {SONGPOS}`` Moves ``SONGID`` in the playlist ``NAME.m3u`` to the position ``SONGPOS``. *Clarifications:* - The second argument is not a ``SONGID`` as used elsewhere in the protocol documentation, but just the ``SONGPOS`` to move *from*, i.e. ``playlistmove {NAME} {FROM_SONGPOS} {TO_SONGPOS}``. """ raise MpdNotImplemented # TODO @handle_request(r'rename\ "(?P[^"]+)"\ "(?P[^"]+)"$') def rename(context, old_name, new_name): """ *musicpd.org, stored playlists section:* ``rename {NAME} {NEW_NAME}`` Renames the playlist ``NAME.m3u`` to ``NEW_NAME.m3u``. """ raise MpdNotImplemented # TODO @handle_request(r'rm\ "(?P[^"]+)"$') def rm(context, name): """ *musicpd.org, stored playlists section:* ``rm {NAME}`` Removes the playlist ``NAME.m3u`` from the playlist directory. """ raise MpdNotImplemented # TODO @handle_request(r'save\ "(?P[^"]+)"$') def save(context, name): """ *musicpd.org, stored playlists section:* ``save {NAME}`` Saves the current playlist to ``NAME.m3u`` in the playlist directory. """ raise MpdNotImplemented # TODO mopidy-0.17.0/mopidy/frontends/mpd/session.py000066400000000000000000000033031224420023200212060ustar00rootroot00000000000000from __future__ import unicode_literals import logging from mopidy.frontends.mpd import dispatcher, protocol from mopidy.utils import formatting, network logger = logging.getLogger('mopidy.frontends.mpd') class MpdSession(network.LineProtocol): """ The MPD client session. Keeps track of a single client session. Any requests from the client is passed on to the MPD request dispatcher. """ terminator = protocol.LINE_TERMINATOR encoding = protocol.ENCODING delimiter = r'\r?\n' def __init__(self, connection, config=None, core=None): super(MpdSession, self).__init__(connection) self.dispatcher = dispatcher.MpdDispatcher( session=self, config=config, core=core) def on_start(self): logger.info('New MPD connection from [%s]:%s', self.host, self.port) self.send_lines(['OK MPD %s' % protocol.VERSION]) def on_line_received(self, line): logger.debug('Request from [%s]:%s: %s', self.host, self.port, line) response = self.dispatcher.handle_request(line) if not response: return logger.debug( 'Response to [%s]:%s: %s', self.host, self.port, formatting.indent(self.terminator.join(response))) self.send_lines(response) def on_idle(self, subsystem): self.dispatcher.handle_idle(subsystem) def decode(self, line): try: return super(MpdSession, self).decode(line.decode('string_escape')) except ValueError: logger.warning( 'Stopping actor due to unescaping error, data ' 'supplied by client was not valid.') self.stop() def close(self): self.stop() mopidy-0.17.0/mopidy/frontends/mpd/translator.py000066400000000000000000000225251224420023200217230ustar00rootroot00000000000000from __future__ import unicode_literals import os import re import shlex import urllib from mopidy.frontends.mpd import protocol from mopidy.frontends.mpd.exceptions import MpdArgError from mopidy.models import TlTrack from mopidy.utils.path import mtime as get_mtime, uri_to_path, split_path # TODO: special handling of local:// uri scheme def track_to_mpd_format(track, position=None): """ Format track for output to MPD client. :param track: the track :type track: :class:`mopidy.models.Track` or :class:`mopidy.models.TlTrack` :param position: track's position in playlist :type position: integer :param key: if we should set key :type key: boolean :param mtime: if we should set mtime :type mtime: boolean :rtype: list of two-tuples """ if isinstance(track, TlTrack): (tlid, track) = track else: (tlid, track) = (None, track) result = [ ('file', track.uri or ''), ('Time', track.length and (track.length // 1000) or 0), ('Artist', artists_to_mpd_format(track.artists)), ('Title', track.name or ''), ('Album', track.album and track.album.name or ''), ] if track.date: result.append(('Date', track.date)) if track.album is not None and track.album.num_tracks != 0: result.append(('Track', '%d/%d' % ( track.track_no, track.album.num_tracks))) else: result.append(('Track', track.track_no)) if position is not None and tlid is not None: result.append(('Pos', position)) result.append(('Id', tlid)) if track.album is not None and track.album.musicbrainz_id is not None: result.append(('MUSICBRAINZ_ALBUMID', track.album.musicbrainz_id)) # FIXME don't use first and best artist? # FIXME don't duplicate following code? if track.album is not None and track.album.artists: artists = artists_to_mpd_format(track.album.artists) result.append(('AlbumArtist', artists)) artists = filter( lambda a: a.musicbrainz_id is not None, track.album.artists) if artists: result.append( ('MUSICBRAINZ_ALBUMARTISTID', artists[0].musicbrainz_id)) if track.artists: artists = filter(lambda a: a.musicbrainz_id is not None, track.artists) if artists: result.append(('MUSICBRAINZ_ARTISTID', artists[0].musicbrainz_id)) if track.composers: result.append(('Composer', artists_to_mpd_format(track.composers))) if track.performers: result.append(('Performer', artists_to_mpd_format(track.performers))) if track.genre: result.append(('Genre', track.genre)) if track.disc_no: result.append(('Disc', track.disc_no)) if track.comment: result.append(('Comment', track.comment)) if track.musicbrainz_id is not None: result.append(('MUSICBRAINZ_TRACKID', track.musicbrainz_id)) return result MPD_KEY_ORDER = ''' key file Time Artist Album AlbumArtist Title Track Genre Date Composer Performer Comment Disc MUSICBRAINZ_ALBUMID MUSICBRAINZ_ALBUMARTISTID MUSICBRAINZ_ARTISTID MUSICBRAINZ_TRACKID mtime '''.split() def order_mpd_track_info(result): """ Order results from :func:`mopidy.frontends.mpd.translator.track_to_mpd_format` so that it matches MPD's ordering. Simply a cosmetic fix for easier diffing of tag_caches. :param result: the track info :type result: list of tuples :rtype: list of tuples """ return sorted(result, key=lambda i: MPD_KEY_ORDER.index(i[0])) def artists_to_mpd_format(artists): """ Format track artists for output to MPD client. :param artists: the artists :type track: array of :class:`mopidy.models.Artist` :rtype: string """ artists = list(artists) artists.sort(key=lambda a: a.name) return ', '.join([a.name for a in artists if a.name]) def tracks_to_mpd_format(tracks, start=0, end=None): """ Format list of tracks for output to MPD client. Optionally limit output to the slice ``[start:end]`` of the list. :param tracks: the tracks :type tracks: list of :class:`mopidy.models.Track` or :class:`mopidy.models.TlTrack` :param start: position of first track to include in output :type start: int (positive or negative) :param end: position after last track to include in output :type end: int (positive or negative) or :class:`None` for end of list :rtype: list of lists of two-tuples """ if end is None: end = len(tracks) tracks = tracks[start:end] positions = range(start, end) assert len(tracks) == len(positions) result = [] for track, position in zip(tracks, positions): result.append(track_to_mpd_format(track, position)) return result def playlist_to_mpd_format(playlist, *args, **kwargs): """ Format playlist for output to MPD client. Arguments as for :func:`tracks_to_mpd_format`, except the first one. """ return tracks_to_mpd_format(playlist.tracks, *args, **kwargs) def query_from_mpd_list_format(field, mpd_query): """ Converts an MPD ``list`` query to a Mopidy query. """ if mpd_query is None: return {} try: # shlex does not seem to be friends with unicode objects tokens = shlex.split(mpd_query.encode('utf-8')) except ValueError as error: if str(error) == 'No closing quotation': raise MpdArgError('Invalid unquoted character', command='list') else: raise tokens = [t.decode('utf-8') for t in tokens] if len(tokens) == 1: if field == 'album': if not tokens[0]: raise ValueError return {'artist': [tokens[0]]} else: raise MpdArgError( 'should be "Album" for 3 arguments', command='list') elif len(tokens) % 2 == 0: query = {} while tokens: key = tokens[0].lower() value = tokens[1] tokens = tokens[2:] if key not in ('artist', 'album', 'albumartist', 'composer', 'date', 'genre', 'performer'): raise MpdArgError('not able to parse args', command='list') if not value: raise ValueError if key in query: query[key].append(value) else: query[key] = [value] return query else: raise MpdArgError('not able to parse args', command='list') # TODO: move to tagcache backend. def tracks_to_tag_cache_format(tracks, media_dir): """ Format list of tracks for output to MPD tag cache :param tracks: the tracks :type tracks: list of :class:`mopidy.models.Track` :param media_dir: the path to the music dir :type media_dir: string :rtype: list of lists of two-tuples """ result = [ ('info_begin',), ('mpd_version', protocol.VERSION), ('fs_charset', protocol.ENCODING), ('info_end',) ] tracks.sort(key=lambda t: t.uri) dirs, files = tracks_to_directory_tree(tracks, media_dir) _add_to_tag_cache(result, dirs, files, media_dir) return result # TODO: bytes only def _add_to_tag_cache(result, dirs, files, media_dir): base_path = media_dir.encode('utf-8') for path, (entry_dirs, entry_files) in dirs.items(): try: text_path = path.decode('utf-8') except UnicodeDecodeError: text_path = urllib.quote(path).decode('utf-8') name = os.path.split(text_path)[1] result.append(('directory', text_path)) result.append(('mtime', get_mtime(os.path.join(base_path, path)))) result.append(('begin', name)) _add_to_tag_cache(result, entry_dirs, entry_files, media_dir) result.append(('end', name)) result.append(('songList begin',)) for track in files: track_result = dict(track_to_mpd_format(track)) # XXX Don't save comments to the tag cache as they may span multiple # lines. We'll start saving track comments when we move from tag_cache # to a JSON file. See #579 for details. if 'Comment' in track_result: del track_result['Comment'] path = uri_to_path(track_result['file']) try: text_path = path.decode('utf-8') except UnicodeDecodeError: text_path = urllib.quote(path).decode('utf-8') relative_path = os.path.relpath(path, base_path) relative_uri = urllib.quote(relative_path) # TODO: use track.last_modified track_result['file'] = relative_uri track_result['mtime'] = get_mtime(path) track_result['key'] = os.path.basename(text_path) track_result = order_mpd_track_info(track_result.items()) result.extend(track_result) result.append(('songList end',)) def tracks_to_directory_tree(tracks, media_dir): directories = ({}, []) for track in tracks: path = b'' current = directories absolute_track_dir_path = os.path.dirname(uri_to_path(track.uri)) relative_track_dir_path = re.sub( '^' + re.escape(media_dir), b'', absolute_track_dir_path) for part in split_path(relative_track_dir_path): path = os.path.join(path, part) if path not in current[0]: current[0][path] = ({}, []) current = current[0][path] current[1].append(track) return directories mopidy-0.17.0/mopidy/models.py000066400000000000000000000270741224420023200162370ustar00rootroot00000000000000from __future__ import unicode_literals import json class ImmutableObject(object): """ Superclass for immutable objects whose fields can only be modified via the constructor. :param kwargs: kwargs to set as fields on the object :type kwargs: any """ def __init__(self, *args, **kwargs): for key, value in kwargs.items(): if not hasattr(self, key) or callable(getattr(self, key)): raise TypeError( '__init__() got an unexpected keyword argument "%s"' % key) self.__dict__[key] = value def __setattr__(self, name, value): if name.startswith('_'): return super(ImmutableObject, self).__setattr__(name, value) raise AttributeError('Object is immutable.') def __repr__(self): kwarg_pairs = [] for (key, value) in sorted(self.__dict__.items()): if isinstance(value, (frozenset, tuple)): value = list(value) kwarg_pairs.append('%s=%s' % (key, repr(value))) return '%(classname)s(%(kwargs)s)' % { 'classname': self.__class__.__name__, 'kwargs': ', '.join(kwarg_pairs), } def __hash__(self): hash_sum = 0 for key, value in self.__dict__.items(): hash_sum += hash(key) + hash(value) return hash_sum def __eq__(self, other): if not isinstance(other, self.__class__): return False return self.__dict__ == other.__dict__ def __ne__(self, other): return not self.__eq__(other) def copy(self, **values): """ Copy the model with ``field`` updated to new value. Examples:: # Returns a track with a new name Track(name='foo').copy(name='bar') # Return an album with a new number of tracks Album(num_tracks=2).copy(num_tracks=5) :param values: the model fields to modify :type values: dict :rtype: new instance of the model being copied """ data = {} for key in self.__dict__.keys(): public_key = key.lstrip('_') data[public_key] = values.pop(public_key, self.__dict__[key]) for key in values.keys(): if hasattr(self, key): data[key] = values.pop(key) if values: raise TypeError( 'copy() got an unexpected keyword argument "%s"' % key) return self.__class__(**data) def serialize(self): data = {} data['__model__'] = self.__class__.__name__ for key in self.__dict__.keys(): public_key = key.lstrip('_') value = self.__dict__[key] if isinstance(value, (set, frozenset, list, tuple)): value = [ v.serialize() if isinstance(v, ImmutableObject) else v for v in value] elif isinstance(value, ImmutableObject): value = value.serialize() if not (isinstance(value, list) and len(value) == 0): data[public_key] = value return data class ModelJSONEncoder(json.JSONEncoder): """ Automatically serialize Mopidy models to JSON. Usage:: >>> import json >>> json.dumps({'a_track': Track(name='name')}, cls=ModelJSONEncoder) '{"a_track": {"__model__": "Track", "name": "name"}}' """ def default(self, obj): if isinstance(obj, ImmutableObject): return obj.serialize() return json.JSONEncoder.default(self, obj) def model_json_decoder(dct): """ Automatically deserialize Mopidy models from JSON. Usage:: >>> import json >>> json.loads( ... '{"a_track": {"__model__": "Track", "name": "name"}}', ... object_hook=model_json_decoder) {u'a_track': Track(artists=[], name=u'name')} """ if '__model__' in dct: model_name = dct.pop('__model__') cls = globals().get(model_name, None) if issubclass(cls, ImmutableObject): kwargs = {} for key, value in dct.items(): kwargs[key] = value return cls(**kwargs) return dct class Artist(ImmutableObject): """ :param uri: artist URI :type uri: string :param name: artist name :type name: string :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string """ #: The artist URI. Read-only. uri = None #: The artist name. Read-only. name = None #: The MusicBrainz ID of the artist. Read-only. musicbrainz_id = None class Album(ImmutableObject): """ :param uri: album URI :type uri: string :param name: album name :type name: string :param artists: album artists :type artists: list of :class:`Artist` :param num_tracks: number of tracks in album :type num_tracks: integer :param num_discs: number of discs in album :type num_discs: integer or :class:`None` if unknown :param date: album release date (YYYY or YYYY-MM-DD) :type date: string :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string :param images: album image URIs :type images: list of strings """ #: The album URI. Read-only. uri = None #: The album name. Read-only. name = None #: A set of album artists. Read-only. artists = frozenset() #: The number of tracks in the album. Read-only. num_tracks = 0 #: The number of discs in the album. Read-only. num_discs = None #: The album release date. Read-only. date = None #: The MusicBrainz ID of the album. Read-only. musicbrainz_id = None #: The album image URIs. Read-only. images = frozenset() # XXX If we want to keep the order of images we shouldn't use frozenset() # as it doesn't preserve order. I'm deferring this issue until we got # actual usage of this field with more than one image. def __init__(self, *args, **kwargs): self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) self.__dict__['images'] = frozenset(kwargs.pop('images', [])) super(Album, self).__init__(*args, **kwargs) class Track(ImmutableObject): """ :param uri: track URI :type uri: string :param name: track name :type name: string :param artists: track artists :type artists: list of :class:`Artist` :param album: track album :type album: :class:`Album` :param composers: track composers :type composers: string :param performers: track performers :type performers: string :param genre: track genre :type genre: string :param track_no: track number in album :type track_no: integer :param disc_no: disc number in album :type disc_no: integer or :class:`None` if unknown :param date: track release date (YYYY or YYYY-MM-DD) :type date: string :param length: track length in milliseconds :type length: integer :param bitrate: bitrate in kbit/s :type bitrate: integer :param comment: track comment :type comment: string :param musicbrainz_id: MusicBrainz ID :type musicbrainz_id: string :param last_modified: Represents last modification time :type last_modified: integer """ #: The track URI. Read-only. uri = None #: The track name. Read-only. name = None #: A set of track artists. Read-only. artists = frozenset() #: The track :class:`Album`. Read-only. album = None #: A set of track composers. Read-only. composers = frozenset() #: A set of track performers`. Read-only. performers = frozenset() #: The track genre. Read-only. genre = None #: The track number in the album. Read-only. track_no = 0 #: The disc number in the album. Read-only. disc_no = None #: The track release date. Read-only. date = None #: The track length in milliseconds. Read-only. length = None #: The track's bitrate in kbit/s. Read-only. bitrate = None #: The track comment. Read-only. comment = None #: The MusicBrainz ID of the track. Read-only. musicbrainz_id = None #: Integer representing when the track was last modified, exact meaning #: depends on source of track. For local files this is the mtime, for other #: backends it could be a timestamp or simply a version counter. last_modified = 0 def __init__(self, *args, **kwargs): self.__dict__['artists'] = frozenset(kwargs.pop('artists', [])) self.__dict__['composers'] = frozenset(kwargs.pop('composers', [])) self.__dict__['performers'] = frozenset(kwargs.pop('performers', [])) super(Track, self).__init__(*args, **kwargs) class TlTrack(ImmutableObject): """ A tracklist track. Wraps a regular track and it's tracklist ID. The use of :class:`TlTrack` allows the same track to appear multiple times in the tracklist. This class also accepts it's parameters as positional arguments. Both arguments must be provided, and they must appear in the order they are listed here. This class also supports iteration, so your extract its values like this:: (tlid, track) = tl_track :param tlid: tracklist ID :type tlid: int :param track: the track :type track: :class:`Track` """ #: The tracklist ID. Read-only. tlid = None #: The track. Read-only. track = None def __init__(self, *args, **kwargs): if len(args) == 2 and len(kwargs) == 0: kwargs['tlid'] = args[0] kwargs['track'] = args[1] args = [] super(TlTrack, self).__init__(*args, **kwargs) def __iter__(self): return iter([self.tlid, self.track]) class Playlist(ImmutableObject): """ :param uri: playlist URI :type uri: string :param name: playlist name :type name: string :param tracks: playlist's tracks :type tracks: list of :class:`Track` elements :param last_modified: playlist's modification time in UTC :type last_modified: :class:`datetime.datetime` """ #: The playlist URI. Read-only. uri = None #: The playlist name. Read-only. name = None #: The playlist's tracks. Read-only. tracks = tuple() #: The playlist modification time in UTC. Read-only. #: #: :class:`datetime.datetime`, or :class:`None` if unknown. last_modified = None def __init__(self, *args, **kwargs): self.__dict__['tracks'] = tuple(kwargs.pop('tracks', [])) super(Playlist, self).__init__(*args, **kwargs) # TODO: def insert(self, pos, track): ... ? @property def length(self): """The number of tracks in the playlist. Read-only.""" return len(self.tracks) class SearchResult(ImmutableObject): """ :param uri: search result URI :type uri: string :param tracks: matching tracks :type tracks: list of :class:`Track` elements :param artists: matching artists :type artists: list of :class:`Artist` elements :param albums: matching albums :type albums: list of :class:`Album` elements """ # The search result URI. Read-only. uri = None # The tracks matching the search query. Read-only. tracks = tuple() # The artists matching the search query. Read-only. artists = tuple() # The albums matching the search query. Read-only. albums = tuple() def __init__(self, *args, **kwargs): self.__dict__['tracks'] = tuple(kwargs.pop('tracks', [])) self.__dict__['artists'] = tuple(kwargs.pop('artists', [])) self.__dict__['albums'] = tuple(kwargs.pop('albums', [])) super(SearchResult, self).__init__(*args, **kwargs) mopidy-0.17.0/mopidy/utils/000077500000000000000000000000001224420023200155305ustar00rootroot00000000000000mopidy-0.17.0/mopidy/utils/__init__.py000066400000000000000000000000501224420023200176340ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/mopidy/utils/deps.py000066400000000000000000000107611224420023200170420ustar00rootroot00000000000000from __future__ import unicode_literals import functools import os import platform import pygst pygst.require('0.10') import gst import pkg_resources from . import formatting def format_dependency_list(adapters=None): if adapters is None: dist_names = set([ ep.dist.project_name for ep in pkg_resources.iter_entry_points('mopidy.ext') if ep.dist.project_name != 'Mopidy']) dist_infos = [ functools.partial(pkg_info, dist_name) for dist_name in dist_names] adapters = [ platform_info, python_info, functools.partial(pkg_info, 'Mopidy', True) ] + dist_infos + [ gstreamer_info, ] return '\n'.join([_format_dependency(a()) for a in adapters]) def _format_dependency(dep_info): lines = [] if 'version' not in dep_info: lines.append('%s: not found' % dep_info['name']) else: if 'path' in dep_info: source = ' from %s' % dep_info['path'] else: source = '' lines.append('%s: %s%s' % ( dep_info['name'], dep_info['version'], source, )) if 'other' in dep_info: lines.append(' Detailed information: %s' % ( formatting.indent(dep_info['other'], places=4)),) if dep_info.get('dependencies', []): for sub_dep_info in dep_info['dependencies']: sub_dep_lines = _format_dependency(sub_dep_info) lines.append( formatting.indent(sub_dep_lines, places=2, singles=True)) return '\n'.join(lines) def platform_info(): return { 'name': 'Platform', 'version': platform.platform(), } def python_info(): return { 'name': 'Python', 'version': '%s %s' % ( platform.python_implementation(), platform.python_version()), 'path': os.path.dirname(platform.__file__), } def pkg_info(project_name=None, include_extras=False): if project_name is None: project_name = 'Mopidy' try: distribution = pkg_resources.get_distribution(project_name) extras = include_extras and distribution.extras or [] dependencies = [ pkg_info(d) for d in distribution.requires(extras)] return { 'name': project_name, 'version': distribution.version, 'path': distribution.location, 'dependencies': dependencies, } except pkg_resources.ResolutionError: return { 'name': project_name, } def gstreamer_info(): other = [] other.append('Python wrapper: gst-python %s' % ( '.'.join(map(str, gst.get_pygst_version())))) found_elements = [] missing_elements = [] for name, status in _gstreamer_check_elements(): if status: found_elements.append(name) else: missing_elements.append(name) other.append('Relevant elements:') other.append(' Found:') for element in found_elements: other.append(' %s' % element) if not found_elements: other.append(' none') other.append(' Not found:') for element in missing_elements: other.append(' %s' % element) if not missing_elements: other.append(' none') return { 'name': 'GStreamer', 'version': '.'.join(map(str, gst.get_gst_version())), 'path': os.path.dirname(gst.__file__), 'other': '\n'.join(other), } def _gstreamer_check_elements(): elements_to_check = [ # Core playback 'uridecodebin', # External HTTP streams 'souphttpsrc', # Spotify 'appsrc', # Mixers and sinks 'alsamixer', 'alsasink', 'ossmixer', 'osssink', 'oss4mixer', 'oss4sink', 'pulsemixer', 'pulsesink', # MP3 encoding and decoding 'mp3parse', 'mad', 'id3demux', 'id3v2mux', 'lame', # Ogg Vorbis encoding and decoding 'vorbisdec', 'vorbisenc', 'vorbisparse', 'oggdemux', 'oggmux', 'oggparse', # Flac decoding 'flacdec', 'flacparse', # Shoutcast output 'shout2send', ] known_elements = [ factory.get_name() for factory in gst.registry_get_default().get_feature_list(gst.TYPE_ELEMENT_FACTORY)] return [ (element, element in known_elements) for element in elements_to_check] mopidy-0.17.0/mopidy/utils/encoding.py000066400000000000000000000003311224420023200176650ustar00rootroot00000000000000from __future__ import unicode_literals import locale def locale_decode(bytestr): try: return unicode(bytestr) except UnicodeError: return str(bytestr).decode(locale.getpreferredencoding()) mopidy-0.17.0/mopidy/utils/formatting.py000066400000000000000000000016031224420023200202540ustar00rootroot00000000000000from __future__ import unicode_literals import re import unicodedata def indent(string, places=4, linebreak='\n', singles=False): lines = string.split(linebreak) if not singles and len(lines) == 1: return string for i, line in enumerate(lines): lines[i] = ' ' * places + line result = linebreak.join(lines) if not singles: result = linebreak + result return result def slugify(value): """ Converts to lowercase, removes non-word characters (alphanumerics and underscores) and converts spaces to hyphens. Also strips leading and trailing whitespace. This function is based on Django's slugify implementation. """ value = unicodedata.normalize('NFKD', value) value = value.encode('ascii', 'ignore').decode('ascii') value = re.sub(r'[^\w\s-]', '', value).strip().lower() return re.sub(r'[-\s]+', '-', value) mopidy-0.17.0/mopidy/utils/jsonrpc.py000066400000000000000000000301411224420023200175570ustar00rootroot00000000000000from __future__ import unicode_literals import inspect import json import traceback import pykka class JsonRpcWrapper(object): """ Wrap objects and make them accessible through JSON-RPC 2.0 messaging. This class takes responsibility of communicating with the objects and processing of JSON-RPC 2.0 messages. The transport of the messages over HTTP, WebSocket, TCP, or whatever is of no concern to this class. The wrapper supports exporting the methods of one or more objects. Either way, the objects must be exported with method name prefixes, called "mounts". To expose objects, add them all to the objects mapping. The key in the mapping is used as the object's mounting point in the exposed API:: jrw = JsonRpcWrapper(objects={ 'foo': foo, 'hello': lambda: 'Hello, world!', }) This will export the Python callables on the left as the JSON-RPC 2.0 method names on the right:: foo.bar() -> foo.bar foo.baz() -> foo.baz lambda -> hello Only the public methods of the mounted objects, or functions/methods included directly in the mapping, will be exposed. If a method returns a :class:`pykka.Future`, the future will be completed and its value unwrapped before the JSON-RPC wrapper returns the response. For further details on the JSON-RPC 2.0 spec, see http://www.jsonrpc.org/specification :param objects: mapping between mounting points and exposed functions or class instances :type objects: dict :param decoders: object builders to be used by :func`json.loads` :type decoders: list of functions taking a dict and returning a dict :param encoders: object serializers to be used by :func:`json.dumps` :type encoders: list of :class:`json.JSONEncoder` subclasses with the method :meth:`default` implemented """ def __init__(self, objects, decoders=None, encoders=None): if '' in objects.keys(): raise AttributeError( 'The empty string is not allowed as an object mount') self.objects = objects self.decoder = get_combined_json_decoder(decoders or []) self.encoder = get_combined_json_encoder(encoders or []) def handle_json(self, request): """ Handles an incoming request encoded as a JSON string. Returns a response as a JSON string for commands, and :class:`None` for notifications. :param request: the serialized JSON-RPC request :type request: string :rtype: string or :class:`None` """ try: request = json.loads(request, object_hook=self.decoder) except ValueError: response = JsonRpcParseError().get_response() else: response = self.handle_data(request) if response is None: return None return json.dumps(response, cls=self.encoder) def handle_data(self, request): """ Handles an incoming request in the form of a Python data structure. Returns a Python data structure for commands, or a :class:`None` for notifications. :param request: the unserialized JSON-RPC request :type request: dict :rtype: dict, list, or :class:`None` """ if isinstance(request, list): return self._handle_batch(request) else: return self._handle_single_request(request) def _handle_batch(self, requests): if not requests: return JsonRpcInvalidRequestError( data='Batch list cannot be empty').get_response() responses = [] for request in requests: response = self._handle_single_request(request) if response: responses.append(response) return responses or None def _handle_single_request(self, request): try: self._validate_request(request) args, kwargs = self._get_params(request) except JsonRpcInvalidRequestError as error: return error.get_response() try: method = self._get_method(request['method']) try: result = method(*args, **kwargs) if self._is_notification(request): return None result = self._unwrap_result(result) return { 'jsonrpc': '2.0', 'id': request['id'], 'result': result, } except TypeError as error: raise JsonRpcInvalidParamsError(data={ 'type': error.__class__.__name__, 'message': unicode(error), 'traceback': traceback.format_exc(), }) except Exception as error: raise JsonRpcApplicationError(data={ 'type': error.__class__.__name__, 'message': unicode(error), 'traceback': traceback.format_exc(), }) except JsonRpcError as error: if self._is_notification(request): return None return error.get_response(request['id']) def _validate_request(self, request): if not isinstance(request, dict): raise JsonRpcInvalidRequestError( data='Request must be an object') if not 'jsonrpc' in request: raise JsonRpcInvalidRequestError( data='"jsonrpc" member must be included') if request['jsonrpc'] != '2.0': raise JsonRpcInvalidRequestError( data='"jsonrpc" value must be "2.0"') if not 'method' in request: raise JsonRpcInvalidRequestError( data='"method" member must be included') if not isinstance(request['method'], unicode): raise JsonRpcInvalidRequestError( data='"method" must be a string') def _get_params(self, request): if not 'params' in request: return [], {} params = request['params'] if isinstance(params, list): return params, {} elif isinstance(params, dict): return [], params else: raise JsonRpcInvalidRequestError( data='"params", if given, must be an array or an object') def _get_method(self, method_path): if callable(self.objects.get(method_path, None)): # The mounted object is the callable return self.objects[method_path] # The mounted object contains the callable if '.' not in method_path: raise JsonRpcMethodNotFoundError( data='Could not find object mount in method name "%s"' % ( method_path)) mount, method_name = method_path.rsplit('.', 1) if method_name.startswith('_'): raise JsonRpcMethodNotFoundError( data='Private methods are not exported') try: obj = self.objects[mount] except KeyError: raise JsonRpcMethodNotFoundError( data='No object found at "%s"' % mount) try: return getattr(obj, method_name) except AttributeError: raise JsonRpcMethodNotFoundError( data='Object mounted at "%s" has no member "%s"' % ( mount, method_name)) def _is_notification(self, request): return 'id' not in request def _unwrap_result(self, result): if isinstance(result, pykka.Future): result = result.get() return result class JsonRpcError(Exception): code = -32000 message = 'Unspecified server error' def __init__(self, data=None): self.data = data def get_response(self, request_id=None): response = { 'jsonrpc': '2.0', 'id': request_id, 'error': { 'code': self.code, 'message': self.message, }, } if self.data: response['error']['data'] = self.data return response class JsonRpcParseError(JsonRpcError): code = -32700 message = 'Parse error' class JsonRpcInvalidRequestError(JsonRpcError): code = -32600 message = 'Invalid Request' class JsonRpcMethodNotFoundError(JsonRpcError): code = -32601 message = 'Method not found' class JsonRpcInvalidParamsError(JsonRpcError): code = -32602 message = 'Invalid params' class JsonRpcApplicationError(JsonRpcError): code = 0 message = 'Application error' def get_combined_json_decoder(decoders): def decode(dct): for decoder in decoders: dct = decoder(dct) return dct return decode def get_combined_json_encoder(encoders): class JsonRpcEncoder(json.JSONEncoder): def default(self, obj): for encoder in encoders: try: return encoder().default(obj) except TypeError: pass # Try next encoder return json.JSONEncoder.default(self, obj) return JsonRpcEncoder class JsonRpcInspector(object): """ Inspects a group of classes and functions to create a description of what methods they can expose over JSON-RPC 2.0. To inspect one or more classes, add them all to the objects mapping. The key in the mapping is used as the classes' mounting point in the exposed API:: jri = JsonRpcInspector(objects={ 'foo': Foo, 'hello': lambda: 'Hello, world!', }) Since the inspector is based on inspecting classes and not instances, it will not include methods added dynamically. The wrapper works with instances, and it will thus export dynamically added methods as well. :param objects: mapping between mounts and exposed functions or classes :type objects: dict """ def __init__(self, objects): if '' in objects.keys(): raise AttributeError( 'The empty string is not allowed as an object mount') self.objects = objects def describe(self): """ Inspects the object and returns a data structure which describes the available properties and methods. """ methods = {} for mount, obj in self.objects.iteritems(): if inspect.isroutine(obj): methods[mount] = self._describe_method(obj) else: obj_methods = self._get_methods(obj) for name, description in obj_methods.iteritems(): if mount: name = '%s.%s' % (mount, name) methods[name] = description return methods def _get_methods(self, obj): methods = {} for name, value in inspect.getmembers(obj): if name.startswith('_'): continue if not inspect.isroutine(value): continue method = self._describe_method(value) if method: methods[name] = method return methods def _describe_method(self, method): return { 'description': inspect.getdoc(method), 'params': self._describe_params(method), } def _describe_params(self, method): argspec = inspect.getargspec(method) defaults = argspec.defaults and list(argspec.defaults) or [] num_args_without_default = len(argspec.args) - len(defaults) no_defaults = [None] * num_args_without_default defaults = no_defaults + defaults params = [] for arg, default in zip(argspec.args, defaults): if arg == 'self': continue params.append({'name': arg}) if argspec.defaults: for i, default in enumerate(reversed(argspec.defaults)): params[len(params) - i - 1]['default'] = default if argspec.varargs: params.append({ 'name': argspec.varargs, 'varargs': True, }) if argspec.keywords: params.append({ 'name': argspec.keywords, 'kwargs': True, }) return params mopidy-0.17.0/mopidy/utils/log.py000066400000000000000000000042371224420023200166710ustar00rootroot00000000000000from __future__ import unicode_literals import logging import logging.config import logging.handlers class DelayedHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) self._released = False self._buffer = [] def handle(self, record): if not self._released: self._buffer.append(record) def release(self): self._released = True root = logging.getLogger('') while self._buffer: root.handle(self._buffer.pop(0)) _delayed_handler = DelayedHandler() def bootstrap_delayed_logging(): root = logging.getLogger('') root.setLevel(logging.DEBUG) root.addHandler(_delayed_handler) def setup_logging(config, verbosity_level, save_debug_log): setup_console_logging(config, verbosity_level) setup_log_levels(config) if save_debug_log: setup_debug_logging_to_file(config) logging.captureWarnings(True) if config['logging']['config_file']: logging.config.fileConfig(config['logging']['config_file']) _delayed_handler.release() def setup_log_levels(config): for name, level in config['loglevels'].items(): logging.getLogger(name).setLevel(level) def setup_console_logging(config, verbosity_level): if verbosity_level < 0: log_level = logging.WARNING log_format = config['logging']['console_format'] elif verbosity_level >= 1: log_level = logging.DEBUG log_format = config['logging']['debug_format'] else: log_level = logging.INFO log_format = config['logging']['console_format'] formatter = logging.Formatter(log_format) handler = logging.StreamHandler() handler.setFormatter(formatter) handler.setLevel(log_level) root = logging.getLogger('') root.addHandler(handler) def setup_debug_logging_to_file(config): formatter = logging.Formatter(config['logging']['debug_format']) handler = logging.handlers.RotatingFileHandler( config['logging']['debug_file'], maxBytes=10485760, backupCount=3) handler.setFormatter(formatter) handler.setLevel(logging.DEBUG) root = logging.getLogger('') root.addHandler(handler) mopidy-0.17.0/mopidy/utils/network.py000066400000000000000000000300431224420023200175730ustar00rootroot00000000000000from __future__ import unicode_literals import errno import gobject import logging import re import socket import threading import pykka from mopidy.utils import encoding logger = logging.getLogger('mopidy.utils.server') class ShouldRetrySocketCall(Exception): """Indicate that attempted socket call should be retried""" def try_ipv6_socket(): """Determine if system really supports IPv6""" if not socket.has_ipv6: return False try: socket.socket(socket.AF_INET6).close() return True except IOError as error: logger.debug( 'Platform supports IPv6, but socket creation failed, ' 'disabling: %s', encoding.locale_decode(error)) return False #: Boolean value that indicates if creating an IPv6 socket will succeed. has_ipv6 = try_ipv6_socket() def create_socket(): """Create a TCP socket with or without IPv6 depending on system support""" if has_ipv6: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) # Explicitly configure socket to work for both IPv4 and IPv6 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) return sock def format_hostname(hostname): """Format hostname for display.""" if (has_ipv6 and re.match(r'\d+.\d+.\d+.\d+', hostname) is not None): hostname = '::ffff:%s' % hostname return hostname class Server(object): """Setup listener and register it with gobject's event loop.""" def __init__(self, host, port, protocol, protocol_kwargs=None, max_connections=5, timeout=30): self.protocol = protocol self.protocol_kwargs = protocol_kwargs or {} self.max_connections = max_connections self.timeout = timeout self.server_socket = self.create_server_socket(host, port) self.register_server_socket(self.server_socket.fileno()) def create_server_socket(self, host, port): sock = create_socket() sock.setblocking(False) sock.bind((host, port)) sock.listen(1) return sock def register_server_socket(self, fileno): gobject.io_add_watch(fileno, gobject.IO_IN, self.handle_connection) def handle_connection(self, fd, flags): try: sock, addr = self.accept_connection() except ShouldRetrySocketCall: return True if self.maximum_connections_exceeded(): self.reject_connection(sock, addr) else: self.init_connection(sock, addr) return True def accept_connection(self): try: return self.server_socket.accept() except socket.error as e: if e.errno in (errno.EAGAIN, errno.EINTR): raise ShouldRetrySocketCall raise def maximum_connections_exceeded(self): return (self.max_connections is not None and self.number_of_connections() >= self.max_connections) def number_of_connections(self): return len(pykka.ActorRegistry.get_by_class(self.protocol)) def reject_connection(self, sock, addr): # FIXME provide more context in logging? logger.warning('Rejected connection from [%s]:%s', addr[0], addr[1]) try: sock.close() except socket.error: pass def init_connection(self, sock, addr): Connection( self.protocol, self.protocol_kwargs, sock, addr, self.timeout) class Connection(object): # NOTE: the callback code is _not_ run in the actor's thread, but in the # same one as the event loop. If code in the callbacks blocks, the rest of # gobject code will likely be blocked as well... # # Also note that source_remove() return values are ignored on purpose, a # false return value would only tell us that what we thought was registered # is already gone, there is really nothing more we can do. def __init__(self, protocol, protocol_kwargs, sock, addr, timeout): sock.setblocking(False) self.host, self.port = addr[:2] # IPv6 has larger addr self.sock = sock self.protocol = protocol self.protocol_kwargs = protocol_kwargs self.timeout = timeout self.send_lock = threading.Lock() self.send_buffer = b'' self.stopping = False self.recv_id = None self.send_id = None self.timeout_id = None self.actor_ref = self.protocol.start(self, **self.protocol_kwargs) self.enable_recv() self.enable_timeout() def stop(self, reason, level=logging.DEBUG): if self.stopping: logger.log(level, 'Already stopping: %s' % reason) return else: self.stopping = True logger.log(level, reason) try: self.actor_ref.stop(block=False) except pykka.ActorDeadError: pass self.disable_timeout() self.disable_recv() self.disable_send() try: self.sock.close() except socket.error: pass def queue_send(self, data): """Try to send data to client exactly as is and queue rest.""" self.send_lock.acquire(True) self.send_buffer = self.send(self.send_buffer + data) self.send_lock.release() if self.send_buffer: self.enable_send() def send(self, data): """Send data to client, return any unsent data.""" try: sent = self.sock.send(data) return data[sent:] except socket.error as e: if e.errno in (errno.EWOULDBLOCK, errno.EINTR): return data self.stop('Unexpected client error: %s' % e) return b'' def enable_timeout(self): """Reactivate timeout mechanism.""" if self.timeout <= 0: return self.disable_timeout() self.timeout_id = gobject.timeout_add_seconds( self.timeout, self.timeout_callback) def disable_timeout(self): """Deactivate timeout mechanism.""" if self.timeout_id is None: return gobject.source_remove(self.timeout_id) self.timeout_id = None def enable_recv(self): if self.recv_id is not None: return try: self.recv_id = gobject.io_add_watch( self.sock.fileno(), gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.recv_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) def disable_recv(self): if self.recv_id is None: return gobject.source_remove(self.recv_id) self.recv_id = None def enable_send(self): if self.send_id is not None: return try: self.send_id = gobject.io_add_watch( self.sock.fileno(), gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self.send_callback) except socket.error as e: self.stop('Problem with connection: %s' % e) def disable_send(self): if self.send_id is None: return gobject.source_remove(self.send_id) self.send_id = None def recv_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True try: data = self.sock.recv(4096) except socket.error as e: if e.errno not in (errno.EWOULDBLOCK, errno.EINTR): self.stop('Unexpected client error: %s' % e) return True if not data: self.stop('Client most likely disconnected.') return True try: self.actor_ref.tell({'received': data}) except pykka.ActorDeadError: self.stop('Actor is dead.') return True def send_callback(self, fd, flags): if flags & (gobject.IO_ERR | gobject.IO_HUP): self.stop('Bad client flags: %s' % flags) return True # If with can't get the lock, simply try again next time socket is # ready for sending. if not self.send_lock.acquire(False): return True try: self.send_buffer = self.send(self.send_buffer) if not self.send_buffer: self.disable_send() finally: self.send_lock.release() return True def timeout_callback(self): self.stop('Client inactive for %ds; closing connection' % self.timeout) return False class LineProtocol(pykka.ThreadingActor): """ Base class for handling line based protocols. Takes care of receiving new data from server's client code, decoding and then splitting data along line boundaries. """ #: Line terminator to use for outputed lines. terminator = '\n' #: Regex to use for spliting lines, will be set compiled version of its #: own value, or to ``terminator``s value if it is not set itself. delimiter = None #: What encoding to expect incomming data to be in, can be :class:`None`. encoding = 'utf-8' def __init__(self, connection): super(LineProtocol, self).__init__() self.connection = connection self.prevent_timeout = False self.recv_buffer = b'' if self.delimiter: self.delimiter = re.compile(self.delimiter) else: self.delimiter = re.compile(self.terminator) @property def host(self): return self.connection.host @property def port(self): return self.connection.port def on_line_received(self, line): """ Called whenever a new line is found. Should be implemented by subclasses. """ raise NotImplementedError def on_receive(self, message): """Handle messages with new data from server.""" if 'received' not in message: return self.connection.disable_timeout() self.recv_buffer += message['received'] for line in self.parse_lines(): line = self.decode(line) if line is not None: self.on_line_received(line) if not self.prevent_timeout: self.connection.enable_timeout() def on_stop(self): """Ensure that cleanup when actor stops.""" self.connection.stop('Actor is shutting down.') def parse_lines(self): """Consume new data and yield any lines found.""" while re.search(self.terminator, self.recv_buffer): line, self.recv_buffer = self.delimiter.split( self.recv_buffer, 1) yield line def encode(self, line): """ Handle encoding of line. Can be overridden by subclasses to change encoding behaviour. """ try: return line.encode(self.encoding) except UnicodeError: logger.warning( 'Stopping actor due to encode problem, data ' 'supplied by client was not valid %s', self.encoding) self.stop() def decode(self, line): """ Handle decoding of line. Can be overridden by subclasses to change decoding behaviour. """ try: return line.decode(self.encoding) except UnicodeError: logger.warning( 'Stopping actor due to decode problem, data ' 'supplied by client was not valid %s', self.encoding) self.stop() def join_lines(self, lines): if not lines: return '' return self.terminator.join(lines) + self.terminator def send_lines(self, lines): """ Send array of lines to client via connection. Join lines using the terminator that is set for this class, encode it and send it to the client. """ if not lines: return data = self.join_lines(lines) self.connection.queue_send(self.encode(data)) mopidy-0.17.0/mopidy/utils/path.py000066400000000000000000000121061224420023200170360ustar00rootroot00000000000000from __future__ import unicode_literals import logging import os import string import urllib import urlparse import glib logger = logging.getLogger('mopidy.utils.path') XDG_DIRS = { 'XDG_CACHE_DIR': glib.get_user_cache_dir(), 'XDG_CONFIG_DIR': glib.get_user_config_dir(), 'XDG_DATA_DIR': glib.get_user_data_dir(), 'XDG_MUSIC_DIR': glib.get_user_special_dir(glib.USER_DIRECTORY_MUSIC), } # XDG_MUSIC_DIR can be none, so filter out any bad data. XDG_DIRS = dict((k, v) for k, v in XDG_DIRS.items() if v is not None) def get_or_create_dir(dir_path): if not isinstance(dir_path, bytes): raise ValueError('Path is not a bytestring.') dir_path = expand_path(dir_path) if os.path.isfile(dir_path): raise OSError( 'A file with the same name as the desired dir, ' '"%s", already exists.' % dir_path) elif not os.path.isdir(dir_path): logger.info('Creating dir %s', dir_path) os.makedirs(dir_path, 0755) return dir_path def get_or_create_file(file_path, mkdir=True, content=None): if not isinstance(file_path, bytes): raise ValueError('Path is not a bytestring.') file_path = expand_path(file_path) if mkdir: get_or_create_dir(os.path.dirname(file_path)) if not os.path.isfile(file_path): logger.info('Creating file %s', file_path) with open(file_path, 'w') as fh: if content: fh.write(content) return file_path def path_to_uri(path): """ Convert OS specific path to file:// URI. Accepts either unicode strings or bytestrings. The encoding of any bytestring will be maintained so that :func:`uri_to_path` can return the same bytestring. Returns a file:// URI as an unicode string. """ if isinstance(path, unicode): path = path.encode('utf-8') path = urllib.quote(path) return urlparse.urlunsplit((b'file', b'', path, b'', b'')) def uri_to_path(uri): """ Convert an URI to a OS specific path. Returns a bytestring, since the file path can contain chars with other encoding than UTF-8. If we had returned these paths as unicode strings, you wouldn't be able to look up the matching dir or file on your file system because the exact path would be lost by ignoring its encoding. """ if isinstance(uri, unicode): uri = uri.encode('utf-8') return urllib.unquote(urlparse.urlsplit(uri).path) def split_path(path): parts = [] while True: path, part = os.path.split(path) if part: parts.insert(0, part) if not path or path == b'/': break return parts def expand_path(path): # TODO: document as we want people to use this. if not isinstance(path, bytes): raise ValueError('Path is not a bytestring.') try: path = string.Template(path).substitute(XDG_DIRS) except KeyError: return None path = os.path.expanduser(path) path = os.path.abspath(path) return path def find_files(path): """ Finds all files within a path. Directories and files with names starting with ``.`` is ignored. :returns: yields the full path to files as bytestrings """ if isinstance(path, unicode): path = path.encode('utf-8') if os.path.isfile(path): if not os.path.basename(path).startswith(b'.'): yield path else: for dirpath, dirnames, filenames in os.walk(path, followlinks=True): for dirname in dirnames: if dirname.startswith(b'.'): # Skip hidden dirs by modifying dirnames inplace dirnames.remove(dirname) for filename in filenames: if filename.startswith(b'.'): # Skip hidden files continue yield os.path.join(dirpath, filename) def find_uris(path): for p in find_files(path): yield path_to_uri(p) def check_file_path_is_inside_base_dir(file_path, base_path): assert not file_path.endswith(os.sep), ( 'File path %s cannot end with a path separator' % file_path) # Expand symlinks real_base_path = os.path.realpath(base_path) real_file_path = os.path.realpath(file_path) # Use dir of file for prefix comparision, so we don't accept # /tmp/foo.m3u as being inside /tmp/foo, simply because they have a # common prefix, /tmp/foo, which matches the base path, /tmp/foo. real_dir_path = os.path.dirname(real_file_path) # Check if dir of file is the base path or a subdir common_prefix = os.path.commonprefix([real_base_path, real_dir_path]) assert common_prefix == real_base_path, ( 'File path %s must be in %s' % (real_file_path, real_base_path)) # FIXME replace with mock usage in tests. class Mtime(object): def __init__(self): self.fake = None def __call__(self, path): if self.fake is not None: return self.fake return int(os.stat(path).st_mtime) def set_fake_time(self, time): self.fake = time def undo_fake(self): self.fake = None mtime = Mtime() mopidy-0.17.0/mopidy/utils/process.py000066400000000000000000000041531224420023200175630ustar00rootroot00000000000000from __future__ import unicode_literals import logging import signal import thread import threading from pykka import ActorDeadError from pykka.registry import ActorRegistry logger = logging.getLogger('mopidy.utils.process') SIGNALS = dict( (k, v) for v, k in signal.__dict__.iteritems() if v.startswith('SIG') and not v.startswith('SIG_')) def exit_process(): logger.debug('Interrupting main...') thread.interrupt_main() logger.debug('Interrupted main') def exit_handler(signum, frame): """A :mod:`signal` handler which will exit the program on signal.""" logger.info('Got %s signal', SIGNALS[signum]) exit_process() def stop_actors_by_class(klass): actors = ActorRegistry.get_by_class(klass) logger.debug('Stopping %d instance(s) of %s', len(actors), klass.__name__) for actor in actors: actor.stop() def stop_remaining_actors(): num_actors = len(ActorRegistry.get_all()) while num_actors: logger.error( 'There are actor threads still running, this is probably a bug') logger.debug( 'Seeing %d actor and %d non-actor thread(s): %s', num_actors, threading.active_count() - num_actors, ', '.join([t.name for t in threading.enumerate()])) logger.debug('Stopping %d actor(s)...', num_actors) ActorRegistry.stop_all() num_actors = len(ActorRegistry.get_all()) logger.debug('All actors stopped.') class BaseThread(threading.Thread): def __init__(self): super(BaseThread, self).__init__() # No thread should block process from exiting self.daemon = True def run(self): logger.debug('%s: Starting thread', self.name) try: self.run_inside_try() except KeyboardInterrupt: logger.info('Interrupted by user') except ImportError as e: logger.error(e) except ActorDeadError as e: logger.warning(e) except Exception as e: logger.exception(e) logger.debug('%s: Exiting thread', self.name) def run_inside_try(self): raise NotImplementedError mopidy-0.17.0/mopidy/utils/versioning.py000066400000000000000000000010351224420023200202640ustar00rootroot00000000000000from __future__ import unicode_literals from subprocess import PIPE, Popen from mopidy import __version__ def get_version(): try: return get_git_version() except EnvironmentError: return __version__ def get_git_version(): process = Popen(['git', 'describe'], stdout=PIPE, stderr=PIPE) if process.wait() != 0: raise EnvironmentError('Execution of "git describe" failed') version = process.stdout.read().strip() if version.startswith('v'): version = version[1:] return version mopidy-0.17.0/mopidy/utils/zeroconf.py000066400000000000000000000052661224420023200177400ustar00rootroot00000000000000from __future__ import unicode_literals import logging import socket import string logger = logging.getLogger('mopidy.utils.zeroconf') try: import dbus except ImportError: dbus = None _AVAHI_IF_UNSPEC = -1 _AVAHI_PROTO_UNSPEC = -1 _AVAHI_PUBLISHFLAGS_NONE = 0 def _is_loopback_address(host): return host.startswith('127.') or host == '::1' def _convert_text_to_dbus_bytes(text): return [dbus.Byte(ord(c)) for c in text] class Zeroconf(object): """Publish a network service with Zeroconf using Avahi.""" def __init__(self, name, port, stype=None, domain=None, host=None, text=None): self.group = None self.stype = stype or '_http._tcp' self.domain = domain or '' self.port = port self.text = text or [] if host in ('::', '0.0.0.0'): self.host = '' else: self.host = host template = string.Template(name) self.name = template.safe_substitute( hostname=self.host or socket.getfqdn(), port=self.port) def publish(self): if _is_loopback_address(self.host): logger.info( 'Zeroconf publish on loopback interface is not supported.') return False if not dbus: logger.debug('Zeroconf publish failed: dbus not installed.') return False try: bus = dbus.SystemBus() if not bus.name_has_owner('org.freedesktop.Avahi'): logger.debug( 'Zeroconf publish failed: Avahi service not running.') return False server = dbus.Interface( bus.get_object('org.freedesktop.Avahi', '/'), 'org.freedesktop.Avahi.Server') self.group = dbus.Interface( bus.get_object( 'org.freedesktop.Avahi', server.EntryGroupNew()), 'org.freedesktop.Avahi.EntryGroup') text = [_convert_text_to_dbus_bytes(t) for t in self.text] self.group.AddService( _AVAHI_IF_UNSPEC, _AVAHI_PROTO_UNSPEC, dbus.UInt32(_AVAHI_PUBLISHFLAGS_NONE), self.name, self.stype, self.domain, self.host, dbus.UInt16(self.port), text) self.group.Commit() return True except dbus.exceptions.DBusException as e: logger.debug('Zeroconf publish failed: %s', e) return False def unpublish(self): if self.group: try: self.group.Reset() except dbus.exceptions.DBusException as e: logger.debug('Zeroconf unpublish failed: %s', e) finally: self.group = None mopidy-0.17.0/requirements/000077500000000000000000000000001224420023200156125ustar00rootroot00000000000000mopidy-0.17.0/requirements/README.rst000066400000000000000000000005411224420023200173010ustar00rootroot00000000000000********************* pip requirement files ********************* The files found here are `requirement files `_ that may be used with `pip `_. To install the dependencies found in one of these files, simply run e.g.:: pip install -r requirements/tests.txt mopidy-0.17.0/requirements/core.txt000066400000000000000000000001731224420023200173040ustar00rootroot00000000000000setuptools # Available as python-setuptools in Debian/Ubuntu Pykka >= 1.1 # Available as python-pykka from apt.mopidy.com mopidy-0.17.0/requirements/docs.txt000066400000000000000000000000311224420023200172750ustar00rootroot00000000000000Sphinx >= 1.0 pygraphviz mopidy-0.17.0/requirements/http.txt000066400000000000000000000003041224420023200173270ustar00rootroot00000000000000cherrypy >= 3.2.2 # Available as python-cherrypy3 in Debian/Ubuntu ws4py >= 0.2.3 # Available as python-ws4py in newer Debian/Ubuntu and from apt.mopidy.com for # older releases of Debian/Ubuntu mopidy-0.17.0/requirements/tests.txt000066400000000000000000000000411224420023200175100ustar00rootroot00000000000000coverage flake8 mock >= 1.0 nose mopidy-0.17.0/setup.py000066400000000000000000000034531224420023200146060ustar00rootroot00000000000000from __future__ import unicode_literals import re from setuptools import setup, find_packages def get_version(filename): init_py = open(filename).read() metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", init_py)) return metadata['version'] setup( name='Mopidy', version=get_version('mopidy/__init__.py'), url='http://www.mopidy.com/', license='Apache License, Version 2.0', author='Stein Magnus Jodal', author_email='stein.magnus@jodal.no', description='Music server with MPD and Spotify support', long_description=open('README.rst').read(), packages=find_packages(exclude=['tests', 'tests.*']), zip_safe=False, include_package_data=True, install_requires=[ 'setuptools', 'Pykka >= 1.1', ], extras_require={ 'http': ['cherrypy >= 3.2.2', 'ws4py >= 0.2.3'], }, test_suite='nose.collector', tests_require=[ 'nose', 'mock >= 1.0', ], entry_points={ 'console_scripts': [ 'mopidy = mopidy.__main__:main', 'mopidy-convert-config = mopidy.config.convert:main', ], 'mopidy.ext': [ 'http = mopidy.frontends.http:Extension [http]', 'local = mopidy.backends.local:Extension', 'mpd = mopidy.frontends.mpd:Extension', 'stream = mopidy.backends.stream:Extension', ], }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', 'Intended Audience :: End Users/Desktop', 'License :: OSI Approved :: Apache Software License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python :: 2.7', 'Topic :: Multimedia :: Sound/Audio :: Players', ], ) mopidy-0.17.0/tests/000077500000000000000000000000001224420023200142315ustar00rootroot00000000000000mopidy-0.17.0/tests/__init__.py000066400000000000000000000013271224420023200163450ustar00rootroot00000000000000from __future__ import unicode_literals import os def path_to_data_dir(name): if not isinstance(name, bytes): name = name.encode('utf-8') path = os.path.dirname(__file__) path = os.path.join(path, b'data') path = os.path.abspath(path) return os.path.join(path, name) class IsA(object): def __init__(self, klass): self.klass = klass def __eq__(self, rhs): try: return isinstance(rhs, self.klass) except TypeError: return type(rhs) == type(self.klass) def __ne__(self, rhs): return not self.__eq__(rhs) def __repr__(self): return str(self.klass) any_int = IsA(int) any_str = IsA(str) any_unicode = IsA(unicode) mopidy-0.17.0/tests/__main__.py000066400000000000000000000001021224420023200163140ustar00rootroot00000000000000from __future__ import unicode_literals import nose nose.main() mopidy-0.17.0/tests/audio/000077500000000000000000000000001224420023200153325ustar00rootroot00000000000000mopidy-0.17.0/tests/audio/__init__.py000066400000000000000000000000001224420023200174310ustar00rootroot00000000000000mopidy-0.17.0/tests/audio/actor_test.py000066400000000000000000000117471224420023200200650ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import pygst pygst.require('0.10') import gst import gobject gobject.threads_init() import pykka from mopidy import audio from mopidy.utils.path import path_to_uri from tests import path_to_data_dir class AudioTest(unittest.TestCase): def setUp(self): config = { 'audio': { 'mixer': 'fakemixer track_max_volume=65536', 'mixer_track': None, 'output': 'fakesink', 'visualizer': None, } } self.song_uri = path_to_uri(path_to_data_dir('song1.wav')) self.audio = audio.Audio.start(config=config).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() def prepare_uri(self, uri): self.audio.prepare_change() self.audio.set_uri(uri) def test_start_playback_existing_file(self): self.prepare_uri(self.song_uri) self.assertTrue(self.audio.start_playback().get()) def test_start_playback_non_existing_file(self): self.prepare_uri(self.song_uri + 'bogus') self.assertFalse(self.audio.start_playback().get()) def test_pause_playback_while_playing(self): self.prepare_uri(self.song_uri) self.audio.start_playback() self.assertTrue(self.audio.pause_playback().get()) def test_stop_playback_while_playing(self): self.prepare_uri(self.song_uri) self.audio.start_playback() self.assertTrue(self.audio.stop_playback().get()) @unittest.SkipTest def test_deliver_data(self): pass # TODO @unittest.SkipTest def test_end_of_data_stream(self): pass # TODO def test_set_volume(self): for value in range(0, 101): self.assertTrue(self.audio.set_volume(value).get()) self.assertEqual(value, self.audio.get_volume().get()) def test_set_volume_with_mixer_max_below_100(self): config = { 'audio': { 'mixer': 'fakemixer track_max_volume=40', 'mixer_track': None, 'output': 'fakesink', 'visualizer': None, } } self.audio = audio.Audio.start(config=config).proxy() for value in range(0, 101): self.assertTrue(self.audio.set_volume(value).get()) self.assertEqual(value, self.audio.get_volume().get()) def test_set_volume_with_mixer_min_equal_max(self): config = { 'audio': { 'mixer': 'fakemixer track_max_volume=0', 'mixer_track': None, 'output': 'fakesink', 'visualizer': None, } } self.audio = audio.Audio.start(config=config).proxy() self.assertEqual(0, self.audio.get_volume().get()) @unittest.SkipTest def test_set_mute(self): pass # TODO Probably needs a fakemixer with a mixer track @unittest.SkipTest def test_set_state_encapsulation(self): pass # TODO @unittest.SkipTest def test_set_position(self): pass # TODO @unittest.SkipTest def test_invalid_output_raises_error(self): pass # TODO class AudioStateTest(unittest.TestCase): def setUp(self): self.audio = audio.Audio(config=None) def test_state_starts_as_stopped(self): self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_does_not_change_when_in_gst_ready_state(self): self.audio._on_playbin_state_changed( gst.STATE_NULL, gst.STATE_READY, gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) def test_state_changes_from_stopped_to_playing_on_play(self): self.audio._on_playbin_state_changed( gst.STATE_NULL, gst.STATE_READY, gst.STATE_PLAYING) self.audio._on_playbin_state_changed( gst.STATE_READY, gst.STATE_PAUSED, gst.STATE_PLAYING) self.audio._on_playbin_state_changed( gst.STATE_PAUSED, gst.STATE_PLAYING, gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PLAYING, self.audio.state) def test_state_changes_from_playing_to_paused_on_pause(self): self.audio.state = audio.PlaybackState.PLAYING self.audio._on_playbin_state_changed( gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.PAUSED, self.audio.state) def test_state_changes_from_playing_to_stopped_on_stop(self): self.audio.state = audio.PlaybackState.PLAYING self.audio._on_playbin_state_changed( gst.STATE_PLAYING, gst.STATE_PAUSED, gst.STATE_NULL) self.audio._on_playbin_state_changed( gst.STATE_PAUSED, gst.STATE_READY, gst.STATE_NULL) # We never get the following call, so the logic must work without it #self.audio._on_playbin_state_changed( # gst.STATE_READY, gst.STATE_NULL, gst.STATE_VOID_PENDING) self.assertEqual(audio.PlaybackState.STOPPED, self.audio.state) mopidy-0.17.0/tests/audio/listener_test.py000066400000000000000000000013741224420023200205750ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy import audio class AudioListenerTest(unittest.TestCase): def setUp(self): self.listener = audio.AudioListener() def test_on_event_forwards_to_specific_handler(self): self.listener.state_changed = mock.Mock() self.listener.on_event( 'state_changed', old_state='stopped', new_state='playing') self.listener.state_changed.assert_called_with( old_state='stopped', new_state='playing') def test_listener_has_default_impl_for_reached_end_of_stream(self): self.listener.reached_end_of_stream() def test_listener_has_default_impl_for_state_changed(self): self.listener.state_changed(None, None) mopidy-0.17.0/tests/audio/playlists_test.py000066400000000000000000000056051224420023200207750ustar00rootroot00000000000000#encoding: utf-8 from __future__ import unicode_literals import io import unittest from mopidy.audio import playlists BAD = b'foobarbaz' M3U = b"""#EXTM3U #EXTINF:123, Sample artist - Sample title file:///tmp/foo #EXTINF:321,Example Artist - Example title file:///tmp/bar #EXTINF:213,Some Artist - Other title file:///tmp/baz """ PLS = b"""[Playlist] NumberOfEntries=3 File1=file:///tmp/foo Title1=Sample Title Length1=123 File2=file:///tmp/bar Title2=Example title Length2=321 File3=file:///tmp/baz Title3=Other title Length3=213 Version=2 """ ASX = b""" Example Sample Title Example title Other title """ XSPF = b""" Sample Title file:///tmp/foo Example title file:///tmp/bar Other title file:///tmp/baz """ class TypeFind(object): def __init__(self, data): self.data = data def peek(self, start, end): return self.data[start:end] class BasePlaylistTest(object): valid = None invalid = None detect = None parse = None def test_detect_valid_header(self): self.assertTrue(self.detect(TypeFind(self.valid))) def test_detect_invalid_header(self): self.assertFalse(self.detect(TypeFind(self.invalid))) def test_parse_valid_playlist(self): uris = list(self.parse(io.BytesIO(self.valid))) expected = [b'file:///tmp/foo', b'file:///tmp/bar', b'file:///tmp/baz'] self.assertEqual(uris, expected) def test_parse_invalid_playlist(self): uris = list(self.parse(io.BytesIO(self.invalid))) self.assertEqual(uris, []) class M3uPlaylistTest(BasePlaylistTest, unittest.TestCase): valid = M3U invalid = BAD detect = staticmethod(playlists.detect_m3u_header) parse = staticmethod(playlists.parse_m3u) class PlsPlaylistTest(BasePlaylistTest, unittest.TestCase): valid = PLS invalid = BAD detect = staticmethod(playlists.detect_pls_header) parse = staticmethod(playlists.parse_pls) class AsxPlsPlaylistTest(BasePlaylistTest, unittest.TestCase): valid = ASX invalid = BAD detect = staticmethod(playlists.detect_asx_header) parse = staticmethod(playlists.parse_asx) class XspfPlaylistTest(BasePlaylistTest, unittest.TestCase): valid = XSPF invalid = BAD detect = staticmethod(playlists.detect_xspf_header) parse = staticmethod(playlists.parse_xspf) mopidy-0.17.0/tests/audio/scan_test.py000066400000000000000000000224451224420023200176760ustar00rootroot00000000000000from __future__ import unicode_literals import unittest from mopidy import exceptions from mopidy.audio import scan from mopidy.models import Track, Artist, Album from mopidy.utils import path as path_lib from tests import path_to_data_dir class FakeGstDate(object): def __init__(self, year, month, day): self.year = year self.month = month self.day = day class TranslatorTest(unittest.TestCase): def setUp(self): self.data = { 'uri': 'uri', 'album': 'albumname', 'track-number': 1, 'artist': 'name', 'composer': 'composer', 'performer': 'performer', 'album-artist': 'albumartistname', 'title': 'trackname', 'track-count': 2, 'album-disc-number': 2, 'album-disc-count': 3, 'date': FakeGstDate(2006, 1, 1,), 'container-format': 'ID3 tag', 'genre': 'genre', 'duration': 4531000000, 'comment': 'comment', 'musicbrainz-trackid': 'mbtrackid', 'musicbrainz-albumid': 'mbalbumid', 'musicbrainz-artistid': 'mbartistid', 'musicbrainz-albumartistid': 'mbalbumartistid', 'mtime': 1234, } self.album = { 'name': 'albumname', 'num_tracks': 2, 'num_discs': 3, 'musicbrainz_id': 'mbalbumid', } self.artist_single = { 'name': 'name', 'musicbrainz_id': 'mbartistid', } self.artist_multiple = { 'name': ['name1', 'name2'], 'musicbrainz_id': 'mbartistid', } self.artist = self.artist_single self.composer_single = { 'name': 'composer', } self.composer_multiple = { 'name': ['composer1', 'composer2'], } self.composer = self.composer_single self.performer_single = { 'name': 'performer', } self.performer_multiple = { 'name': ['performer1', 'performer2'], } self.performer = self.performer_single self.albumartist = { 'name': 'albumartistname', 'musicbrainz_id': 'mbalbumartistid', } self.track = { 'uri': 'uri', 'name': 'trackname', 'date': '2006-01-01', 'genre': 'genre', 'track_no': 1, 'disc_no': 2, 'comment': 'comment', 'length': 4531, 'musicbrainz_id': 'mbtrackid', 'last_modified': 1234, } def build_track(self): if self.albumartist: self.album['artists'] = [Artist(**self.albumartist)] self.track['album'] = Album(**self.album) if ('name' in self.artist and not isinstance(self.artist['name'], basestring)): self.track['artists'] = [Artist(name=artist) for artist in self.artist['name']] else: self.track['artists'] = [Artist(**self.artist)] if ('name' in self.composer and not isinstance(self.composer['name'], basestring)): self.track['composers'] = [Artist(name=artist) for artist in self.composer['name']] else: self.track['composers'] = [Artist(**self.composer)] \ if self.composer else '' if ('name' in self.performer and not isinstance(self.performer['name'], basestring)): self.track['performers'] = [Artist(name=artist) for artist in self.performer['name']] else: self.track['performers'] = [Artist(**self.performer)] \ if self.performer else '' return Track(**self.track) def check(self): expected = self.build_track() actual = scan.audio_data_to_track(self.data) self.assertEqual(expected, actual) def test_basic_data(self): self.check() def test_missing_track_number(self): del self.data['track-number'] del self.track['track_no'] self.check() def test_missing_track_count(self): del self.data['track-count'] del self.album['num_tracks'] self.check() def test_missing_track_name(self): del self.data['title'] del self.track['name'] self.check() def test_missing_track_musicbrainz_id(self): del self.data['musicbrainz-trackid'] del self.track['musicbrainz_id'] self.check() def test_missing_album_name(self): del self.data['album'] del self.album['name'] self.check() def test_missing_album_musicbrainz_id(self): del self.data['musicbrainz-albumid'] del self.album['musicbrainz_id'] self.check() def test_missing_artist_name(self): del self.data['artist'] del self.artist['name'] self.check() def test_missing_composer_name(self): del self.data['composer'] del self.composer['name'] self.check() def test_multiple_track_composers(self): self.data['composer'] = ['composer1', 'composer2'] self.composer = self.composer_multiple self.check() def test_multiple_track_performers(self): self.data['performer'] = ['performer1', 'performer2'] self.performer = self.performer_multiple self.check() def test_missing_performer_name(self): del self.data['performer'] del self.performer['name'] self.check() def test_missing_artist_musicbrainz_id(self): del self.data['musicbrainz-artistid'] del self.artist['musicbrainz_id'] self.check() def test_multiple_track_artists(self): self.data['artist'] = ['name1', 'name2'] self.data['musicbrainz-artistid'] = 'mbartistid' self.artist = self.artist_multiple self.check() def test_missing_album_artist(self): del self.data['album-artist'] del self.albumartist['name'] self.check() def test_missing_album_artist_musicbrainz_id(self): del self.data['musicbrainz-albumartistid'] del self.albumartist['musicbrainz_id'] self.check() def test_missing_genre(self): del self.data['genre'] del self.track['genre'] self.check() def test_missing_date(self): del self.data['date'] del self.track['date'] self.check() def test_invalid_date(self): self.data['date'] = FakeGstDate(65535, 1, 1) del self.track['date'] self.check() def test_missing_comment(self): del self.data['comment'] del self.track['comment'] self.check() class ScannerTest(unittest.TestCase): def setUp(self): self.errors = {} self.data = {} def scan(self, path): paths = path_lib.find_files(path_to_data_dir(path)) uris = (path_lib.path_to_uri(p) for p in paths) scanner = scan.Scanner() for uri in uris: key = uri[len('file://'):] try: self.data[key] = scanner.scan(uri) except exceptions.ScannerError as error: self.errors[key] = error def check(self, name, key, value): name = path_to_data_dir(name) self.assertEqual(self.data[name][key], value) def test_data_is_set(self): self.scan('scanner/simple') self.assert_(self.data) def test_errors_is_not_set(self): self.scan('scanner/simple') self.assert_(not self.errors) def test_uri_is_set(self): self.scan('scanner/simple') self.check( 'scanner/simple/song1.mp3', 'uri', 'file://%s' % path_to_data_dir('scanner/simple/song1.mp3')) self.check( 'scanner/simple/song1.ogg', 'uri', 'file://%s' % path_to_data_dir('scanner/simple/song1.ogg')) def test_duration_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'duration', 4680000000) self.check('scanner/simple/song1.ogg', 'duration', 4680000000) def test_artist_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'artist', 'name') self.check('scanner/simple/song1.ogg', 'artist', 'name') def test_album_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'album', 'albumname') self.check('scanner/simple/song1.ogg', 'album', 'albumname') def test_track_is_set(self): self.scan('scanner/simple') self.check('scanner/simple/song1.mp3', 'title', 'trackname') self.check('scanner/simple/song1.ogg', 'title', 'trackname') def test_nonexistant_dir_does_not_fail(self): self.scan('scanner/does-not-exist') self.assert_(not self.errors) def test_other_media_is_ignored(self): self.scan('scanner/image') self.assert_(self.errors) def test_log_file_that_gst_thinks_is_mpeg_1_is_ignored(self): self.scan('scanner/example.log') self.assert_(self.errors) def test_empty_wav_file_is_ignored(self): self.scan('scanner/empty.wav') self.assert_(self.errors) @unittest.SkipTest def test_song_without_time_is_handeled(self): pass mopidy-0.17.0/tests/backends/000077500000000000000000000000001224420023200160035ustar00rootroot00000000000000mopidy-0.17.0/tests/backends/__init__.py000066400000000000000000000000501224420023200201070ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/tests/backends/listener_test.py000066400000000000000000000010701224420023200212370ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.backends.listener import BackendListener class BackendListenerTest(unittest.TestCase): def setUp(self): self.listener = BackendListener() def test_on_event_forwards_to_specific_handler(self): self.listener.playlists_loaded = mock.Mock() self.listener.on_event('playlists_loaded') self.listener.playlists_loaded.assert_called_with() def test_listener_has_default_impl_for_playlists_loaded(self): self.listener.playlists_loaded() mopidy-0.17.0/tests/backends/local/000077500000000000000000000000001224420023200170755ustar00rootroot00000000000000mopidy-0.17.0/tests/backends/local/__init__.py000066400000000000000000000005231224420023200212060ustar00rootroot00000000000000from __future__ import unicode_literals def generate_song(i): return 'local:track:song%s.wav' % i def populate_tracklist(func): def wrapper(self): self.tl_tracks = self.core.tracklist.add(self.tracks) return func(self) wrapper.__name__ = func.__name__ wrapper.__doc__ = func.__doc__ return wrapper mopidy-0.17.0/tests/backends/local/events_test.py000066400000000000000000000020411224420023200220070ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import mock import pykka from mopidy import core, audio from mopidy.backends import listener from mopidy.backends.local import actor from tests import path_to_data_dir @mock.patch.object(listener.BackendListener, 'send') class LocalBackendEventsTest(unittest.TestCase): config = { 'local': { 'media_dir': path_to_data_dir(''), 'playlists_dir': b'', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } def setUp(self): self.audio = audio.DummyAudio.start().proxy() self.backend = actor.LocalBackend.start( config=self.config, audio=self.audio).proxy() self.core = core.Core.start(backends=[self.backend]).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() def test_playlists_refresh_sends_playlists_loaded_event(self, send): send.reset_mock() self.core.playlists.refresh().get() self.assertEqual(send.call_args[0][0], 'playlists_loaded') mopidy-0.17.0/tests/backends/local/library_test.py000066400000000000000000000502211224420023200221520ustar00rootroot00000000000000from __future__ import unicode_literals import tempfile import unittest import pykka from mopidy import core from mopidy.backends.local import actor from mopidy.models import Track, Album, Artist from tests import path_to_data_dir # TODO: update tests to only use backend, not core. we need a seperate # core test that does this integration test. class LocalLibraryProviderTest(unittest.TestCase): artists = [ Artist(name='artist1'), Artist(name='artist2'), Artist(name='artist3'), Artist(name='artist4'), Artist(name='artist5'), Artist(name='artist6'), ] albums = [ Album(name='album1', artists=[artists[0]]), Album(name='album2', artists=[artists[1]]), Album(name='album3', artists=[artists[2]]), Album(name='album4'), ] tracks = [ Track( uri='local:track:path1', name='track1', artists=[artists[0]], album=albums[0], date='2001-02-03', length=4000, track_no=1), Track( uri='local:track:path2', name='track2', artists=[artists[1]], album=albums[1], date='2002', length=4000, track_no=2), Track( uri='local:track:path3', name='track3', artists=[artists[3]], album=albums[2], date='2003', length=4000, track_no=3), Track( uri='local:track:path4', name='track4', artists=[artists[2]], album=albums[3], date='2004', length=60000, track_no=4, comment='This is a fantastic track'), Track( uri='local:track:path5', name='track5', genre='genre1', album=albums[3], length=4000, composers=[artists[4]]), Track( uri='local:track:path6', name='track6', genre='genre2', album=albums[3], length=4000, performers=[artists[5]]), ] config = { 'local': { 'media_dir': path_to_data_dir(''), 'playlists_dir': b'', 'tag_cache_file': path_to_data_dir('library_tag_cache'), } } def setUp(self): self.backend = actor.LocalBackend.start( config=self.config, audio=None).proxy() self.core = core.Core(backends=[self.backend]) self.library = self.core.library def tearDown(self): pykka.ActorRegistry.stop_all() def test_refresh(self): self.library.refresh() @unittest.SkipTest def test_refresh_uri(self): pass def test_refresh_missing_uri(self): # Verifies that https://github.com/mopidy/mopidy/issues/500 # has been fixed. tag_cache = tempfile.NamedTemporaryFile() with open(self.config['local']['tag_cache_file']) as fh: tag_cache.write(fh.read()) tag_cache.flush() config = {'local': self.config['local'].copy()} config['local']['tag_cache_file'] = tag_cache.name backend = actor.LocalBackend(config=config, audio=None) # Sanity check that value is in tag cache result = backend.library.lookup(self.tracks[0].uri) self.assertEqual(result, self.tracks[0:1]) # Clear tag cache and refresh tag_cache.seek(0) tag_cache.truncate() backend.library.refresh() # Now it should be gone. result = backend.library.lookup(self.tracks[0].uri) self.assertEqual(result, []) def test_lookup(self): tracks = self.library.lookup(self.tracks[0].uri) self.assertEqual(tracks, self.tracks[0:1]) def test_lookup_unknown_track(self): tracks = self.library.lookup('fake uri') self.assertEqual(tracks, []) def test_find_exact_no_hits(self): result = self.library.find_exact(track_name=['unknown track']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(artist=['unknown artist']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(albumartist=['unknown albumartist']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(composer=['unknown composer']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(performer=['unknown performer']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(album=['unknown album']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(date=['1990']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(genre=['unknown genre']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(track_no=['9']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(track_no=['no_match']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(comment=['fake comment']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(uri=['fake uri']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(any=['unknown any']) self.assertEqual(list(result[0].tracks), []) def test_find_exact_uri(self): track_1_uri = 'local:track:path1' result = self.library.find_exact(uri=track_1_uri) self.assertEqual(list(result[0].tracks), self.tracks[:1]) track_2_uri = 'local:track:path2' result = self.library.find_exact(uri=track_2_uri) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_track_name(self): result = self.library.find_exact(track_name=['track1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(track_name=['track2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_artist(self): result = self.library.find_exact(artist=['artist1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(artist=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) result = self.library.find_exact(artist=['artist3']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) def test_find_exact_composer(self): result = self.library.find_exact(composer=['artist5']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.find_exact(composer=['artist6']) self.assertEqual(list(result[0].tracks), []) def test_find_exact_performer(self): result = self.library.find_exact(performer=['artist6']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) result = self.library.find_exact(performer=['artist5']) self.assertEqual(list(result[0].tracks), []) def test_find_exact_album(self): result = self.library.find_exact(album=['album1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(album=['album2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_albumartist(self): # Artist is both track artist and album artist result = self.library.find_exact(albumartist=['artist1']) self.assertEqual(list(result[0].tracks), [self.tracks[0]]) # Artist is both track and album artist result = self.library.find_exact(albumartist=['artist2']) self.assertEqual(list(result[0].tracks), [self.tracks[1]]) # Artist is just album artist result = self.library.find_exact(albumartist=['artist3']) self.assertEqual(list(result[0].tracks), [self.tracks[2]]) def test_find_exact_track_no(self): result = self.library.find_exact(track_no=['1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(track_no=['2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_genre(self): result = self.library.find_exact(genre=['genre1']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.find_exact(genre=['genre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_find_exact_date(self): result = self.library.find_exact(date=['2001']) self.assertEqual(list(result[0].tracks), []) result = self.library.find_exact(date=['2001-02-03']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(date=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_find_exact_comment(self): result = self.library.find_exact( comment=['This is a fantastic track']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) result = self.library.find_exact( comment=['This is a fantastic']) self.assertEqual(list(result[0].tracks), []) def test_find_exact_any(self): # Matches on track artist result = self.library.find_exact(any=['artist1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(any=['artist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) # Matches on track name result = self.library.find_exact(any=['track1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.find_exact(any=['track2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) # Matches on track album result = self.library.find_exact(any=['album1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) # Matches on track album artists result = self.library.find_exact(any=['artist3']) self.assertEqual( list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on track composer result = self.library.find_exact(any=['artist5']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) # Matches on track performer result = self.library.find_exact(any=['artist6']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track genre result = self.library.find_exact(any=['genre1']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.find_exact(any=['genre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track date result = self.library.find_exact(any=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) # Matches on track comment result = self.library.find_exact( any=['This is a fantastic track']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) # Matches on URI result = self.library.find_exact(any=['local:track:path1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) def test_find_exact_wrong_type(self): test = lambda: self.library.find_exact(wrong=['test']) self.assertRaises(LookupError, test) def test_find_exact_with_empty_query(self): test = lambda: self.library.find_exact(artist=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(albumartist=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(track_name=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(composer=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(performer=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(album=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(track_no=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(genre=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(date=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(comment=['']) self.assertRaises(LookupError, test) test = lambda: self.library.find_exact(any=['']) self.assertRaises(LookupError, test) def test_search_no_hits(self): result = self.library.search(track_name=['unknown track']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(artist=['unknown artist']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(albumartist=['unknown albumartist']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(composer=['unknown composer']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(performer=['unknown performer']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(album=['unknown album']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(track_no=['9']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(track_no=['no_match']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(genre=['unknown genre']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(date=['unknown date']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(comment=['unknown comment']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(uri=['unknown uri']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(any=['unknown anything']) self.assertEqual(list(result[0].tracks), []) def test_search_uri(self): result = self.library.search(uri=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(uri=['TH2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_track_name(self): result = self.library.search(track_name=['Rack1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(track_name=['Rack2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_artist(self): result = self.library.search(artist=['Tist1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(artist=['Tist2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_albumartist(self): # Artist is both track artist and album artist result = self.library.search(albumartist=['Tist1']) self.assertEqual(list(result[0].tracks), [self.tracks[0]]) # Artist is both track artist and album artist result = self.library.search(albumartist=['Tist2']) self.assertEqual(list(result[0].tracks), [self.tracks[1]]) # Artist is just album artist result = self.library.search(albumartist=['Tist3']) self.assertEqual(list(result[0].tracks), [self.tracks[2]]) def test_search_composer(self): result = self.library.search(composer=['Tist5']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) def test_search_performer(self): result = self.library.search(performer=['Tist6']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_search_album(self): result = self.library.search(album=['Bum1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(album=['Bum2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_genre(self): result = self.library.search(genre=['Enre1']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.search(genre=['Enre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) def test_search_date(self): result = self.library.search(date=['2001']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(date=['2001-02-03']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(date=['2001-02-04']) self.assertEqual(list(result[0].tracks), []) result = self.library.search(date=['2002']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_track_no(self): result = self.library.search(track_no=['1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(track_no=['2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) def test_search_comment(self): result = self.library.search(comment=['fantastic']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) result = self.library.search(comment=['antasti']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) def test_search_any(self): # Matches on track artist result = self.library.search(any=['Tist1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) # Matches on track composer result = self.library.search(any=['Tist5']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) # Matches on track performer result = self.library.search(any=['Tist6']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track result = self.library.search(any=['Rack1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) result = self.library.search(any=['Rack2']) self.assertEqual(list(result[0].tracks), self.tracks[1:2]) # Matches on track album result = self.library.search(any=['Bum1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) # Matches on track album artists result = self.library.search(any=['Tist3']) self.assertEqual( list(result[0].tracks), [self.tracks[3], self.tracks[2]]) # Matches on track genre result = self.library.search(any=['Enre1']) self.assertEqual(list(result[0].tracks), self.tracks[4:5]) result = self.library.search(any=['Enre2']) self.assertEqual(list(result[0].tracks), self.tracks[5:6]) # Matches on track comment result = self.library.search(any=['fanta']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) result = self.library.search(any=['is a fan']) self.assertEqual(list(result[0].tracks), self.tracks[3:4]) # Matches on URI result = self.library.search(any=['TH1']) self.assertEqual(list(result[0].tracks), self.tracks[:1]) def test_search_wrong_type(self): test = lambda: self.library.search(wrong=['test']) self.assertRaises(LookupError, test) def test_search_with_empty_query(self): test = lambda: self.library.search(artist=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(albumartist=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(composer=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(performer=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(track_name=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(album=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(genre=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(date=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(comment=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(uri=['']) self.assertRaises(LookupError, test) test = lambda: self.library.search(any=['']) self.assertRaises(LookupError, test) mopidy-0.17.0/tests/backends/local/playback_test.py000066400000000000000000001160611224420023200223010ustar00rootroot00000000000000from __future__ import unicode_literals import mock import time import unittest import pykka from mopidy import audio, core from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Track from tests import path_to_data_dir from tests.backends.local import generate_song, populate_tracklist # TODO Test 'playlist repeat', e.g. repeat=1,single=0 class LocalPlaybackProviderTest(unittest.TestCase): config = { 'local': { 'media_dir': path_to_data_dir(''), 'playlists_dir': b'', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } # We need four tracks so that our shuffled track tests behave nicely with # reversed as a fake shuffle. Ensuring that shuffled order is [4,3,2,1] and # normal order [1,2,3,4] which means next_track != next_track_with_random tracks = [ Track(uri=generate_song(i), length=4464) for i in (1, 2, 3, 4)] def add_track(self, uri): track = Track(uri=uri, length=4464) self.tracklist.add([track]) def setUp(self): self.audio = audio.DummyAudio.start().proxy() self.backend = actor.LocalBackend.start( config=self.config, audio=self.audio).proxy() self.core = core.Core(backends=[self.backend]) self.playback = self.core.playback self.tracklist = self.core.tracklist assert len(self.tracks) >= 3, \ 'Need at least three tracks to run tests.' assert self.tracks[0].length >= 2000, \ 'First song needs to be at least 2000 miliseconds' def tearDown(self): pykka.ActorRegistry.stop_all() def test_uri_scheme(self): self.assertNotIn('file', self.core.uri_schemes) self.assertIn('local', self.core.uri_schemes) def test_play_mp3(self): self.add_track('local:track:blank.mp3') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_ogg(self): self.add_track('local:track:blank.ogg') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_flac(self): self.add_track('local:track:blank.flac') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_play_uri_with_non_ascii_bytes(self): # Regression test: If trying to do .split(u':') on a bytestring, the # string will be decoded from ASCII to Unicode, which will crash on # non-ASCII strings, like the bytestring the following URI decodes to. self.add_track('local:track:12%20Doin%E2%80%99%20It%20Right.flac') self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) def test_initial_state_is_stopped(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_play_with_empty_playlist(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_play_with_empty_playlist_return_value(self): self.assertEqual(self.playback.play(), None) @populate_tracklist def test_play_state(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_play_return_value(self): self.assertEqual(self.playback.play(), None) @populate_tracklist def test_play_track_state(self): self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play(self.tracklist.tl_tracks[-1]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_play_track_return_value(self): self.assertEqual(self.playback.play( self.tracklist.tl_tracks[-1]), None) @populate_tracklist def test_play_when_playing(self): self.playback.play() track = self.playback.current_track self.playback.play() self.assertEqual(track, self.playback.current_track) @populate_tracklist def test_play_when_paused(self): self.playback.play() track = self.playback.current_track self.playback.pause() self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) @populate_tracklist def test_play_when_pause_after_next(self): self.playback.play() self.playback.next() self.playback.next() track = self.playback.current_track self.playback.pause() self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(track, self.playback.current_track) @populate_tracklist def test_play_sets_current_track(self): self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_play_track_sets_current_track(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.assertEqual(self.playback.current_track, self.tracks[-1]) @populate_tracklist def test_play_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[0] self.playback.play() self.assertNotEqual(self.playback.current_track, self.tracks[0]) self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist def test_current_track_after_completed_playlist(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_tracklist def test_previous(self): self.playback.play() self.playback.next() self.playback.previous() self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_previous_more(self): self.playback.play() # At track 0 self.playback.next() # At track 1 self.playback.next() # At track 2 self.playback.previous() # At track 1 self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist def test_previous_return_value(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.previous(), None) @populate_tracklist def test_previous_does_not_trigger_playback(self): self.playback.play() self.playback.next() self.playback.stop() self.playback.previous() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_previous_at_start_of_playlist(self): self.playback.previous() self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) def test_previous_for_empty_playlist(self): self.playback.previous() self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_tracklist def test_previous_skips_to_previous_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] self.playback.play(self.tracklist.tl_tracks[2]) self.assertEqual(self.playback.current_track, self.tracks[2]) self.playback.previous() self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_next(self): self.playback.play() tl_track = self.playback.current_tl_track old_position = self.tracklist.index(tl_track) old_uri = tl_track.track.uri self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.index(tl_track), old_position + 1) self.assertNotEqual(self.playback.current_track.uri, old_uri) @populate_tracklist def test_next_return_value(self): self.playback.play() self.assertEqual(self.playback.next(), None) @populate_tracklist def test_next_does_not_trigger_playback(self): self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_next_at_end_of_playlist(self): self.playback.play() for i, track in enumerate(self.tracks): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), i) self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_next_until_end_of_playlist_and_play_from_start(self): self.playback.play() for _ in self.tracks: self.playback.next() self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, self.tracks[0]) def test_next_for_empty_playlist(self): self.playback.next() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_next_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.next() self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[2]) @populate_tracklist def test_next_track_before_play(self): tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[0]) @populate_tracklist def test_next_track_during_play(self): self.playback.play() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[1]) @populate_tracklist def test_next_track_after_previous(self): self.playback.play() self.playback.next() self.playback.previous() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[1]) def test_next_track_empty_playlist(self): tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_next_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_next_track_at_end_of_playlist_with_repeat(self): self.tracklist.repeat = True self.playback.play() for _ in self.tracks[1:]: self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[0]) @populate_tracklist @mock.patch('random.shuffle') def test_next_track_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True current_tl_track = self.playback.current_tl_track next_tl_track = self.tracklist.next_track(current_tl_track) self.assertEqual(next_tl_track, self.tl_tracks[-1]) @populate_tracklist def test_next_with_consume(self): self.tracklist.consume = True self.playback.play() self.playback.next() self.assertIn(self.tracks[0], self.tracklist.tracks) @populate_tracklist def test_next_with_single_and_repeat(self): self.tracklist.single = True self.tracklist.repeat = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist @mock.patch('random.shuffle') def test_next_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[-1]) self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[-2]) @populate_tracklist @mock.patch('random.shuffle') def test_next_track_with_random_after_append_playlist(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True current_tl_track = self.playback.current_tl_track expected_tl_track = self.tracklist.tl_tracks[-1] next_tl_track = self.tracklist.next_track(current_tl_track) # Baseline checking that first next_track is last tl track per our fake # shuffle. self.assertEqual(next_tl_track, expected_tl_track) self.tracklist.add(self.tracks[:1]) old_next_tl_track = next_tl_track expected_tl_track = self.tracklist.tl_tracks[-1] next_tl_track = self.tracklist.next_track(current_tl_track) # Verify that first next track has changed since we added to the # playlist. self.assertEqual(next_tl_track, expected_tl_track) self.assertNotEqual(next_tl_track, old_next_tl_track) @populate_tracklist def test_end_of_track(self): self.playback.play() tl_track = self.playback.current_tl_track old_position = self.tracklist.index(tl_track) old_uri = tl_track.track.uri self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.index(tl_track), old_position + 1) self.assertNotEqual(self.playback.current_track.uri, old_uri) @populate_tracklist def test_end_of_track_return_value(self): self.playback.play() self.assertEqual(self.playback.on_end_of_track(), None) @populate_tracklist def test_end_of_track_does_not_trigger_playback(self): self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_end_of_track_at_end_of_playlist(self): self.playback.play() for i, track in enumerate(self.tracks): self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), i) self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_end_of_track_until_end_of_playlist_and_play_from_start(self): self.playback.play() for _ in self.tracks: self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, self.tracks[0]) def test_end_of_track_for_empty_playlist(self): self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_end_of_track_skips_to_next_track_on_failure(self): # If backend's play() returns False, it is a failure. self.backend.playback.play = lambda track: track != self.tracks[1] self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.on_end_of_track() self.assertNotEqual(self.playback.current_track, self.tracks[1]) self.assertEqual(self.playback.current_track, self.tracks[2]) @populate_tracklist def test_end_of_track_track_before_play(self): tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[0]) @populate_tracklist def test_end_of_track_track_during_play(self): self.playback.play() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[1]) @populate_tracklist def test_end_of_track_track_after_previous(self): self.playback.play() self.playback.on_end_of_track() self.playback.previous() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[1]) def test_end_of_track_track_empty_playlist(self): tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_end_of_track_track_at_end_of_playlist(self): self.playback.play() for _ in self.tracklist.tl_tracks[1:]: self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_end_of_track_track_at_end_of_playlist_with_repeat(self): self.tracklist.repeat = True self.playback.play() for _ in self.tracks[1:]: self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[0]) @populate_tracklist @mock.patch('random.shuffle') def test_end_of_track_track_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.next_track(tl_track), self.tl_tracks[-1]) @populate_tracklist def test_end_of_track_with_consume(self): self.tracklist.consume = True self.playback.play() self.playback.on_end_of_track() self.assertNotIn(self.tracks[0], self.tracklist.tracks) @populate_tracklist @mock.patch('random.shuffle') def test_end_of_track_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[-1]) self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[-2]) @populate_tracklist @mock.patch('random.shuffle') def test_end_of_track_track_with_random_after_append_playlist( self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True current_tl_track = self.playback.current_tl_track expected_tl_track = self.tracklist.tl_tracks[-1] eot_tl_track = self.tracklist.eot_track(current_tl_track) # Baseline checking that first eot_track is last tl track per our fake # shuffle. self.assertEqual(eot_tl_track, expected_tl_track) self.tracklist.add(self.tracks[:1]) old_eot_tl_track = eot_tl_track expected_tl_track = self.tracklist.tl_tracks[-1] eot_tl_track = self.tracklist.eot_track(current_tl_track) # Verify that first next track has changed since we added to the # playlist. self.assertEqual(eot_tl_track, expected_tl_track) self.assertNotEqual(eot_tl_track, old_eot_tl_track) @populate_tracklist def test_previous_track_before_play(self): tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.previous_track(tl_track), None) @populate_tracklist def test_previous_track_after_play(self): self.playback.play() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.previous_track(tl_track), None) @populate_tracklist def test_previous_track_after_next(self): self.playback.play() self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.previous_track(tl_track), self.tl_tracks[0]) @populate_tracklist def test_previous_track_after_previous(self): self.playback.play() # At track 0 self.playback.next() # At track 1 self.playback.next() # At track 2 self.playback.previous() # At track 1 tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.previous_track(tl_track), self.tl_tracks[0]) def test_previous_track_empty_playlist(self): tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.previous_track(tl_track), None) @populate_tracklist def test_previous_track_with_consume(self): self.tracklist.consume = True for _ in self.tracks: self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.previous_track(tl_track), self.playback.current_tl_track) @populate_tracklist def test_previous_track_with_random(self): self.tracklist.random = True for _ in self.tracks: self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual( self.tracklist.previous_track(tl_track), self.playback.current_tl_track) @populate_tracklist def test_initial_current_track(self): self.assertEqual(self.playback.current_track, None) @populate_tracklist def test_current_track_during_play(self): self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_current_track_after_next(self): self.playback.play() self.playback.next() self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist def test_initial_tracklist_position(self): tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), None) @populate_tracklist def test_tracklist_position_during_play(self): self.playback.play() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), 0) @populate_tracklist def test_tracklist_position_after_next(self): self.playback.play() self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), 1) @populate_tracklist def test_tracklist_position_at_end_of_playlist(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.index(tl_track), None) def test_on_tracklist_change_gets_called(self): callback = self.playback.on_tracklist_change def wrapper(): wrapper.called = True return callback() wrapper.called = False self.playback.on_tracklist_change = wrapper self.tracklist.add([Track()]) self.assert_(wrapper.called) @unittest.SkipTest # Blocks for 10ms @populate_tracklist def test_end_of_track_callback_gets_called(self): self.playback.play() result = self.playback.seek(self.tracks[0].length - 10) self.assertTrue(result, 'Seek failed') message = self.core_queue.get(True, 1) self.assertEqual('end_of_track', message['command']) @populate_tracklist def test_on_tracklist_change_when_playing(self): self.playback.play() current_track = self.playback.current_track self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, current_track) @populate_tracklist def test_on_tracklist_change_when_stopped(self): self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_tracklist def test_on_tracklist_change_when_paused(self): self.playback.play() self.playback.pause() current_track = self.playback.current_track self.tracklist.add([self.tracks[2]]) self.assertEqual(self.playback.state, PlaybackState.PAUSED) self.assertEqual(self.playback.current_track, current_track) @populate_tracklist def test_pause_when_stopped(self): self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_tracklist def test_pause_when_playing(self): self.playback.play() self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_tracklist def test_pause_when_paused(self): self.playback.play() self.playback.pause() self.playback.pause() self.assertEqual(self.playback.state, PlaybackState.PAUSED) @populate_tracklist def test_pause_return_value(self): self.playback.play() self.assertEqual(self.playback.pause(), None) @populate_tracklist def test_resume_when_stopped(self): self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_resume_when_playing(self): self.playback.play() self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_resume_when_paused(self): self.playback.play() self.playback.pause() self.playback.resume() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_resume_return_value(self): self.playback.play() self.playback.pause() self.assertEqual(self.playback.resume(), None) @unittest.SkipTest # Uses sleep and might not work with LocalBackend @populate_tracklist def test_resume_continues_from_right_position(self): self.playback.play() time.sleep(0.2) self.playback.pause() self.playback.resume() self.assertNotEqual(self.playback.time_position, 0) @populate_tracklist def test_seek_when_stopped(self): result = self.playback.seek(1000) self.assert_(result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_when_stopped_updates_position(self): self.playback.seek(1000) position = self.playback.time_position self.assertGreaterEqual(position, 990) def test_seek_on_empty_playlist(self): self.assertFalse(self.playback.seek(0)) def test_seek_on_empty_playlist_updates_position(self): self.playback.seek(0) self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_seek_when_stopped_triggers_play(self): self.playback.seek(0) self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_seek_when_playing(self): self.playback.play() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_when_playing_updates_position(self): length = self.tracklist.tracks[0].length self.playback.play() self.playback.seek(length - 1000) position = self.playback.time_position self.assertGreaterEqual(position, length - 1010) @populate_tracklist def test_seek_when_paused(self): self.playback.play() self.playback.pause() result = self.playback.seek(self.tracks[0].length - 1000) self.assert_(result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_when_paused_updates_position(self): length = self.tracklist.tracks[0].length self.playback.play() self.playback.pause() self.playback.seek(length - 1000) position = self.playback.time_position self.assertGreaterEqual(position, length - 1010) @populate_tracklist def test_seek_when_paused_triggers_play(self): self.playback.play() self.playback.pause() self.playback.seek(0) self.assertEqual(self.playback.state, PlaybackState.PLAYING) @unittest.SkipTest @populate_tracklist def test_seek_beyond_end_of_song(self): # FIXME need to decide return value self.playback.play() result = self.playback.seek(self.tracks[0].length * 100) self.assert_(not result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_beyond_end_of_song_jumps_to_next_song(self): self.playback.play() self.playback.seek(self.tracks[0].length * 100) self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist def test_seek_beyond_end_of_song_for_last_track(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.seek(self.tracklist.tracks[-1].length * 100) self.assertEqual(self.playback.state, PlaybackState.STOPPED) @unittest.SkipTest @populate_tracklist def test_seek_beyond_start_of_song(self): # FIXME need to decide return value self.playback.play() result = self.playback.seek(-1000) self.assert_(not result, 'Seek return value was %s' % result) @populate_tracklist def test_seek_beyond_start_of_song_update_postion(self): self.playback.play() self.playback.seek(-1000) position = self.playback.time_position self.assertGreaterEqual(position, 0) self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_stop_when_stopped(self): self.playback.stop() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_stop_when_playing(self): self.playback.play() self.playback.stop() self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_stop_when_paused(self): self.playback.play() self.playback.pause() self.playback.stop() self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_stop_return_value(self): self.playback.play() self.assertEqual(self.playback.stop(), None) def test_time_position_when_stopped(self): future = mock.Mock() future.get = mock.Mock(return_value=0) self.audio.get_position = mock.Mock(return_value=future) self.assertEqual(self.playback.time_position, 0) @populate_tracklist def test_time_position_when_stopped_with_playlist(self): future = mock.Mock() future.get = mock.Mock(return_value=0) self.audio.get_position = mock.Mock(return_value=future) self.assertEqual(self.playback.time_position, 0) @unittest.SkipTest # Uses sleep and does might not work with LocalBackend @populate_tracklist def test_time_position_when_playing(self): self.playback.play() first = self.playback.time_position time.sleep(1) second = self.playback.time_position self.assertGreater(second, first) @unittest.SkipTest # Uses sleep @populate_tracklist def test_time_position_when_paused(self): self.playback.play() time.sleep(0.2) self.playback.pause() time.sleep(0.2) first = self.playback.time_position second = self.playback.time_position self.assertEqual(first, second) @populate_tracklist def test_play_with_consume(self): self.tracklist.consume = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_playlist_is_empty_after_all_tracks_are_played_with_consume(self): self.tracklist.consume = True self.playback.play() for _ in range(len(self.tracklist.tracks)): self.playback.on_end_of_track() self.assertEqual(len(self.tracklist.tracks), 0) @populate_tracklist @mock.patch('random.shuffle') def test_play_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[-1]) @populate_tracklist @mock.patch('random.shuffle') def test_previous_with_random(self, shuffle_mock): shuffle_mock.side_effect = lambda tracks: tracks.reverse() self.tracklist.random = True self.playback.play() self.playback.next() current_track = self.playback.current_track self.playback.previous() self.assertEqual(self.playback.current_track, current_track) @populate_tracklist def test_end_of_song_starts_next_track(self): self.playback.play() self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[1]) @populate_tracklist def test_end_of_song_with_single_and_repeat_starts_same(self): self.tracklist.single = True self.tracklist.repeat = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, self.tracks[0]) @populate_tracklist def test_end_of_song_with_single_random_and_repeat_starts_same(self): self.tracklist.single = True self.tracklist.repeat = True self.tracklist.random = True self.playback.play() current_track = self.playback.current_track self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, current_track) @populate_tracklist def test_end_of_song_with_single_stops(self): self.tracklist.single = True self.playback.play() self.assertEqual(self.playback.current_track, self.tracks[0]) self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_end_of_song_with_single_and_random_stops(self): self.tracklist.single = True self.tracklist.random = True self.playback.play() self.playback.on_end_of_track() self.assertEqual(self.playback.current_track, None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) @populate_tracklist def test_end_of_playlist_stops(self): self.playback.play(self.tracklist.tl_tracks[-1]) self.playback.on_end_of_track() self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_repeat_off_by_default(self): self.assertEqual(self.tracklist.repeat, False) def test_random_off_by_default(self): self.assertEqual(self.tracklist.random, False) def test_consume_off_by_default(self): self.assertEqual(self.tracklist.consume, False) @populate_tracklist def test_random_until_end_of_playlist(self): self.tracklist.random = True self.playback.play() for _ in self.tracks[1:]: self.playback.next() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_random_with_eot_until_end_of_playlist(self): self.tracklist.random = True self.playback.play() for _ in self.tracks[1:]: self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertEqual(self.tracklist.eot_track(tl_track), None) @populate_tracklist def test_random_until_end_of_playlist_and_play_from_start(self): self.tracklist.random = True self.playback.play() for _ in self.tracks: self.playback.next() tl_track = self.playback.current_tl_track self.assertNotEqual(self.tracklist.next_track(tl_track), None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_random_with_eot_until_end_of_playlist_and_play_from_start(self): self.tracklist.random = True self.playback.play() for _ in self.tracks: self.playback.on_end_of_track() tl_track = self.playback.current_tl_track self.assertNotEqual(self.tracklist.eot_track(tl_track), None) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) @populate_tracklist def test_random_until_end_of_playlist_with_repeat(self): self.tracklist.repeat = True self.tracklist.random = True self.playback.play() for _ in self.tracks[1:]: self.playback.next() tl_track = self.playback.current_tl_track self.assertNotEqual(self.tracklist.next_track(tl_track), None) @populate_tracklist def test_played_track_during_random_not_played_again(self): self.tracklist.random = True self.playback.play() played = [] for _ in self.tracks: self.assertNotIn(self.playback.current_track, played) played.append(self.playback.current_track) self.playback.next() @populate_tracklist @mock.patch('random.shuffle') def test_play_track_then_enable_random(self, shuffle_mock): # Covers underlying issue IssueGH17RegressionTest tests for. shuffle_mock.side_effect = lambda tracks: tracks.reverse() expected = self.tl_tracks[::-1] + [None] actual = [] self.playback.play() self.tracklist.random = True while self.playback.state != PlaybackState.STOPPED: self.playback.next() actual.append(self.playback.current_tl_track) self.assertEqual(actual, expected) @populate_tracklist def test_playing_track_that_isnt_in_playlist(self): test = lambda: self.playback.play((17, Track())) self.assertRaises(AssertionError, test) mopidy-0.17.0/tests/backends/local/playlists_test.py000066400000000000000000000176111224420023200225400ustar00rootroot00000000000000from __future__ import unicode_literals import os import shutil import tempfile import unittest import pykka from mopidy import audio, core from mopidy.backends.local import actor from mopidy.models import Playlist, Track from tests import path_to_data_dir from tests.backends.local import generate_song class LocalPlaylistsProviderTest(unittest.TestCase): backend_class = actor.LocalBackend config = { 'local': { 'media_dir': path_to_data_dir(''), 'tag_cache_file': path_to_data_dir('library_tag_cache'), } } def setUp(self): self.config['local']['playlists_dir'] = tempfile.mkdtemp() self.playlists_dir = self.config['local']['playlists_dir'] self.audio = audio.DummyAudio.start().proxy() self.backend = actor.LocalBackend.start( config=self.config, audio=self.audio).proxy() self.core = core.Core(backends=[self.backend]) def tearDown(self): pykka.ActorRegistry.stop_all() if os.path.exists(self.playlists_dir): shutil.rmtree(self.playlists_dir) def test_created_playlist_is_persisted(self): path = os.path.join(self.playlists_dir, 'test.m3u') self.assertFalse(os.path.exists(path)) self.core.playlists.create('test') self.assertTrue(os.path.exists(path)) def test_create_slugifies_playlist_name(self): path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test FOO baR') self.assertEqual('test-foo-bar', playlist.name) self.assertTrue(os.path.exists(path)) def test_create_slugifies_names_which_tries_to_change_directory(self): path = os.path.join(self.playlists_dir, 'test-foo-bar.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('../../test FOO baR') self.assertEqual('test-foo-bar', playlist.name) self.assertTrue(os.path.exists(path)) def test_saved_playlist_is_persisted(self): path1 = os.path.join(self.playlists_dir, 'test1.m3u') path2 = os.path.join(self.playlists_dir, 'test2-foo-bar.m3u') playlist = self.core.playlists.create('test1') self.assertTrue(os.path.exists(path1)) self.assertFalse(os.path.exists(path2)) playlist = playlist.copy(name='test2 FOO baR') playlist = self.core.playlists.save(playlist) self.assertEqual('test2-foo-bar', playlist.name) self.assertFalse(os.path.exists(path1)) self.assertTrue(os.path.exists(path2)) def test_deleted_playlist_is_removed(self): path = os.path.join(self.playlists_dir, 'test.m3u') self.assertFalse(os.path.exists(path)) playlist = self.core.playlists.create('test') self.assertTrue(os.path.exists(path)) self.core.playlists.delete(playlist.uri) self.assertFalse(os.path.exists(path)) def test_playlist_contents_is_written_to_disk(self): track = Track(uri=generate_song(1)) playlist = self.core.playlists.create('test') playlist_path = os.path.join(self.playlists_dir, 'test.m3u') playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) with open(playlist_path) as playlist_file: contents = playlist_file.read() self.assertEqual(track.uri, contents.strip()) def test_playlists_are_loaded_at_startup(self): track = Track(uri='local:track:path2') playlist = self.core.playlists.create('test') playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) backend = self.backend_class(config=self.config, audio=self.audio) self.assert_(backend.playlists.playlists) self.assertEqual( 'local:playlist:test', backend.playlists.playlists[0].uri) self.assertEqual( playlist.name, backend.playlists.playlists[0].name) self.assertEqual( track.uri, backend.playlists.playlists[0].tracks[0].uri) @unittest.SkipTest def test_santitising_of_playlist_filenames(self): pass @unittest.SkipTest def test_playlist_dir_is_created(self): pass def test_create_returns_playlist_with_name_set(self): playlist = self.core.playlists.create('test') self.assertEqual(playlist.name, 'test') def test_create_returns_playlist_with_uri_set(self): playlist = self.core.playlists.create('test') self.assert_(playlist.uri) def test_create_adds_playlist_to_playlists_collection(self): playlist = self.core.playlists.create('test') self.assert_(self.core.playlists.playlists) self.assertIn(playlist, self.core.playlists.playlists) def test_playlists_empty_to_start_with(self): self.assert_(not self.core.playlists.playlists) def test_delete_non_existant_playlist(self): self.core.playlists.delete('file:///unknown/playlist') def test_delete_playlist_removes_it_from_the_collection(self): playlist = self.core.playlists.create('test') self.assertIn(playlist, self.core.playlists.playlists) self.core.playlists.delete(playlist.uri) self.assertNotIn(playlist, self.core.playlists.playlists) def test_filter_without_criteria(self): self.assertEqual( self.core.playlists.playlists, self.core.playlists.filter()) def test_filter_with_wrong_criteria(self): self.assertEqual([], self.core.playlists.filter(name='foo')) def test_filter_with_right_criteria(self): playlist = self.core.playlists.create('test') playlists = self.core.playlists.filter(name='test') self.assertEqual([playlist], playlists) def test_filter_by_name_returns_single_match(self): playlist = Playlist(name='b') self.backend.playlists.playlists = [Playlist(name='a'), playlist] self.assertEqual([playlist], self.core.playlists.filter(name='b')) def test_filter_by_name_returns_multiple_matches(self): playlist = Playlist(name='b') self.backend.playlists.playlists = [ playlist, Playlist(name='a'), Playlist(name='b')] playlists = self.core.playlists.filter(name='b') self.assertIn(playlist, playlists) self.assertEqual(2, len(playlists)) def test_filter_by_name_returns_no_matches(self): self.backend.playlists.playlists = [ Playlist(name='a'), Playlist(name='b')] self.assertEqual([], self.core.playlists.filter(name='c')) def test_lookup_finds_playlist_by_uri(self): original_playlist = self.core.playlists.create('test') looked_up_playlist = self.core.playlists.lookup(original_playlist.uri) self.assertEqual(original_playlist, looked_up_playlist) @unittest.SkipTest def test_refresh(self): pass def test_save_replaces_existing_playlist_with_updated_playlist(self): playlist1 = self.core.playlists.create('test1') self.assertIn(playlist1, self.core.playlists.playlists) playlist2 = playlist1.copy(name='test2') playlist2 = self.core.playlists.save(playlist2) self.assertNotIn(playlist1, self.core.playlists.playlists) self.assertIn(playlist2, self.core.playlists.playlists) def test_playlist_with_unknown_track(self): track = Track(uri='file:///dev/null') playlist = self.core.playlists.create('test') playlist = playlist.copy(tracks=[track]) playlist = self.core.playlists.save(playlist) backend = self.backend_class(config=self.config, audio=self.audio) self.assert_(backend.playlists.playlists) self.assertEqual( 'local:playlist:test', backend.playlists.playlists[0].uri) self.assertEqual( playlist.name, backend.playlists.playlists[0].name) self.assertEqual( track.uri, backend.playlists.playlists[0].tracks[0].uri) mopidy-0.17.0/tests/backends/local/tracklist_test.py000066400000000000000000000276201224420023200225150ustar00rootroot00000000000000from __future__ import unicode_literals import random import unittest import pykka from mopidy import audio, core from mopidy.backends.local import actor from mopidy.core import PlaybackState from mopidy.models import Playlist, TlTrack, Track from tests import path_to_data_dir from tests.backends.local import generate_song, populate_tracklist class LocalTracklistProviderTest(unittest.TestCase): config = { 'local': { 'media_dir': path_to_data_dir(''), 'playlists_dir': b'', 'tag_cache_file': path_to_data_dir('empty_tag_cache'), } } tracks = [ Track(uri=generate_song(i), length=4464) for i in range(1, 4)] def setUp(self): self.audio = audio.DummyAudio.start().proxy() self.backend = actor.LocalBackend.start( config=self.config, audio=self.audio).proxy() self.core = core.Core(audio=self.audio, backends=[self.backend]) self.controller = self.core.tracklist self.playback = self.core.playback assert len(self.tracks) == 3, 'Need three tracks to run tests.' def tearDown(self): pykka.ActorRegistry.stop_all() def test_length(self): self.assertEqual(0, len(self.controller.tl_tracks)) self.assertEqual(0, self.controller.length) self.controller.add(self.tracks) self.assertEqual(3, len(self.controller.tl_tracks)) self.assertEqual(3, self.controller.length) def test_add(self): for track in self.tracks: tl_tracks = self.controller.add([track]) self.assertEqual(track, self.controller.tracks[-1]) self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1]) self.assertEqual(track, tl_tracks[0].track) def test_add_at_position(self): for track in self.tracks[:-1]: tl_tracks = self.controller.add([track], 0) self.assertEqual(track, self.controller.tracks[0]) self.assertEqual(tl_tracks[0], self.controller.tl_tracks[0]) self.assertEqual(track, tl_tracks[0].track) @populate_tracklist def test_add_at_position_outside_of_playlist(self): for track in self.tracks: tl_tracks = self.controller.add([track], len(self.tracks) + 2) self.assertEqual(track, self.controller.tracks[-1]) self.assertEqual(tl_tracks[0], self.controller.tl_tracks[-1]) self.assertEqual(track, tl_tracks[0].track) @populate_tracklist def test_filter_by_tlid(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( [tl_track], self.controller.filter(tlid=[tl_track.tlid])) @populate_tracklist def test_filter_by_uri(self): tl_track = self.controller.tl_tracks[1] self.assertEqual( [tl_track], self.controller.filter(uri=[tl_track.track.uri])) @populate_tracklist def test_filter_by_uri_returns_nothing_for_invalid_uri(self): self.assertEqual([], self.controller.filter(uri=['foobar'])) def test_filter_by_uri_returns_single_match(self): track = Track(uri='a') self.controller.add([Track(uri='z'), track, Track(uri='y')]) self.assertEqual(track, self.controller.filter(uri=['a'])[0].track) def test_filter_by_uri_returns_multiple_matches(self): track = Track(uri='a') self.controller.add([Track(uri='z'), track, track]) tl_tracks = self.controller.filter(uri=['a']) self.assertEqual(track, tl_tracks[0].track) self.assertEqual(track, tl_tracks[1].track) def test_filter_by_uri_returns_nothing_if_no_match(self): self.controller.playlist = Playlist( tracks=[Track(uri=['z']), Track(uri=['y'])]) self.assertEqual([], self.controller.filter(uri=['a'])) def test_filter_by_multiple_criteria_returns_elements_matching_all(self): track1 = Track(uri='a', name='x') track2 = Track(uri='b', name='x') track3 = Track(uri='b', name='y') self.controller.add([track1, track2, track3]) self.assertEqual( track1, self.controller.filter(uri=['a'], name=['x'])[0].track) self.assertEqual( track2, self.controller.filter(uri=['b'], name=['x'])[0].track) self.assertEqual( track3, self.controller.filter(uri=['b'], name=['y'])[0].track) def test_filter_by_criteria_that_is_not_present_in_all_elements(self): track1 = Track() track2 = Track(uri='b') track3 = Track() self.controller.add([track1, track2, track3]) self.assertEqual(track2, self.controller.filter(uri=['b'])[0].track) @populate_tracklist def test_clear(self): self.controller.clear() self.assertEqual(len(self.controller.tracks), 0) def test_clear_empty_playlist(self): self.controller.clear() self.assertEqual(len(self.controller.tracks), 0) @populate_tracklist def test_clear_when_playing(self): self.playback.play() self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.controller.clear() self.assertEqual(self.playback.state, PlaybackState.STOPPED) def test_add_appends_to_the_tracklist(self): self.controller.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.controller.tracks), 2) self.controller.add([Track(uri='c'), Track(uri='d')]) self.assertEqual(len(self.controller.tracks), 4) self.assertEqual(self.controller.tracks[0].uri, 'a') self.assertEqual(self.controller.tracks[1].uri, 'b') self.assertEqual(self.controller.tracks[2].uri, 'c') self.assertEqual(self.controller.tracks[3].uri, 'd') def test_add_does_not_reset_version(self): version = self.controller.version self.controller.add([]) self.assertEqual(self.controller.version, version) @populate_tracklist def test_add_preserves_playing_state(self): self.playback.play() track = self.playback.current_track self.controller.add(self.controller.tracks[1:2]) self.assertEqual(self.playback.state, PlaybackState.PLAYING) self.assertEqual(self.playback.current_track, track) @populate_tracklist def test_add_preserves_stopped_state(self): self.controller.add(self.controller.tracks[1:2]) self.assertEqual(self.playback.state, PlaybackState.STOPPED) self.assertEqual(self.playback.current_track, None) @populate_tracklist def test_add_returns_the_tl_tracks_that_was_added(self): tl_tracks = self.controller.add(self.controller.tracks[1:2]) self.assertEqual(tl_tracks[0].track, self.controller.tracks[1]) def test_index_returns_index_of_track(self): tl_tracks = self.controller.add(self.tracks) self.assertEqual(0, self.controller.index(tl_tracks[0])) self.assertEqual(1, self.controller.index(tl_tracks[1])) self.assertEqual(2, self.controller.index(tl_tracks[2])) def test_index_returns_none_if_item_not_found(self): tl_track = TlTrack(0, Track()) self.assertEqual(self.controller.index(tl_track), None) @populate_tracklist def test_move_single(self): self.controller.move(0, 0, 2) tracks = self.controller.tracks self.assertEqual(tracks[2], self.tracks[0]) @populate_tracklist def test_move_group(self): self.controller.move(0, 2, 1) tracks = self.controller.tracks self.assertEqual(tracks[1], self.tracks[0]) self.assertEqual(tracks[2], self.tracks[1]) @populate_tracklist def test_moving_track_outside_of_playlist(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(0, 0, tracks + 5) self.assertRaises(AssertionError, test) @populate_tracklist def test_move_group_outside_of_playlist(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(0, 2, tracks + 5) self.assertRaises(AssertionError, test) @populate_tracklist def test_move_group_out_of_range(self): tracks = len(self.controller.tracks) test = lambda: self.controller.move(tracks + 2, tracks + 3, 0) self.assertRaises(AssertionError, test) @populate_tracklist def test_move_group_invalid_group(self): test = lambda: self.controller.move(2, 1, 0) self.assertRaises(AssertionError, test) def test_tracks_attribute_is_immutable(self): tracks1 = self.controller.tracks tracks2 = self.controller.tracks self.assertNotEqual(id(tracks1), id(tracks2)) @populate_tracklist def test_remove(self): track1 = self.controller.tracks[1] track2 = self.controller.tracks[2] version = self.controller.version self.controller.remove(uri=[track1.uri]) self.assertLess(version, self.controller.version) self.assertNotIn(track1, self.controller.tracks) self.assertEqual(track2, self.controller.tracks[1]) @populate_tracklist def test_removing_track_that_does_not_exist_does_nothing(self): self.controller.remove(uri=['/nonexistant']) def test_removing_from_empty_playlist_does_nothing(self): self.controller.remove(uri=['/nonexistant']) @populate_tracklist def test_remove_lists(self): track0 = self.controller.tracks[0] track1 = self.controller.tracks[1] track2 = self.controller.tracks[2] version = self.controller.version self.controller.remove(uri=[track0.uri, track2.uri]) self.assertLess(version, self.controller.version) self.assertNotIn(track0, self.controller.tracks) self.assertNotIn(track2, self.controller.tracks) self.assertEqual(track1, self.controller.tracks[0]) @populate_tracklist def test_shuffle(self): random.seed(1) self.controller.shuffle() shuffled_tracks = self.controller.tracks self.assertNotEqual(self.tracks, shuffled_tracks) self.assertEqual(set(self.tracks), set(shuffled_tracks)) @populate_tracklist def test_shuffle_subset(self): random.seed(1) self.controller.shuffle(1, 3) shuffled_tracks = self.controller.tracks self.assertNotEqual(self.tracks, shuffled_tracks) self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(set(self.tracks), set(shuffled_tracks)) @populate_tracklist def test_shuffle_invalid_subset(self): test = lambda: self.controller.shuffle(3, 1) self.assertRaises(AssertionError, test) @populate_tracklist def test_shuffle_superset(self): tracks = len(self.controller.tracks) test = lambda: self.controller.shuffle(1, tracks + 5) self.assertRaises(AssertionError, test) @populate_tracklist def test_shuffle_open_subset(self): random.seed(1) self.controller.shuffle(1) shuffled_tracks = self.controller.tracks self.assertNotEqual(self.tracks, shuffled_tracks) self.assertEqual(self.tracks[0], shuffled_tracks[0]) self.assertEqual(set(self.tracks), set(shuffled_tracks)) @populate_tracklist def test_slice_returns_a_subset_of_tracks(self): track_slice = self.controller.slice(1, 3) self.assertEqual(2, len(track_slice)) self.assertEqual(self.tracks[1], track_slice[0].track) self.assertEqual(self.tracks[2], track_slice[1].track) @populate_tracklist def test_slice_returns_empty_list_if_indexes_outside_tracks_list(self): self.assertEqual(0, len(self.controller.slice(7, 8))) self.assertEqual(0, len(self.controller.slice(-1, 1))) def test_version_does_not_change_when_adding_nothing(self): version = self.controller.version self.controller.add([]) self.assertEquals(version, self.controller.version) def test_version_increases_when_adding_something(self): version = self.controller.version self.controller.add([Track()]) self.assertLess(version, self.controller.version) mopidy-0.17.0/tests/backends/local/translator_test.py000066400000000000000000000155001224420023200227000ustar00rootroot00000000000000# encoding: utf-8 from __future__ import unicode_literals import os import tempfile import unittest from mopidy.backends.local.translator import parse_m3u, parse_mpd_tag_cache from mopidy.models import Track, Artist, Album from mopidy.utils.path import path_to_uri from tests import path_to_data_dir data_dir = path_to_data_dir('') song1_path = path_to_data_dir('song1.mp3') song2_path = path_to_data_dir('song2.mp3') encoded_path = path_to_data_dir('æøå.mp3') song1_uri = path_to_uri(song1_path) song2_uri = path_to_uri(song2_path) encoded_uri = path_to_uri(encoded_path) # FIXME use mock instead of tempfile.NamedTemporaryFile class M3UToUriTest(unittest.TestCase): def test_empty_file(self): uris = parse_m3u(path_to_data_dir('empty.m3u'), data_dir) self.assertEqual([], uris) def test_basic_file(self): uris = parse_m3u(path_to_data_dir('one.m3u'), data_dir) self.assertEqual([song1_uri], uris) def test_file_with_comment(self): uris = parse_m3u(path_to_data_dir('comment.m3u'), data_dir) self.assertEqual([song1_uri], uris) def test_file_is_relative_to_correct_dir(self): with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write('song1.mp3') try: uris = parse_m3u(tmp.name, data_dir) self.assertEqual([song1_uri], uris) finally: if os.path.exists(tmp.name): os.remove(tmp.name) def test_file_with_absolute_files(self): with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write(song1_path) try: uris = parse_m3u(tmp.name, data_dir) self.assertEqual([song1_uri], uris) finally: if os.path.exists(tmp.name): os.remove(tmp.name) def test_file_with_multiple_absolute_files(self): with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write(song1_path + '\n') tmp.write('# comment \n') tmp.write(song2_path) try: uris = parse_m3u(tmp.name, data_dir) self.assertEqual([song1_uri, song2_uri], uris) finally: if os.path.exists(tmp.name): os.remove(tmp.name) def test_file_with_uri(self): with tempfile.NamedTemporaryFile(delete=False) as tmp: tmp.write(song1_uri) try: uris = parse_m3u(tmp.name, data_dir) self.assertEqual([song1_uri], uris) finally: if os.path.exists(tmp.name): os.remove(tmp.name) def test_encoding_is_latin1(self): uris = parse_m3u(path_to_data_dir('encoding.m3u'), data_dir) self.assertEqual([encoded_uri], uris) def test_open_missing_file(self): uris = parse_m3u(path_to_data_dir('non-existant.m3u'), data_dir) self.assertEqual([], uris) class URItoM3UTest(unittest.TestCase): pass expected_artists = [Artist(name='name')] expected_albums = [ Album(name='albumname', artists=expected_artists, num_tracks=2), Album(name='albumname', num_tracks=2), ] expected_tracks = [] def generate_track(path, ident, album_id): uri = 'local:track:%s' % path track = Track( uri=uri, name='trackname', artists=expected_artists, album=expected_albums[album_id], track_no=1, date='2006', length=4000, last_modified=1272319626) expected_tracks.append(track) generate_track('song1.mp3', 6, 0) generate_track('song2.mp3', 7, 0) generate_track('song3.mp3', 8, 1) generate_track('subdir1/song4.mp3', 2, 0) generate_track('subdir1/song5.mp3', 3, 0) generate_track('subdir2/song6.mp3', 4, 1) generate_track('subdir2/song7.mp3', 5, 1) generate_track('subdir1/subsubdir/song8.mp3', 0, 0) generate_track('subdir1/subsubdir/song9.mp3', 1, 1) class MPDTagCacheToTracksTest(unittest.TestCase): def test_emtpy_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('empty_tag_cache'), path_to_data_dir('')) self.assertEqual(set(), tracks) def test_simple_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('simple_tag_cache'), path_to_data_dir('')) track = Track( uri='local:track:song1.mp3', name='trackname', artists=expected_artists, track_no=1, album=expected_albums[0], date='2006', length=4000, last_modified=1272319626) self.assertEqual(set([track]), tracks) def test_advanced_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('advanced_tag_cache'), path_to_data_dir('')) self.assertEqual(set(expected_tracks), tracks) def test_unicode_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('utf8_tag_cache'), path_to_data_dir('')) artists = [Artist(name='æøå')] album = Album(name='æøå', artists=artists) track = Track( uri='local:track:song1.mp3', name='æøå', artists=artists, composers=artists, performers=artists, genre='æøå', album=album, length=4000, last_modified=1272319626, comment='æøå&^`ൂ㔶') self.assertEqual(track, list(tracks)[0]) @unittest.SkipTest def test_misencoded_cache(self): # FIXME not sure if this can happen pass def test_cache_with_blank_track_info(self): tracks = parse_mpd_tag_cache( path_to_data_dir('blank_tag_cache'), path_to_data_dir('')) expected = Track( uri='local:track:song1.mp3', length=4000, last_modified=1272319626) self.assertEqual(set([expected]), tracks) def test_musicbrainz_tagcache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('musicbrainz_tag_cache'), path_to_data_dir('')) artist = list(expected_tracks[0].artists)[0].copy( musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') albumartist = list(expected_tracks[0].artists)[0].copy( name='albumartistname', musicbrainz_id='7364dea6-ca9a-48e3-be01-b44ad0d19897') album = expected_tracks[0].album.copy( artists=[albumartist], musicbrainz_id='cb5f1603-d314-4c9c-91e5-e295cfb125d2') track = expected_tracks[0].copy( artists=[artist], album=album, musicbrainz_id='90488461-8c1f-4a4e-826b-4c6dc70801f0') self.assertEqual(track, list(tracks)[0]) def test_albumartist_tag_cache(self): tracks = parse_mpd_tag_cache( path_to_data_dir('albumartist_tag_cache'), path_to_data_dir('')) artist = Artist(name='albumartistname') album = expected_albums[0].copy(artists=[artist]) track = Track( uri='local:track:song1.mp3', name='trackname', artists=expected_artists, track_no=1, album=album, date='2006', length=4000, last_modified=1272319626) self.assertEqual(track, list(tracks)[0]) mopidy-0.17.0/tests/commands_test.py000066400000000000000000000416611224420023200174530ustar00rootroot00000000000000from __future__ import unicode_literals import argparse import mock import unittest from mopidy import commands class ConfigOverrideTypeTest(unittest.TestCase): def test_valid_override(self): expected = (b'section', b'key', b'value') self.assertEqual( expected, commands.config_override_type(b'section/key=value')) self.assertEqual( expected, commands.config_override_type(b'section/key=value ')) self.assertEqual( expected, commands.config_override_type(b'section/key =value')) self.assertEqual( expected, commands.config_override_type(b'section /key=value')) def test_valid_override_is_bytes(self): section, key, value = commands.config_override_type( b'section/key=value') self.assertIsInstance(section, bytes) self.assertIsInstance(key, bytes) self.assertIsInstance(value, bytes) def test_empty_override(self): expected = ('section', 'key', '') self.assertEqual( expected, commands.config_override_type(b'section/key=')) self.assertEqual( expected, commands.config_override_type(b'section/key= ')) def test_invalid_override(self): self.assertRaises( argparse.ArgumentTypeError, commands.config_override_type, b'section/key') self.assertRaises( argparse.ArgumentTypeError, commands.config_override_type, b'section=') self.assertRaises( argparse.ArgumentTypeError, commands.config_override_type, b'section') class CommandParsingTest(unittest.TestCase): def setUp(self): self.exit_patcher = mock.patch.object(commands.Command, 'exit') self.exit_mock = self.exit_patcher.start() self.exit_mock.side_effect = SystemExit def tearDown(self): self.exit_patcher.stop() def test_command_parsing_returns_namespace(self): cmd = commands.Command() self.assertIsInstance(cmd.parse([]), argparse.Namespace) def test_command_parsing_does_not_contain_args(self): cmd = commands.Command() result = cmd.parse([]) self.assertFalse(hasattr(result, '_args')) def test_unknown_options_bails(self): cmd = commands.Command() with self.assertRaises(SystemExit): cmd.parse(['--foobar']) def test_invalid_sub_command_bails(self): cmd = commands.Command() with self.assertRaises(SystemExit): cmd.parse(['foo']) def test_command_arguments(self): cmd = commands.Command() cmd.add_argument('--bar') result = cmd.parse(['--bar', 'baz']) self.assertEqual(result.bar, 'baz') def test_command_arguments_and_sub_command(self): child = commands.Command() child.add_argument('--baz') cmd = commands.Command() cmd.add_argument('--bar') cmd.add_child('foo', child) result = cmd.parse(['--bar', 'baz', 'foo']) self.assertEqual(result.bar, 'baz') self.assertEqual(result.baz, None) def test_subcommand_may_have_positional(self): child = commands.Command() child.add_argument('bar') cmd = commands.Command() cmd.add_child('foo', child) result = cmd.parse(['foo', 'baz']) self.assertEqual(result.bar, 'baz') def test_subcommand_may_have_remainder(self): child = commands.Command() child.add_argument('bar', nargs=argparse.REMAINDER) cmd = commands.Command() cmd.add_child('foo', child) result = cmd.parse(['foo', 'baz', 'bep', 'bop']) self.assertEqual(result.bar, ['baz', 'bep', 'bop']) def test_result_stores_choosen_command(self): child = commands.Command() cmd = commands.Command() cmd.add_child('foo', child) result = cmd.parse(['foo']) self.assertEqual(result.command, child) result = cmd.parse([]) self.assertEqual(result.command, cmd) child2 = commands.Command() cmd.add_child('bar', child2) subchild = commands.Command() child.add_child('baz', subchild) result = cmd.parse(['bar']) self.assertEqual(result.command, child2) result = cmd.parse(['foo', 'baz']) self.assertEqual(result.command, subchild) def test_invalid_type(self): cmd = commands.Command() cmd.add_argument('--bar', type=int) with self.assertRaises(SystemExit): cmd.parse(['--bar', b'zero'], prog='foo') self.exit_mock.assert_called_once_with( 1, "argument --bar: invalid int value: 'zero'", 'usage: foo [--bar BAR]') @mock.patch('sys.argv') def test_command_error_usage_prog(self, argv_mock): argv_mock.__getitem__.return_value = '/usr/bin/foo' cmd = commands.Command() cmd.add_argument('--bar', required=True) with self.assertRaises(SystemExit): cmd.parse([]) self.exit_mock.assert_called_once_with( mock.ANY, mock.ANY, 'usage: foo --bar BAR') self.exit_mock.reset_mock() with self.assertRaises(SystemExit): cmd.parse([], prog='baz') self.exit_mock.assert_called_once_with( mock.ANY, mock.ANY, 'usage: baz --bar BAR') def test_missing_required(self): cmd = commands.Command() cmd.add_argument('--bar', required=True) with self.assertRaises(SystemExit): cmd.parse([], prog='foo') self.exit_mock.assert_called_once_with( 1, 'argument --bar is required', 'usage: foo --bar BAR') def test_missing_positionals(self): cmd = commands.Command() cmd.add_argument('bar') with self.assertRaises(SystemExit): cmd.parse([], prog='foo') self.exit_mock.assert_called_once_with( 1, 'too few arguments', 'usage: foo bar') def test_missing_positionals_subcommand(self): child = commands.Command() child.add_argument('baz') cmd = commands.Command() cmd.add_child('bar', child) with self.assertRaises(SystemExit): cmd.parse(['bar'], prog='foo') self.exit_mock.assert_called_once_with( 1, 'too few arguments', 'usage: foo bar baz') def test_unknown_command(self): cmd = commands.Command() with self.assertRaises(SystemExit): cmd.parse(['--help'], prog='foo') self.exit_mock.assert_called_once_with( 1, 'unrecognized arguments: --help', 'usage: foo') def test_invalid_subcommand(self): cmd = commands.Command() cmd.add_child('baz', commands.Command()) with self.assertRaises(SystemExit): cmd.parse(['bar'], prog='foo') self.exit_mock.assert_called_once_with( 1, 'unrecognized command: bar', 'usage: foo') def test_set(self): cmd = commands.Command() cmd.set(foo='bar') result = cmd.parse([]) self.assertEqual(result.foo, 'bar') def test_set_propegate(self): child = commands.Command() cmd = commands.Command() cmd.set(foo='bar') cmd.add_child('command', child) result = cmd.parse(['command']) self.assertEqual(result.foo, 'bar') def test_innermost_set_wins(self): child = commands.Command() child.set(foo='bar', baz=1) cmd = commands.Command() cmd.set(foo='baz', baz=None) cmd.add_child('command', child) result = cmd.parse(['command']) self.assertEqual(result.foo, 'bar') self.assertEqual(result.baz, 1) def test_help_action_works(self): cmd = commands.Command() cmd.add_argument('-h', action='help') cmd.format_help = mock.Mock() with self.assertRaises(SystemExit): cmd.parse(['-h']) cmd.format_help.assert_called_once_with(mock.ANY) self.exit_mock.assert_called_once_with(0, cmd.format_help.return_value) class UsageTest(unittest.TestCase): @mock.patch('sys.argv') def test_prog_name_default_and_override(self, argv_mock): argv_mock.__getitem__.return_value = '/usr/bin/foo' cmd = commands.Command() self.assertEqual('usage: foo', cmd.format_usage().strip()) self.assertEqual('usage: baz', cmd.format_usage('baz').strip()) def test_basic_usage(self): cmd = commands.Command() self.assertEqual('usage: foo', cmd.format_usage('foo').strip()) cmd.add_argument('-h', '--help', action='store_true') self.assertEqual('usage: foo [-h]', cmd.format_usage('foo').strip()) cmd.add_argument('bar') self.assertEqual('usage: foo [-h] bar', cmd.format_usage('foo').strip()) def test_nested_usage(self): child = commands.Command() cmd = commands.Command() cmd.add_child('bar', child) self.assertEqual('usage: foo', cmd.format_usage('foo').strip()) self.assertEqual('usage: foo bar', cmd.format_usage('foo bar').strip()) cmd.add_argument('-h', '--help', action='store_true') self.assertEqual('usage: foo bar', child.format_usage('foo bar').strip()) child.add_argument('-h', '--help', action='store_true') self.assertEqual('usage: foo bar [-h]', child.format_usage('foo bar').strip()) class HelpTest(unittest.TestCase): @mock.patch('sys.argv') def test_prog_name_default_and_override(self, argv_mock): argv_mock.__getitem__.return_value = '/usr/bin/foo' cmd = commands.Command() self.assertEqual('usage: foo', cmd.format_help().strip()) self.assertEqual('usage: bar', cmd.format_help('bar').strip()) def test_command_without_documenation_or_options(self): cmd = commands.Command() self.assertEqual('usage: bar', cmd.format_help('bar').strip()) def test_command_with_option(self): cmd = commands.Command() cmd.add_argument('-h', '--help', action='store_true', help='show this message') expected = ('usage: foo [-h]\n\n' 'OPTIONS:\n\n' ' -h, --help show this message') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_command_with_option_and_positional(self): cmd = commands.Command() cmd.add_argument('-h', '--help', action='store_true', help='show this message') cmd.add_argument('bar', help='some help text') expected = ('usage: foo [-h] bar\n\n' 'OPTIONS:\n\n' ' -h, --help show this message\n' ' bar some help text') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_command_with_documentation(self): cmd = commands.Command() cmd.help = 'some text about everything this command does.' expected = ('usage: foo\n\n' 'some text about everything this command does.') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_command_with_documentation_and_option(self): cmd = commands.Command() cmd.help = 'some text about everything this command does.' cmd.add_argument('-h', '--help', action='store_true', help='show this message') expected = ('usage: foo [-h]\n\n' 'some text about everything this command does.\n\n' 'OPTIONS:\n\n' ' -h, --help show this message') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_subcommand_without_documentation_or_options(self): child = commands.Command() cmd = commands.Command() cmd.add_child('bar', child) self.assertEqual('usage: foo', cmd.format_help('foo').strip()) def test_subcommand_with_documentation_shown(self): child = commands.Command() child.help = 'some text about everything this command does.' cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar\n\n' ' some text about everything this command does.') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_subcommand_with_options_shown(self): child = commands.Command() child.add_argument('-h', '--help', action='store_true', help='show this message') cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar [-h]\n\n' ' -h, --help show this message') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_subcommand_with_positional_shown(self): child = commands.Command() child.add_argument('baz', help='the great and wonderful') cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar baz\n\n' ' baz the great and wonderful') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_subcommand_with_options_and_documentation(self): child = commands.Command() child.help = ' some text about everything this command does.' child.add_argument('-h', '--help', action='store_true', help='show this message') cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar [-h]\n\n' ' some text about everything this command does.\n\n' ' -h, --help show this message') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_nested_subcommands_with_options(self): subchild = commands.Command() subchild.add_argument('--test', help='the great and wonderful') child = commands.Command() child.add_child('baz', subchild) child.add_argument('-h', '--help', action='store_true', help='show this message') cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar [-h]\n\n' ' -h, --help show this message\n\n' 'bar baz [--test TEST]\n\n' ' --test TEST the great and wonderful') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_nested_subcommands_skipped_intermediate(self): subchild = commands.Command() subchild.add_argument('--test', help='the great and wonderful') child = commands.Command() child.add_child('baz', subchild) cmd = commands.Command() cmd.add_child('bar', child) expected = ('usage: foo\n\n' 'COMMANDS:\n\n' 'bar baz [--test TEST]\n\n' ' --test TEST the great and wonderful') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_command_with_option_and_subcommand_with_option(self): child = commands.Command() child.add_argument('--test', help='the great and wonderful') cmd = commands.Command() cmd.add_argument('-h', '--help', action='store_true', help='show this message') cmd.add_child('bar', child) expected = ('usage: foo [-h]\n\n' 'OPTIONS:\n\n' ' -h, --help show this message\n\n' 'COMMANDS:\n\n' 'bar [--test TEST]\n\n' ' --test TEST the great and wonderful') self.assertEqual(expected, cmd.format_help('foo').strip()) def test_command_with_options_doc_and_subcommand_with_option_and_doc(self): child = commands.Command() child.help = 'some text about this sub-command.' child.add_argument('--test', help='the great and wonderful') cmd = commands.Command() cmd.help = 'some text about everything this command does.' cmd.add_argument('-h', '--help', action='store_true', help='show this message') cmd.add_child('bar', child) expected = ('usage: foo [-h]\n\n' 'some text about everything this command does.\n\n' 'OPTIONS:\n\n' ' -h, --help show this message\n\n' 'COMMANDS:\n\n' 'bar [--test TEST]\n\n' ' some text about this sub-command.\n\n' ' --test TEST the great and wonderful') self.assertEqual(expected, cmd.format_help('foo').strip()) class RunTest(unittest.TestCase): def test_default_implmentation_raises_error(self): with self.assertRaises(NotImplementedError): commands.Command().run() mopidy-0.17.0/tests/config/000077500000000000000000000000001224420023200154765ustar00rootroot00000000000000mopidy-0.17.0/tests/config/__init__.py000066400000000000000000000000001224420023200175750ustar00rootroot00000000000000mopidy-0.17.0/tests/config/config_test.py000066400000000000000000000237261224420023200203660ustar00rootroot00000000000000# encoding: utf-8 from __future__ import unicode_literals import mock import unittest from mopidy import config from tests import path_to_data_dir class LoadConfigTest(unittest.TestCase): def test_load_nothing(self): self.assertEqual({}, config._load([], [], [])) def test_load_single_default(self): default = b'[foo]\nbar = baz' expected = {'foo': {'bar': 'baz'}} result = config._load([], [default], []) self.assertEqual(expected, result) def test_unicode_default(self): default = '[foo]\nbar = æøå' expected = {'foo': {'bar': 'æøå'.encode('utf-8')}} result = config._load([], [default], []) self.assertEqual(expected, result) def test_load_defaults(self): default1 = b'[foo]\nbar = baz' default2 = b'[foo2]\n' expected = {'foo': {'bar': 'baz'}, 'foo2': {}} result = config._load([], [default1, default2], []) self.assertEqual(expected, result) def test_load_single_override(self): override = ('foo', 'bar', 'baz') expected = {'foo': {'bar': 'baz'}} result = config._load([], [], [override]) self.assertEqual(expected, result) def test_load_overrides(self): override1 = ('foo', 'bar', 'baz') override2 = ('foo2', 'bar', 'baz') expected = {'foo': {'bar': 'baz'}, 'foo2': {'bar': 'baz'}} result = config._load([], [], [override1, override2]) self.assertEqual(expected, result) def test_load_single_file(self): file1 = path_to_data_dir('file1.conf') expected = {'foo': {'bar': 'baz'}} result = config._load([file1], [], []) self.assertEqual(expected, result) def test_load_files(self): file1 = path_to_data_dir('file1.conf') file2 = path_to_data_dir('file2.conf') expected = {'foo': {'bar': 'baz'}, 'foo2': {'bar': 'baz'}} result = config._load([file1, file2], [], []) self.assertEqual(expected, result) def test_load_file_with_utf8(self): expected = {'foo': {'bar': 'æøå'.encode('utf-8')}} result = config._load([path_to_data_dir('file3.conf')], [], []) self.assertEqual(expected, result) def test_load_file_with_error(self): expected = {'foo': {'bar': 'baz'}} result = config._load([path_to_data_dir('file4.conf')], [], []) self.assertEqual(expected, result) class ValidateTest(unittest.TestCase): def setUp(self): self.schema = config.ConfigSchema('foo') self.schema['bar'] = config.ConfigValue() def test_empty_config_no_schemas(self): conf, errors = config._validate({}, []) self.assertEqual({}, conf) self.assertEqual({}, errors) def test_config_no_schemas(self): raw_config = {'foo': {'bar': 'baz'}} conf, errors = config._validate(raw_config, []) self.assertEqual({}, conf) self.assertEqual({}, errors) def test_empty_config_single_schema(self): conf, errors = config._validate({}, [self.schema]) self.assertEqual({'foo': {'bar': None}}, conf) self.assertEqual({'foo': {'bar': 'config key not found.'}}, errors) def test_config_single_schema(self): raw_config = {'foo': {'bar': 'baz'}} conf, errors = config._validate(raw_config, [self.schema]) self.assertEqual({'foo': {'bar': 'baz'}}, conf) self.assertEqual({}, errors) def test_config_single_schema_config_error(self): raw_config = {'foo': {'bar': 'baz'}} self.schema['bar'] = mock.Mock() self.schema['bar'].deserialize.side_effect = ValueError('bad') conf, errors = config._validate(raw_config, [self.schema]) self.assertEqual({'foo': {'bar': None}}, conf) self.assertEqual({'foo': {'bar': 'bad'}}, errors) # TODO: add more tests INPUT_CONFIG = """# comments before first section should work [section] anything goes ; after the [] block it seems. ; this is a valid comment this-should-equal-baz = baz ; as this is a comment this-should-equal-everything = baz # as this is not a comment # this is also a comment ; and the next line should be a blank comment. ; # foo # = should all be treated as a comment.""" PROCESSED_CONFIG = """[__COMMENTS__] __HASH0__ = comments before first section should work __BLANK1__ = [section] __SECTION2__ = anything goes __INLINE3__ = after the [] block it seems. __SEMICOLON4__ = this is a valid comment this-should-equal-baz = baz __INLINE5__ = as this is a comment this-should-equal-everything = baz # as this is not a comment __BLANK6__ = __HASH7__ = this is also a comment __INLINE8__ = and the next line should be a blank comment. __SEMICOLON9__ = __HASH10__ = foo # = should all be treated as a comment.""" class PreProcessorTest(unittest.TestCase): maxDiff = None # Show entire diff. def test_empty_config(self): result = config._preprocess('') self.assertEqual(result, '[__COMMENTS__]') def test_plain_section(self): result = config._preprocess('[section]\nfoo = bar') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar') def test_initial_comments(self): result = config._preprocess('; foobar') self.assertEqual(result, '[__COMMENTS__]\n' '__SEMICOLON0__ = foobar') result = config._preprocess('# foobar') self.assertEqual(result, '[__COMMENTS__]\n' '__HASH0__ = foobar') result = config._preprocess('; foo\n# bar') self.assertEqual(result, '[__COMMENTS__]\n' '__SEMICOLON0__ = foo\n' '__HASH1__ = bar') def test_initial_comment_inline_handling(self): result = config._preprocess('; foo ; bar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '__SEMICOLON0__ = foo\n' '__INLINE1__ = bar\n' '__INLINE2__ = baz') def test_inline_semicolon_comment(self): result = config._preprocess('[section]\nfoo = bar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar\n' '__INLINE0__ = baz') def test_no_inline_hash_comment(self): result = config._preprocess('[section]\nfoo = bar # baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar # baz') def test_section_extra_text(self): result = config._preprocess('[section] foobar') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar') def test_section_extra_text_inline_semicolon(self): result = config._preprocess('[section] foobar ; baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar\n' '__INLINE1__ = baz') def test_conversion(self): """Tests all of the above cases at once.""" result = config._preprocess(INPUT_CONFIG) self.assertEqual(result, PROCESSED_CONFIG) class PostProcessorTest(unittest.TestCase): maxDiff = None # Show entire diff. def test_empty_config(self): result = config._postprocess('[__COMMENTS__]') self.assertEqual(result, '') def test_plain_section(self): result = config._postprocess('[__COMMENTS__]\n' '[section]\n' 'foo = bar') self.assertEqual(result, '[section]\nfoo = bar') def test_initial_comments(self): result = config._postprocess('[__COMMENTS__]\n' '__SEMICOLON0__ = foobar') self.assertEqual(result, '; foobar') result = config._postprocess('[__COMMENTS__]\n' '__HASH0__ = foobar') self.assertEqual(result, '# foobar') result = config._postprocess('[__COMMENTS__]\n' '__SEMICOLON0__ = foo\n' '__HASH1__ = bar') self.assertEqual(result, '; foo\n# bar') def test_initial_comment_inline_handling(self): result = config._postprocess('[__COMMENTS__]\n' '__SEMICOLON0__ = foo\n' '__INLINE1__ = bar\n' '__INLINE2__ = baz') self.assertEqual(result, '; foo ; bar ; baz') def test_inline_semicolon_comment(self): result = config._postprocess('[__COMMENTS__]\n' '[section]\n' 'foo = bar\n' '__INLINE0__ = baz') self.assertEqual(result, '[section]\nfoo = bar ; baz') def test_no_inline_hash_comment(self): result = config._preprocess('[section]\nfoo = bar # baz') self.assertEqual(result, '[__COMMENTS__]\n' '[section]\n' 'foo = bar # baz') def test_section_extra_text(self): result = config._postprocess('[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar') self.assertEqual(result, '[section] foobar') def test_section_extra_text_inline_semicolon(self): result = config._postprocess('[__COMMENTS__]\n' '[section]\n' '__SECTION0__ = foobar\n' '__INLINE1__ = baz') self.assertEqual(result, '[section] foobar ; baz') def test_conversion(self): result = config._postprocess(PROCESSED_CONFIG) self.assertEqual(result, INPUT_CONFIG) mopidy-0.17.0/tests/config/schemas_test.py000066400000000000000000000075351224420023200205440ustar00rootroot00000000000000from __future__ import unicode_literals import logging import mock import unittest from mopidy.config import schemas from tests import any_unicode class ConfigSchemaTest(unittest.TestCase): def setUp(self): self.schema = schemas.ConfigSchema('test') self.schema['foo'] = mock.Mock() self.schema['bar'] = mock.Mock() self.schema['baz'] = mock.Mock() self.values = {'bar': '123', 'foo': '456', 'baz': '678'} def test_deserialize(self): self.schema.deserialize(self.values) def test_deserialize_with_missing_value(self): del self.values['foo'] result, errors = self.schema.deserialize(self.values) self.assertEqual({'foo': any_unicode}, errors) self.assertIsNone(result.pop('foo')) self.assertIsNotNone(result.pop('bar')) self.assertIsNotNone(result.pop('baz')) self.assertEqual({}, result) def test_deserialize_with_extra_value(self): self.values['extra'] = '123' result, errors = self.schema.deserialize(self.values) self.assertEqual({'extra': any_unicode}, errors) self.assertIsNotNone(result.pop('foo')) self.assertIsNotNone(result.pop('bar')) self.assertIsNotNone(result.pop('baz')) self.assertEqual({}, result) def test_deserialize_with_deserialization_error(self): self.schema['foo'].deserialize.side_effect = ValueError('failure') result, errors = self.schema.deserialize(self.values) self.assertEqual({'foo': 'failure'}, errors) self.assertIsNone(result.pop('foo')) self.assertIsNotNone(result.pop('bar')) self.assertIsNotNone(result.pop('baz')) self.assertEqual({}, result) def test_deserialize_with_multiple_deserialization_errors(self): self.schema['foo'].deserialize.side_effect = ValueError('failure') self.schema['bar'].deserialize.side_effect = ValueError('other') result, errors = self.schema.deserialize(self.values) self.assertEqual({'foo': 'failure', 'bar': 'other'}, errors) self.assertIsNone(result.pop('foo')) self.assertIsNone(result.pop('bar')) self.assertIsNotNone(result.pop('baz')) self.assertEqual({}, result) def test_deserialize_deserialization_unknown_and_missing_errors(self): self.values['extra'] = '123' self.schema['bar'].deserialize.side_effect = ValueError('failure') del self.values['baz'] result, errors = self.schema.deserialize(self.values) self.assertIn('unknown', errors['extra']) self.assertNotIn('foo', errors) self.assertIn('failure', errors['bar']) self.assertIn('not found', errors['baz']) self.assertNotIn('unknown', result) self.assertIn('foo', result) self.assertIsNone(result['bar']) self.assertIsNone(result['baz']) class LogLevelConfigSchemaTest(unittest.TestCase): def test_conversion(self): schema = schemas.LogLevelConfigSchema('test') result, errors = schema.deserialize( {'foo.bar': 'DEBUG', 'baz': 'INFO'}) self.assertEqual(logging.DEBUG, result['foo.bar']) self.assertEqual(logging.INFO, result['baz']) class DidYouMeanTest(unittest.TestCase): def testSuggestoins(self): choices = ('enabled', 'username', 'password', 'bitrate', 'timeout') suggestion = schemas._did_you_mean('bitrate', choices) self.assertEqual(suggestion, 'bitrate') suggestion = schemas._did_you_mean('bitrote', choices) self.assertEqual(suggestion, 'bitrate') suggestion = schemas._did_you_mean('Bitrot', choices) self.assertEqual(suggestion, 'bitrate') suggestion = schemas._did_you_mean('BTROT', choices) self.assertEqual(suggestion, 'bitrate') suggestion = schemas._did_you_mean('btro', choices) self.assertEqual(suggestion, None) mopidy-0.17.0/tests/config/types_test.py000066400000000000000000000336111224420023200202570ustar00rootroot00000000000000# encoding: utf-8 from __future__ import unicode_literals import logging import mock import socket import unittest from mopidy.config import types # TODO: DecodeTest and EncodeTest class ConfigValueTest(unittest.TestCase): def test_deserialize_passes_through(self): value = types.ConfigValue() sentinel = object() self.assertEqual(sentinel, value.deserialize(sentinel)) def test_serialize_conversion_to_string(self): value = types.ConfigValue() self.assertIsInstance(value.serialize(object()), bytes) def test_serialize_none(self): value = types.ConfigValue() result = value.serialize(None) self.assertIsInstance(result, bytes) self.assertEqual(b'', result) def test_serialize_supports_display(self): value = types.ConfigValue() self.assertIsInstance(value.serialize(object(), display=True), bytes) class StringTest(unittest.TestCase): def test_deserialize_conversion_success(self): value = types.String() self.assertEqual('foo', value.deserialize(b' foo ')) self.assertIsInstance(value.deserialize(b'foo'), unicode) def test_deserialize_decodes_utf8(self): value = types.String() result = value.deserialize('æøå'.encode('utf-8')) self.assertEqual('æøå', result) def test_deserialize_does_not_double_encode_unicode(self): value = types.String() result = value.deserialize('æøå') self.assertEqual('æøå', result) def test_deserialize_handles_escapes(self): value = types.String(optional=True) result = value.deserialize(b'a\\t\\nb') self.assertEqual('a\t\nb', result) def test_deserialize_enforces_choices(self): value = types.String(choices=['foo', 'bar', 'baz']) self.assertEqual('foo', value.deserialize(b'foo')) self.assertRaises(ValueError, value.deserialize, b'foobar') def test_deserialize_enforces_required(self): value = types.String() self.assertRaises(ValueError, value.deserialize, b'') def test_deserialize_respects_optional(self): value = types.String(optional=True) self.assertIsNone(value.deserialize(b'')) self.assertIsNone(value.deserialize(b' ')) def test_deserialize_decode_failure(self): value = types.String() incorrectly_encoded_bytes = u'æøå'.encode('iso-8859-1') self.assertRaises( ValueError, value.deserialize, incorrectly_encoded_bytes) def test_serialize_encodes_utf8(self): value = types.String() result = value.serialize('æøå') self.assertIsInstance(result, bytes) self.assertEqual('æøå'.encode('utf-8'), result) def test_serialize_does_not_encode_bytes(self): value = types.String() result = value.serialize('æøå'.encode('utf-8')) self.assertIsInstance(result, bytes) self.assertEqual('æøå'.encode('utf-8'), result) def test_serialize_handles_escapes(self): value = types.String() result = value.serialize('a\n\tb') self.assertIsInstance(result, bytes) self.assertEqual(r'a\n\tb'.encode('utf-8'), result) def test_serialize_none(self): value = types.String() result = value.serialize(None) self.assertIsInstance(result, bytes) self.assertEqual(b'', result) def test_deserialize_enforces_choices_optional(self): value = types.String(optional=True, choices=['foo', 'bar', 'baz']) self.assertEqual(None, value.deserialize(b'')) self.assertRaises(ValueError, value.deserialize, b'foobar') class SecretTest(unittest.TestCase): def test_deserialize_decodes_utf8(self): value = types.Secret() result = value.deserialize('æøå'.encode('utf-8')) self.assertIsInstance(result, unicode) self.assertEqual('æøå', result) def test_deserialize_enforces_required(self): value = types.Secret() self.assertRaises(ValueError, value.deserialize, b'') def test_deserialize_respects_optional(self): value = types.Secret(optional=True) self.assertIsNone(value.deserialize(b'')) self.assertIsNone(value.deserialize(b' ')) def test_serialize_none(self): value = types.Secret() result = value.serialize(None) self.assertIsInstance(result, bytes) self.assertEqual(b'', result) def test_serialize_for_display_masks_value(self): value = types.Secret() result = value.serialize('s3cret', display=True) self.assertIsInstance(result, bytes) self.assertEqual(b'********', result) def test_serialize_none_for_display(self): value = types.Secret() result = value.serialize(None, display=True) self.assertIsInstance(result, bytes) self.assertEqual(b'', result) class IntegerTest(unittest.TestCase): def test_deserialize_conversion_success(self): value = types.Integer() self.assertEqual(123, value.deserialize('123')) self.assertEqual(0, value.deserialize('0')) self.assertEqual(-10, value.deserialize('-10')) def test_deserialize_conversion_failure(self): value = types.Integer() self.assertRaises(ValueError, value.deserialize, 'asd') self.assertRaises(ValueError, value.deserialize, '3.14') self.assertRaises(ValueError, value.deserialize, '') self.assertRaises(ValueError, value.deserialize, ' ') def test_deserialize_enforces_choices(self): value = types.Integer(choices=[1, 2, 3]) self.assertEqual(3, value.deserialize('3')) self.assertRaises(ValueError, value.deserialize, '5') def test_deserialize_enforces_minimum(self): value = types.Integer(minimum=10) self.assertEqual(15, value.deserialize('15')) self.assertRaises(ValueError, value.deserialize, '5') def test_deserialize_enforces_maximum(self): value = types.Integer(maximum=10) self.assertEqual(5, value.deserialize('5')) self.assertRaises(ValueError, value.deserialize, '15') def test_deserialize_respects_optional(self): value = types.Integer(optional=True) self.assertEqual(None, value.deserialize('')) class BooleanTest(unittest.TestCase): def test_deserialize_conversion_success(self): value = types.Boolean() for true in ('1', 'yes', 'true', 'on'): self.assertIs(value.deserialize(true), True) self.assertIs(value.deserialize(true.upper()), True) self.assertIs(value.deserialize(true.capitalize()), True) for false in ('0', 'no', 'false', 'off'): self.assertIs(value.deserialize(false), False) self.assertIs(value.deserialize(false.upper()), False) self.assertIs(value.deserialize(false.capitalize()), False) def test_deserialize_conversion_failure(self): value = types.Boolean() self.assertRaises(ValueError, value.deserialize, 'nope') self.assertRaises(ValueError, value.deserialize, 'sure') self.assertRaises(ValueError, value.deserialize, '') def test_serialize_true(self): value = types.Boolean() result = value.serialize(True) self.assertEqual(b'true', result) self.assertIsInstance(result, bytes) def test_serialize_false(self): value = types.Boolean() result = value.serialize(False) self.assertEqual(b'false', result) self.assertIsInstance(result, bytes) # TODO: test None or other invalid values into serialize? class ListTest(unittest.TestCase): # TODO: add test_deserialize_ignores_blank # TODO: add test_serialize_ignores_blank # TODO: add test_deserialize_handles_escapes def test_deserialize_conversion_success(self): value = types.List() expected = ('foo', 'bar', 'baz') self.assertEqual(expected, value.deserialize(b'foo, bar ,baz ')) expected = ('foo,bar', 'bar', 'baz') self.assertEqual(expected, value.deserialize(b' foo,bar\nbar\nbaz')) def test_deserialize_creates_tuples(self): value = types.List(optional=True) self.assertIsInstance(value.deserialize(b'foo,bar,baz'), tuple) self.assertIsInstance(value.deserialize(b''), tuple) def test_deserialize_decodes_utf8(self): value = types.List() result = value.deserialize('æ, ø, å'.encode('utf-8')) self.assertEqual(('æ', 'ø', 'å'), result) result = value.deserialize('æ\nø\nå'.encode('utf-8')) self.assertEqual(('æ', 'ø', 'å'), result) def test_deserialize_does_not_double_encode_unicode(self): value = types.List() result = value.deserialize('æ, ø, å') self.assertEqual(('æ', 'ø', 'å'), result) result = value.deserialize('æ\nø\nå') self.assertEqual(('æ', 'ø', 'å'), result) def test_deserialize_enforces_required(self): value = types.List() self.assertRaises(ValueError, value.deserialize, b'') def test_deserialize_respects_optional(self): value = types.List(optional=True) self.assertEqual(tuple(), value.deserialize(b'')) def test_serialize(self): value = types.List() result = value.serialize(('foo', 'bar', 'baz')) self.assertIsInstance(result, bytes) self.assertRegexpMatches(result, r'foo\n\s*bar\n\s*baz') class LogLevelTest(unittest.TestCase): levels = {'critical': logging.CRITICAL, 'error': logging.ERROR, 'warning': logging.WARNING, 'info': logging.INFO, 'debug': logging.DEBUG} def test_deserialize_conversion_success(self): value = types.LogLevel() for name, level in self.levels.items(): self.assertEqual(level, value.deserialize(name)) self.assertEqual(level, value.deserialize(name.upper())) self.assertEqual(level, value.deserialize(name.capitalize())) def test_deserialize_conversion_failure(self): value = types.LogLevel() self.assertRaises(ValueError, value.deserialize, 'nope') self.assertRaises(ValueError, value.deserialize, 'sure') self.assertRaises(ValueError, value.deserialize, '') self.assertRaises(ValueError, value.deserialize, ' ') def test_serialize(self): value = types.LogLevel() for name, level in self.levels.items(): self.assertEqual(name, value.serialize(level)) self.assertEqual(b'', value.serialize(1337)) class HostnameTest(unittest.TestCase): @mock.patch('socket.getaddrinfo') def test_deserialize_conversion_success(self, getaddrinfo_mock): value = types.Hostname() value.deserialize('example.com') getaddrinfo_mock.assert_called_once_with('example.com', None) @mock.patch('socket.getaddrinfo') def test_deserialize_conversion_failure(self, getaddrinfo_mock): value = types.Hostname() getaddrinfo_mock.side_effect = socket.error self.assertRaises(ValueError, value.deserialize, 'example.com') @mock.patch('socket.getaddrinfo') def test_deserialize_enforces_required(self, getaddrinfo_mock): value = types.Hostname() self.assertRaises(ValueError, value.deserialize, '') self.assertEqual(0, getaddrinfo_mock.call_count) @mock.patch('socket.getaddrinfo') def test_deserialize_respects_optional(self, getaddrinfo_mock): value = types.Hostname(optional=True) self.assertIsNone(value.deserialize('')) self.assertIsNone(value.deserialize(' ')) self.assertEqual(0, getaddrinfo_mock.call_count) class PortTest(unittest.TestCase): def test_valid_ports(self): value = types.Port() self.assertEqual(0, value.deserialize('0')) self.assertEqual(1, value.deserialize('1')) self.assertEqual(80, value.deserialize('80')) self.assertEqual(6600, value.deserialize('6600')) self.assertEqual(65535, value.deserialize('65535')) def test_invalid_ports(self): value = types.Port() self.assertRaises(ValueError, value.deserialize, '65536') self.assertRaises(ValueError, value.deserialize, '100000') self.assertRaises(ValueError, value.deserialize, '-1') self.assertRaises(ValueError, value.deserialize, '') class ExpandedPathTest(unittest.TestCase): def test_is_bytes(self): self.assertIsInstance(types.ExpandedPath(b'/tmp', b'foo'), bytes) def test_defaults_to_expanded(self): original = b'~' expanded = b'expanded_path' self.assertEqual(expanded, types.ExpandedPath(original, expanded)) @mock.patch('mopidy.utils.path.expand_path') def test_orginal_stores_unexpanded(self, expand_path_mock): original = b'~' expanded = b'expanded_path' result = types.ExpandedPath(original, expanded) self.assertEqual(original, result.original) class PathTest(unittest.TestCase): def test_deserialize_conversion_success(self): result = types.Path().deserialize(b'/foo') self.assertEqual('/foo', result) self.assertIsInstance(result, types.ExpandedPath) self.assertIsInstance(result, bytes) def test_deserialize_enforces_required(self): value = types.Path() self.assertRaises(ValueError, value.deserialize, b'') def test_deserialize_respects_optional(self): value = types.Path(optional=True) self.assertIsNone(value.deserialize(b'')) self.assertIsNone(value.deserialize(b' ')) def test_serialize_uses_original(self): path = types.ExpandedPath(b'original_path', b'expanded_path') value = types.Path() self.assertEqual('expanded_path', path) self.assertEqual('original_path', value.serialize(path)) def test_serialize_plain_string(self): value = types.Path() self.assertEqual('path', value.serialize(b'path')) def test_serialize_unicode_string(self): value = types.Path() self.assertRaises(ValueError, value.serialize, 'æøå') mopidy-0.17.0/tests/config/validator_tests.py000066400000000000000000000045331224420023200212640ustar00rootroot00000000000000from __future__ import unicode_literals import unittest from mopidy.config import validators class ValidateChoiceTest(unittest.TestCase): def test_no_choices_passes(self): validators.validate_choice('foo', None) def test_valid_value_passes(self): validators.validate_choice('foo', ['foo', 'bar', 'baz']) validators.validate_choice(1, [1, 2, 3]) def test_empty_choices_fails(self): self.assertRaises(ValueError, validators.validate_choice, 'foo', []) def test_invalid_value_fails(self): words = ['foo', 'bar', 'baz'] self.assertRaises( ValueError, validators.validate_choice, 'foobar', words) self.assertRaises( ValueError, validators.validate_choice, 5, [1, 2, 3]) class ValidateMinimumTest(unittest.TestCase): def test_no_minimum_passes(self): validators.validate_minimum(10, None) def test_valid_value_passes(self): validators.validate_minimum(10, 5) def test_to_small_value_fails(self): self.assertRaises(ValueError, validators.validate_minimum, 10, 20) def test_to_small_value_fails_with_zero_as_minimum(self): self.assertRaises(ValueError, validators.validate_minimum, -1, 0) class ValidateMaximumTest(unittest.TestCase): def test_no_maximum_passes(self): validators.validate_maximum(5, None) def test_valid_value_passes(self): validators.validate_maximum(5, 10) def test_to_large_value_fails(self): self.assertRaises(ValueError, validators.validate_maximum, 10, 5) def test_to_large_value_fails_with_zero_as_maximum(self): self.assertRaises(ValueError, validators.validate_maximum, 5, 0) class ValidateRequiredTest(unittest.TestCase): def test_passes_when_false(self): validators.validate_required('foo', False) validators.validate_required('', False) validators.validate_required(' ', False) validators.validate_required([], False) def test_passes_when_required_and_set(self): validators.validate_required('foo', True) validators.validate_required(' foo ', True) validators.validate_required([1], True) def test_blocks_when_required_and_emtpy(self): self.assertRaises(ValueError, validators.validate_required, '', True) self.assertRaises(ValueError, validators.validate_required, [], True) mopidy-0.17.0/tests/core/000077500000000000000000000000001224420023200151615ustar00rootroot00000000000000mopidy-0.17.0/tests/core/__init__.py000066400000000000000000000000501224420023200172650ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/tests/core/actor_test.py000066400000000000000000000021731224420023200177050ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest import pykka from mopidy.core import Core class CoreActorTest(unittest.TestCase): def setUp(self): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.core = Core(audio=None, backends=[self.backend1, self.backend2]) def tearDown(self): pykka.ActorRegistry.stop_all() def test_uri_schemes_has_uris_from_all_backends(self): result = self.core.uri_schemes self.assertIn('dummy1', result) self.assertIn('dummy2', result) def test_backends_with_colliding_uri_schemes_fails(self): self.backend1.__class__.__name__ = b'B1' self.backend2.__class__.__name__ = b'B2' self.backend2.uri_schemes.get.return_value = ['dummy1', 'dummy2'] self.assertRaisesRegexp( AssertionError, 'Cannot add URI scheme dummy1 for B2, it is already handled by B1', Core, audio=None, backends=[self.backend1, self.backend2]) mopidy-0.17.0/tests/core/events_test.py000066400000000000000000000130641224420023200201020ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest import pykka from mopidy import core from mopidy.backends import dummy from mopidy.models import Track @mock.patch.object(core.CoreListener, 'send') class BackendEventsTest(unittest.TestCase): def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() def tearDown(self): pykka.ActorRegistry.stop_all() def test_backends_playlists_loaded_forwards_event_to_frontends(self, send): self.core.playlists_loaded().get() self.assertEqual(send.call_args[0][0], 'playlists_loaded') def test_pause_sends_track_playback_paused_event(self, send): tl_tracks = self.core.tracklist.add([Track(uri='dummy:a')]).get() self.core.playback.play().get() send.reset_mock() self.core.playback.pause().get() self.assertEqual(send.call_args[0][0], 'track_playback_paused') self.assertEqual(send.call_args[1]['tl_track'], tl_tracks[0]) self.assertEqual(send.call_args[1]['time_position'], 0) def test_resume_sends_track_playback_resumed(self, send): tl_tracks = self.core.tracklist.add([Track(uri='dummy:a')]).get() self.core.playback.play() self.core.playback.pause().get() send.reset_mock() self.core.playback.resume().get() self.assertEqual(send.call_args[0][0], 'track_playback_resumed') self.assertEqual(send.call_args[1]['tl_track'], tl_tracks[0]) self.assertEqual(send.call_args[1]['time_position'], 0) def test_play_sends_track_playback_started_event(self, send): tl_tracks = self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() self.core.playback.play().get() self.assertEqual(send.call_args[0][0], 'track_playback_started') self.assertEqual(send.call_args[1]['tl_track'], tl_tracks[0]) def test_stop_sends_track_playback_ended_event(self, send): tl_tracks = self.core.tracklist.add([Track(uri='dummy:a')]).get() self.core.playback.play().get() send.reset_mock() self.core.playback.stop().get() self.assertEqual(send.call_args_list[0][0][0], 'track_playback_ended') self.assertEqual(send.call_args_list[0][1]['tl_track'], tl_tracks[0]) self.assertEqual(send.call_args_list[0][1]['time_position'], 0) def test_seek_sends_seeked_event(self, send): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play().get() send.reset_mock() self.core.playback.seek(1000).get() self.assertEqual(send.call_args[0][0], 'seeked') self.assertEqual(send.call_args[1]['time_position'], 1000) def test_tracklist_add_sends_tracklist_changed_event(self, send): send.reset_mock() self.core.tracklist.add([Track(uri='dummy:a')]).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_clear_sends_tracklist_changed_event(self, send): self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() self.core.tracklist.clear().get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_move_sends_tracklist_changed_event(self, send): self.core.tracklist.add( [Track(uri='dummy:a'), Track(uri='dummy:b')]).get() send.reset_mock() self.core.tracklist.move(0, 1, 1).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_remove_sends_tracklist_changed_event(self, send): self.core.tracklist.add([Track(uri='dummy:a')]).get() send.reset_mock() self.core.tracklist.remove(uri=['dummy:a']).get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_tracklist_shuffle_sends_tracklist_changed_event(self, send): self.core.tracklist.add( [Track(uri='dummy:a'), Track(uri='dummy:b')]).get() send.reset_mock() self.core.tracklist.shuffle().get() self.assertEqual(send.call_args[0][0], 'tracklist_changed') def test_playlists_refresh_sends_playlists_loaded_event(self, send): send.reset_mock() self.core.playlists.refresh().get() self.assertEqual(send.call_args[0][0], 'playlists_loaded') def test_playlists_refresh_uri_sends_playlists_loaded_event(self, send): send.reset_mock() self.core.playlists.refresh(uri_scheme='dummy').get() self.assertEqual(send.call_args[0][0], 'playlists_loaded') def test_playlists_create_sends_playlist_changed_event(self, send): send.reset_mock() self.core.playlists.create('foo').get() self.assertEqual(send.call_args[0][0], 'playlist_changed') @unittest.SkipTest def test_playlists_delete_sends_playlist_deleted_event(self, send): # TODO We should probably add a playlist_deleted event pass def test_playlists_save_sends_playlist_changed_event(self, send): playlist = self.core.playlists.create('foo').get() playlist = playlist.copy(name='bar') send.reset_mock() self.core.playlists.save(playlist).get() self.assertEqual(send.call_args[0][0], 'playlist_changed') def test_set_volume_sends_volume_changed_event(self, send): self.core.playback.set_volume(10).get() send.reset_mock() self.core.playback.set_volume(20).get() self.assertEqual(send.call_args[0][0], 'volume_changed') self.assertEqual(send.call_args[1]['volume'], 20) mopidy-0.17.0/tests/core/library_test.py000066400000000000000000000206421224420023200202420ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.backends import base from mopidy.core import Core from mopidy.models import SearchResult, Track class CoreLibraryTest(unittest.TestCase): def setUp(self): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.library1 = mock.Mock(spec=base.BaseLibraryProvider) self.backend1.library = self.library1 self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.library2 = mock.Mock(spec=base.BaseLibraryProvider) self.backend2.library = self.library2 # A backend without the optional library provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.has_library().get.return_value = False self.core = Core(audio=None, backends=[ self.backend1, self.backend2, self.backend3]) def test_lookup_selects_dummy1_backend(self): self.core.library.lookup('dummy1:a') self.library1.lookup.assert_called_once_with('dummy1:a') self.assertFalse(self.library2.lookup.called) def test_lookup_selects_dummy2_backend(self): self.core.library.lookup('dummy2:a') self.assertFalse(self.library1.lookup.called) self.library2.lookup.assert_called_once_with('dummy2:a') def test_lookup_returns_nothing_for_dummy3_track(self): result = self.core.library.lookup('dummy3:a') self.assertEqual(result, []) self.assertFalse(self.library1.lookup.called) self.assertFalse(self.library2.lookup.called) def test_refresh_with_uri_selects_dummy1_backend(self): self.core.library.refresh('dummy1:a') self.library1.refresh.assert_called_once_with('dummy1:a') self.assertFalse(self.library2.refresh.called) def test_refresh_with_uri_selects_dummy2_backend(self): self.core.library.refresh('dummy2:a') self.assertFalse(self.library1.refresh.called) self.library2.refresh.assert_called_once_with('dummy2:a') def test_refresh_with_uri_fails_silently_for_dummy3_uri(self): self.core.library.refresh('dummy3:a') self.assertFalse(self.library1.refresh.called) self.assertFalse(self.library2.refresh.called) def test_refresh_without_uri_calls_all_backends(self): self.core.library.refresh() self.library1.refresh.assert_called_once_with(None) self.library2.refresh.assert_called_once_with(None) def test_find_exact_combines_results_from_all_backends(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') result1 = SearchResult(tracks=[track1]) result2 = SearchResult(tracks=[track2]) self.library1.find_exact().get.return_value = result1 self.library1.find_exact.reset_mock() self.library2.find_exact().get.return_value = result2 self.library2.find_exact.reset_mock() result = self.core.library.find_exact(any=['a']) self.assertIn(result1, result) self.assertIn(result2, result) self.library1.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) def test_find_exact_with_uris_selects_dummy1_backend(self): self.core.library.find_exact( any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy3:']) self.library1.find_exact.assert_called_once_with( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) self.assertFalse(self.library2.find_exact.called) def test_find_exact_with_uris_selects_both_backends(self): self.core.library.find_exact( any=['a'], uris=['dummy1:', 'dummy1:foo', 'dummy2:']) self.library1.find_exact.assert_called_once_with( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) self.library2.find_exact.assert_called_once_with( query=dict(any=['a']), uris=['dummy2:']) def test_find_exact_filters_out_none(self): track1 = Track(uri='dummy1:a') result1 = SearchResult(tracks=[track1]) self.library1.find_exact().get.return_value = result1 self.library1.find_exact.reset_mock() self.library2.find_exact().get.return_value = None self.library2.find_exact.reset_mock() result = self.core.library.find_exact(any=['a']) self.assertIn(result1, result) self.assertNotIn(None, result) self.library1.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) def test_find_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') result1 = SearchResult(tracks=[track1]) result2 = SearchResult(tracks=[track2]) self.library1.find_exact().get.return_value = result1 self.library1.find_exact.reset_mock() self.library2.find_exact().get.return_value = result2 self.library2.find_exact.reset_mock() result = self.core.library.find_exact(dict(any=['a'])) self.assertIn(result1, result) self.assertIn(result2, result) self.library1.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.find_exact.assert_called_once_with( query=dict(any=['a']), uris=None) def test_search_combines_results_from_all_backends(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') result1 = SearchResult(tracks=[track1]) result2 = SearchResult(tracks=[track2]) self.library1.search().get.return_value = result1 self.library1.search.reset_mock() self.library2.search().get.return_value = result2 self.library2.search.reset_mock() result = self.core.library.search(any=['a']) self.assertIn(result1, result) self.assertIn(result2, result) self.library1.search.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.search.assert_called_once_with( query=dict(any=['a']), uris=None) def test_search_with_uris_selects_dummy1_backend(self): self.core.library.search( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy3:']) self.library1.search.assert_called_once_with( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) self.assertFalse(self.library2.search.called) def test_search_with_uris_selects_both_backends(self): self.core.library.search( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo', 'dummy2:']) self.library1.search.assert_called_once_with( query=dict(any=['a']), uris=['dummy1:', 'dummy1:foo']) self.library2.search.assert_called_once_with( query=dict(any=['a']), uris=['dummy2:']) def test_search_filters_out_none(self): track1 = Track(uri='dummy1:a') result1 = SearchResult(tracks=[track1]) self.library1.search().get.return_value = result1 self.library1.search.reset_mock() self.library2.search().get.return_value = None self.library2.search.reset_mock() result = self.core.library.search(any=['a']) self.assertIn(result1, result) self.assertNotIn(None, result) self.library1.search.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.search.assert_called_once_with( query=dict(any=['a']), uris=None) def test_search_accepts_query_dict_instead_of_kwargs(self): track1 = Track(uri='dummy1:a') track2 = Track(uri='dummy2:a') result1 = SearchResult(tracks=[track1]) result2 = SearchResult(tracks=[track2]) self.library1.search().get.return_value = result1 self.library1.search.reset_mock() self.library2.search().get.return_value = result2 self.library2.search.reset_mock() result = self.core.library.search(dict(any=['a'])) self.assertIn(result1, result) self.assertIn(result2, result) self.library1.search.assert_called_once_with( query=dict(any=['a']), uris=None) self.library2.search.assert_called_once_with( query=dict(any=['a']), uris=None) mopidy-0.17.0/tests/core/listener_test.py000066400000000000000000000037721224420023200204300ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.core import CoreListener, PlaybackState from mopidy.models import Playlist, TlTrack class CoreListenerTest(unittest.TestCase): def setUp(self): self.listener = CoreListener() def test_on_event_forwards_to_specific_handler(self): self.listener.track_playback_paused = mock.Mock() self.listener.on_event( 'track_playback_paused', track=TlTrack(), position=0) self.listener.track_playback_paused.assert_called_with( track=TlTrack(), position=0) def test_listener_has_default_impl_for_track_playback_paused(self): self.listener.track_playback_paused(TlTrack(), 0) def test_listener_has_default_impl_for_track_playback_resumed(self): self.listener.track_playback_resumed(TlTrack(), 0) def test_listener_has_default_impl_for_track_playback_started(self): self.listener.track_playback_started(TlTrack()) def test_listener_has_default_impl_for_track_playback_ended(self): self.listener.track_playback_ended(TlTrack(), 0) def test_listener_has_default_impl_for_playback_state_changed(self): self.listener.playback_state_changed( PlaybackState.STOPPED, PlaybackState.PLAYING) def test_listener_has_default_impl_for_tracklist_changed(self): self.listener.tracklist_changed() def test_listener_has_default_impl_for_playlists_loaded(self): self.listener.playlists_loaded() def test_listener_has_default_impl_for_playlist_changed(self): self.listener.playlist_changed(Playlist()) def test_listener_has_default_impl_for_options_changed(self): self.listener.options_changed() def test_listener_has_default_impl_for_volume_changed(self): self.listener.volume_changed(70) def test_listener_has_default_impl_for_mute_changed(self): self.listener.mute_changed(True) def test_listener_has_default_impl_for_seeked(self): self.listener.seeked(0) mopidy-0.17.0/tests/core/playback_test.py000066400000000000000000000154661224420023200203740ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.backends import base from mopidy.core import Core, PlaybackState from mopidy.models import Track class CorePlaybackTest(unittest.TestCase): def setUp(self): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.playback1 = mock.Mock(spec=base.BasePlaybackProvider) self.backend1.playback = self.playback1 self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.playback2 = mock.Mock(spec=base.BasePlaybackProvider) self.backend2.playback = self.playback2 # A backend without the optional playback provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.has_playback().get.return_value = False self.tracks = [ Track(uri='dummy1:a', length=40000), Track(uri='dummy2:a', length=40000), Track(uri='dummy3:a', length=40000), # Unplayable Track(uri='dummy1:b', length=40000), ] self.core = Core(audio=None, backends=[ self.backend1, self.backend2, self.backend3]) self.core.tracklist.add(self.tracks) self.tl_tracks = self.core.tracklist.tl_tracks self.unplayable_tl_track = self.tl_tracks[2] def test_play_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.playback1.play.assert_called_once_with(self.tracks[0]) self.assertFalse(self.playback2.play.called) def test_play_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.assertFalse(self.playback1.play.called) self.playback2.play.assert_called_once_with(self.tracks[1]) def test_play_skips_to_next_on_unplayable_track(self): self.core.playback.play(self.unplayable_tl_track) self.playback1.play.assert_called_once_with(self.tracks[3]) self.assertFalse(self.playback2.play.called) self.assertEqual( self.core.playback.current_tl_track, self.tl_tracks[3]) def test_pause_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.core.playback.pause() self.playback1.pause.assert_called_once_with() self.assertFalse(self.playback2.pause.called) def test_pause_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.core.playback.pause() self.assertFalse(self.playback1.pause.called) self.playback2.pause.assert_called_once_with() def test_pause_changes_state_even_if_track_is_unplayable(self): self.core.playback.current_tl_track = self.unplayable_tl_track self.core.playback.pause() self.assertEqual(self.core.playback.state, PlaybackState.PAUSED) self.assertFalse(self.playback1.pause.called) self.assertFalse(self.playback2.pause.called) def test_resume_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.core.playback.pause() self.core.playback.resume() self.playback1.resume.assert_called_once_with() self.assertFalse(self.playback2.resume.called) def test_resume_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.core.playback.pause() self.core.playback.resume() self.assertFalse(self.playback1.resume.called) self.playback2.resume.assert_called_once_with() def test_resume_does_nothing_if_track_is_unplayable(self): self.core.playback.current_tl_track = self.unplayable_tl_track self.core.playback.state = PlaybackState.PAUSED self.core.playback.resume() self.assertEqual(self.core.playback.state, PlaybackState.PAUSED) self.assertFalse(self.playback1.resume.called) self.assertFalse(self.playback2.resume.called) def test_stop_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.core.playback.stop() self.playback1.stop.assert_called_once_with() self.assertFalse(self.playback2.stop.called) def test_stop_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.core.playback.stop() self.assertFalse(self.playback1.stop.called) self.playback2.stop.assert_called_once_with() def test_stop_changes_state_even_if_track_is_unplayable(self): self.core.playback.current_tl_track = self.unplayable_tl_track self.core.playback.state = PlaybackState.PAUSED self.core.playback.stop() self.assertEqual(self.core.playback.state, PlaybackState.STOPPED) self.assertFalse(self.playback1.stop.called) self.assertFalse(self.playback2.stop.called) def test_seek_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.core.playback.seek(10000) self.playback1.seek.assert_called_once_with(10000) self.assertFalse(self.playback2.seek.called) def test_seek_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.core.playback.seek(10000) self.assertFalse(self.playback1.seek.called) self.playback2.seek.assert_called_once_with(10000) def test_seek_fails_for_unplayable_track(self): self.core.playback.current_tl_track = self.unplayable_tl_track self.core.playback.state = PlaybackState.PLAYING success = self.core.playback.seek(1000) self.assertFalse(success) self.assertFalse(self.playback1.seek.called) self.assertFalse(self.playback2.seek.called) def test_time_position_selects_dummy1_backend(self): self.core.playback.play(self.tl_tracks[0]) self.core.playback.seek(10000) self.core.playback.time_position self.playback1.get_time_position.assert_called_once_with() self.assertFalse(self.playback2.get_time_position.called) def test_time_position_selects_dummy2_backend(self): self.core.playback.play(self.tl_tracks[1]) self.core.playback.seek(10000) self.core.playback.time_position self.assertFalse(self.playback1.get_time_position.called) self.playback2.get_time_position.assert_called_once_with() def test_time_position_returns_0_if_track_is_unplayable(self): self.core.playback.current_tl_track = self.unplayable_tl_track result = self.core.playback.time_position self.assertEqual(result, 0) self.assertFalse(self.playback1.get_time_position.called) self.assertFalse(self.playback2.get_time_position.called) def test_mute(self): self.assertEqual(self.core.playback.mute, False) self.core.playback.mute = True self.assertEqual(self.core.playback.mute, True) mopidy-0.17.0/tests/core/playlists_test.py000066400000000000000000000172561224420023200206310ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.backends import base from mopidy.core import Core from mopidy.models import Playlist, Track class PlaylistsTest(unittest.TestCase): def setUp(self): self.backend1 = mock.Mock() self.backend1.uri_schemes.get.return_value = ['dummy1'] self.sp1 = mock.Mock(spec=base.BasePlaylistsProvider) self.backend1.playlists = self.sp1 self.backend2 = mock.Mock() self.backend2.uri_schemes.get.return_value = ['dummy2'] self.sp2 = mock.Mock(spec=base.BasePlaylistsProvider) self.backend2.playlists = self.sp2 # A backend without the optional playlists provider self.backend3 = mock.Mock() self.backend3.uri_schemes.get.return_value = ['dummy3'] self.backend3.has_playlists().get.return_value = False self.backend3.playlists = None self.pl1a = Playlist(name='A', tracks=[Track(uri='dummy1:a')]) self.pl1b = Playlist(name='B', tracks=[Track(uri='dummy1:b')]) self.sp1.playlists.get.return_value = [self.pl1a, self.pl1b] self.pl2a = Playlist(name='A', tracks=[Track(uri='dummy2:a')]) self.pl2b = Playlist(name='B', tracks=[Track(uri='dummy2:b')]) self.sp2.playlists.get.return_value = [self.pl2a, self.pl2b] self.core = Core(audio=None, backends=[ self.backend3, self.backend1, self.backend2]) def test_get_playlists_combines_result_from_backends(self): result = self.core.playlists.playlists self.assertIn(self.pl1a, result) self.assertIn(self.pl1b, result) self.assertIn(self.pl2a, result) self.assertIn(self.pl2b, result) def test_get_playlists_includes_tracks_by_default(self): result = self.core.playlists.get_playlists() self.assertEqual(result[0].name, 'A') self.assertEqual(len(result[0].tracks), 1) self.assertEqual(result[1].name, 'B') self.assertEqual(len(result[1].tracks), 1) def test_get_playlist_can_strip_tracks_from_returned_playlists(self): result = self.core.playlists.get_playlists(include_tracks=False) self.assertEqual(result[0].name, 'A') self.assertEqual(len(result[0].tracks), 0) self.assertEqual(result[1].name, 'B') self.assertEqual(len(result[1].tracks), 0) def test_create_without_uri_scheme_uses_first_backend(self): playlist = Playlist() self.sp1.create().get.return_value = playlist self.sp1.reset_mock() result = self.core.playlists.create('foo') self.assertEqual(playlist, result) self.sp1.create.assert_called_once_with('foo') self.assertFalse(self.sp2.create.called) def test_create_with_uri_scheme_selects_the_matching_backend(self): playlist = Playlist() self.sp2.create().get.return_value = playlist self.sp2.reset_mock() result = self.core.playlists.create('foo', uri_scheme='dummy2') self.assertEqual(playlist, result) self.assertFalse(self.sp1.create.called) self.sp2.create.assert_called_once_with('foo') def test_create_with_unsupported_uri_scheme_uses_first_backend(self): playlist = Playlist() self.sp1.create().get.return_value = playlist self.sp1.reset_mock() result = self.core.playlists.create('foo', uri_scheme='dummy3') self.assertEqual(playlist, result) self.sp1.create.assert_called_once_with('foo') self.assertFalse(self.sp2.create.called) def test_delete_selects_the_dummy1_backend(self): self.core.playlists.delete('dummy1:a') self.sp1.delete.assert_called_once_with('dummy1:a') self.assertFalse(self.sp2.delete.called) def test_delete_selects_the_dummy2_backend(self): self.core.playlists.delete('dummy2:a') self.assertFalse(self.sp1.delete.called) self.sp2.delete.assert_called_once_with('dummy2:a') def test_delete_with_unknown_uri_scheme_does_nothing(self): self.core.playlists.delete('unknown:a') self.assertFalse(self.sp1.delete.called) self.assertFalse(self.sp2.delete.called) def test_delete_ignores_backend_without_playlist_support(self): self.core.playlists.delete('dummy3:a') self.assertFalse(self.sp1.delete.called) self.assertFalse(self.sp2.delete.called) def test_filter_returns_matching_playlists(self): result = self.core.playlists.filter(name='A') self.assertEqual(2, len(result)) def test_filter_accepts_dict_instead_of_kwargs(self): result = self.core.playlists.filter({'name': 'A'}) self.assertEqual(2, len(result)) def test_lookup_selects_the_dummy1_backend(self): self.core.playlists.lookup('dummy1:a') self.sp1.lookup.assert_called_once_with('dummy1:a') self.assertFalse(self.sp2.lookup.called) def test_lookup_selects_the_dummy2_backend(self): self.core.playlists.lookup('dummy2:a') self.assertFalse(self.sp1.lookup.called) self.sp2.lookup.assert_called_once_with('dummy2:a') def test_lookup_track_in_backend_without_playlists_fails(self): result = self.core.playlists.lookup('dummy3:a') self.assertIsNone(result) self.assertFalse(self.sp1.lookup.called) self.assertFalse(self.sp2.lookup.called) def test_refresh_without_uri_scheme_refreshes_all_backends(self): self.core.playlists.refresh() self.sp1.refresh.assert_called_once_with() self.sp2.refresh.assert_called_once_with() def test_refresh_with_uri_scheme_refreshes_matching_backend(self): self.core.playlists.refresh(uri_scheme='dummy2') self.assertFalse(self.sp1.refresh.called) self.sp2.refresh.assert_called_once_with() def test_refresh_with_unknown_uri_scheme_refreshes_nothing(self): self.core.playlists.refresh(uri_scheme='foobar') self.assertFalse(self.sp1.refresh.called) self.assertFalse(self.sp2.refresh.called) def test_refresh_ignores_backend_without_playlist_support(self): self.core.playlists.refresh(uri_scheme='dummy3') self.assertFalse(self.sp1.refresh.called) self.assertFalse(self.sp2.refresh.called) def test_save_selects_the_dummy1_backend(self): playlist = Playlist(uri='dummy1:a') self.sp1.save().get.return_value = playlist self.sp1.reset_mock() result = self.core.playlists.save(playlist) self.assertEqual(playlist, result) self.sp1.save.assert_called_once_with(playlist) self.assertFalse(self.sp2.save.called) def test_save_selects_the_dummy2_backend(self): playlist = Playlist(uri='dummy2:a') self.sp2.save().get.return_value = playlist self.sp2.reset_mock() result = self.core.playlists.save(playlist) self.assertEqual(playlist, result) self.assertFalse(self.sp1.save.called) self.sp2.save.assert_called_once_with(playlist) def test_save_does_nothing_if_playlist_uri_is_unset(self): result = self.core.playlists.save(Playlist()) self.assertIsNone(result) self.assertFalse(self.sp1.save.called) self.assertFalse(self.sp2.save.called) def test_save_does_nothing_if_playlist_uri_has_unknown_scheme(self): result = self.core.playlists.save(Playlist(uri='foobar:a')) self.assertIsNone(result) self.assertFalse(self.sp1.save.called) self.assertFalse(self.sp2.save.called) def test_save_ignores_backend_without_playlist_support(self): result = self.core.playlists.save(Playlist(uri='dummy3:a')) self.assertIsNone(result) self.assertFalse(self.sp1.save.called) self.assertFalse(self.sp2.save.called) mopidy-0.17.0/tests/core/tracklist_test.py000066400000000000000000000052771224420023200206050ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.backends import base from mopidy.core import Core from mopidy.models import Track class TracklistTest(unittest.TestCase): def setUp(self): self.tracks = [ Track(uri='dummy1:a', name='foo'), Track(uri='dummy1:b', name='foo'), Track(uri='dummy1:c', name='bar'), ] self.backend = mock.Mock() self.backend.uri_schemes.get.return_value = ['dummy1'] self.library = mock.Mock(spec=base.BaseLibraryProvider) self.backend.library = self.library self.core = Core(audio=None, backends=[self.backend]) self.tl_tracks = self.core.tracklist.add(self.tracks) def test_add_by_uri_looks_up_uri_in_library(self): track = Track(uri='dummy1:x', name='x') self.library.lookup().get.return_value = [track] self.library.lookup.reset_mock() tl_tracks = self.core.tracklist.add(uri='dummy1:x') self.library.lookup.assert_called_once_with('dummy1:x') self.assertEqual(1, len(tl_tracks)) self.assertEqual(track, tl_tracks[0].track) self.assertEqual(tl_tracks, self.core.tracklist.tl_tracks[-1:]) def test_remove_removes_tl_tracks_matching_query(self): tl_tracks = self.core.tracklist.remove(name=['foo']) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) self.assertEqual(1, self.core.tracklist.length) self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks) def test_remove_works_with_dict_instead_of_kwargs(self): tl_tracks = self.core.tracklist.remove({'name': ['foo']}) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) self.assertEqual(1, self.core.tracklist.length) self.assertListEqual(self.tl_tracks[2:], self.core.tracklist.tl_tracks) def test_filter_returns_tl_tracks_matching_query(self): tl_tracks = self.core.tracklist.filter(name=['foo']) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) def test_filter_works_with_dict_instead_of_kwargs(self): tl_tracks = self.core.tracklist.filter({'name': ['foo']}) self.assertEqual(2, len(tl_tracks)) self.assertListEqual(self.tl_tracks[:2], tl_tracks) def test_filter_fails_if_values_isnt_iterable(self): self.assertRaises(ValueError, self.core.tracklist.filter, tlid=3) def test_filter_fails_if_values_is_a_string(self): self.assertRaises(ValueError, self.core.tracklist.filter, uri='a') # TODO Extract tracklist tests from the base backend tests mopidy-0.17.0/tests/data/000077500000000000000000000000001224420023200151425ustar00rootroot00000000000000mopidy-0.17.0/tests/data/.blank.mp3000066400000000000000000000222201224420023200167260ustar00rootroot00000000000000ID3vTIT2titleTPE1artistTALBalbumTDRC2010TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtitle artist album 2010 mopidy-0.17.0/tests/data/.hidden/000077500000000000000000000000001224420023200164535ustar00rootroot00000000000000mopidy-0.17.0/tests/data/.hidden/.gitignore000066400000000000000000000000001224420023200204310ustar00rootroot00000000000000mopidy-0.17.0/tests/data/advanced_tag_cache000066400000000000000000000031341224420023200206110ustar00rootroot00000000000000info_begin mpd_version: 0.14.2 fs_charset: UTF-8 info_end directory: subdir1 begin: subdir1 directory: subsubdir begin: subdir1/subsubdir songList begin key: song8.mp3 file: subdir1/subsubdir/song8.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 key: song9.mp3 file: subdir1/subsubdir/song9.mp3 Time: 4 Artist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 songList end end: subdir1/subsubdir songList begin key: song4.mp3 file: subdir1/song4.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 key: song5.mp3 file: subdir1/song5.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 songList end end: subdir1 directory: subdir2 begin: subdir2 songList begin key: song6.mp3 file: subdir2/song6.mp3 Time: 4 Artist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 key: song7.mp3 file: subdir2/song7.mp3 Time: 4 Artist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 songList end end: subdir2 songList begin key: song1.mp3 file: /song1.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 key: song2.mp3 file: /song2.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 key: song3.mp3 file: /song3.mp3 Time: 4 Artist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 songList end mopidy-0.17.0/tests/data/albumartist_tag_cache000066400000000000000000000003621224420023200213730ustar00rootroot00000000000000info_begin mpd_version: 0.14.2 fs_charset: UTF-8 info_end songList begin key: song1.mp3 file: /song1.mp3 Time: 4 Artist: name Title: trackname Album: albumname AlbumArtist: albumartistname Track: 1/2 Date: 2006 mtime: 1272319626 songList end mopidy-0.17.0/tests/data/blank.flac000066400000000000000000000345431224420023200170710ustar00rootroot00000000000000fLaC"pl&>r3( reference libFLAC 1.2.1 20070917 ?~=ӻ/w?o~gfzs>g9gysݹϿ33]9?^ztϥ.ɜ>|3f۝~wMMs{^?gsY󛞶M>}IO/|Od&{399f7_>o$/?ϖ3˙M=̳>O'$s'Zug{/ϧ%w?ξ{z{\}9w2w?7<~y˯?ys?';s>u}׿Ϲss+93?+l9=ɧo>N\|UL?r_s_m_wy6???_tܷouzs&tͺynow2r5s濓漥_yNi=3j/9^<=?|g>yϒ-'Nߙ{_>~?O5|_/>o3f_s?s<\_ӟgs;?S﹙?'{s?:'3yO3<ϻ>Y߽vgY<Z~?ӿ~lܛOw9L$_Ogrrϛ^S^͟$?>O}/w|3OL?=N>K|ys?'og~y~fݟ?7+Lӿ9}OgO%w_}ɓgy}'?9>_g|*|?Iw>jsrs&W'g۾s_[<-Oגs<<9)|3em>[>Oϓ%45gϷ7Lw~O'.:>3wS9yW?s]?o?}62Lv˧yyfRK'|3͟sʗ6?O7߳}ygy=/?e?i>[}fWylOOzM}穻9jgwv99tLJwy~:~~||[yoӓ???k>ӼM_w?o&sy'ݖ3Ͽd?~n&/w=>zL/ϝOۓ_;}~y}'?9?e|;ߟ9Y?'2rwwI}Ot%g3yg?}?^k'}6gI[sK\g3?uK?d]~~'߿fwf缟WsϟOryfӜ7\gɧ?|Ͽ?7-ϙ94i|:}湙|~5^}͛[sOoo-O=r|u?OI$ig$;{&w'9~ue?9>W|w3wyL\'MwϤgs.}yy3d?}?mg'?ڒVz?%y[K6O)9g<>y_~w-3ww'lf2e{_?{OϗvOv~|~7y$ϟӟ秜wyw|}7<}d۵?L~g=K'3O7w=߹|es<}w|wsOV~o5t'?r+eYvO92~y~Y7e|9=f}y̟7<{|yo}{^f<ݞso9r65?NNo'4mM{eIMs[?_Sg$e]^o9{z~Il|?>߿y|<92O\~I;>~|3濺~3}yJ~dLgyܿ<_߼oZ~w?_<>gʛo~[O+gܿ}?oRdߓ3yNg}Y䟟_|s&%O7n}ϙ?_2noY>?ܙ^Լ?rm矹~o3/ng:O>}|~stw79'4gɛ{9O3w6[s3RuOgww?rJYly?i3_lf?˼ϓ?ݜ_w5~_ϟ?vw?;s3|vwϛy?Ov97loϓ俞yO̜9O%7d˟>?-Ϸ}>O??ܟgng?fϳ}3r_s~}^j~of&{s읝/gIΟ;?&N~=~{r~Ϝ)v?RsM?/ts|d>MoϜ?}Ҧwi4N2翙3?ϳΖyolg:'O˖|?_=?7ϙ*~ɓgϞo$y3do&{}3{Ϫ\<{Ϝ?OS9۟>w?/ygwy3>ϼs̗}?|?O|Ӿly{8ssy-rN\Y;黺e|Κyym|Ԟg;/??'oMoLsZgK~yO矿g^~>\s^g?y~]K?m?fnsyϞIL?i-y˿3?ϳ%fy~=zr|{;K糥o3J{w_~Wh]t jUVJUUWJTꪺꪪJRJ]TUU.U}+*R*UTtWJUIUUtvjUUI|U_UUUU*RU+UWTUUU*T˥UJ.UJUjRꫪꪫꪕꮥU*UUUګ_UUuTRꪩuUiUUKU*UU*UJUTUjRJ]]}TUJWJUUUUUUJU_TTWU%UUUUU]JZUU*]UZJU*ժzTRJZIUIJ)tUUuW)JJUZK%UwuU)uJRUjUWWJ*RTWTUUUJU]}uJJJU*UUUU*IwUW_UU֥RU]U]UUWTU]]UUU*.R.wU]UZ]UU*UUW]RTJ_UUUԹUrJJ*ꪤW}%UJJUjUuURUUU*UUW]RU*ꪥW}TU*J]UҪUUuU]WU]TԾUVIuUKTRKKWRWUWUKꪥ)WZuIWuuUUUuUUIUZU]TU)WuRꪪWU]vUK]*Pmopidy-0.17.0/tests/data/blank.mp3000066400000000000000000000222201224420023200166500ustar00rootroot00000000000000ID3vTIT2titleTPE1artistTALBalbumTDRC2010TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtitle artist album 2010 mopidy-0.17.0/tests/data/blank.ogg000066400000000000000000000207371224420023200167400ustar00rootroot00000000000000OggSb~|vorbis@@OggSb~X -vorbisXiph.Org libVorbis I 20090709vorbisBCV R!%SJcRR)cP[Gc9F!dSI{O*XJRX)ESLSIR)EcSH!S1esKI %lMtKc1FcZJc1EcRRIs:f%d:Fb|0:B(R-[S-KiasJjc1S(АU@BCV P EQАU@EqqG$BCV@((#IdYeYy/.FuL*CCc3C LcN4 23Ő2[,.!+(b 9dR"瘔NJQ(K[1Q(eBŌRT@@PhȊ 0)B)s1 1 d)NJ圓Ic1sNJrIɤ`!"0HY&gꉢZgigj뚪ʖ癦gꙦ꺦l.jۮkl+ʺʶKgnkۚjʮm-l,fl-,ڶ*˺/n,lںk,*˾1۶˺.'뙪몮k۪ںl-+۪,+˶,+ۦʲ*˾ʲn,*1̶*˺ʲn nʲ ˬۺ1﫲-, 2>ct]_WmYV}cuaYm[][gn nʭ ˲ڶ̺,.|[ڶ麺nʲ˺.uWF}ն}_e߷_ið,k/뺰/,m+0ۺܾ, ˪۾ҵue}+ p0 "@r)b BB*cR2dI)JIbLJ朔1)J)RZ*Ji-bJPJkJIbL1&%sNJ朔Rk%2(eJ J*b朤:+J*1b VJjc+1Z!KJZm1Z5bLJ朔9*%J*eI 9(b*)9I2ȨZ+Rc)b)CI-Z,bTS'ŒR%[j VJ[k1cK+ZlZ5TcJc1k=bM1ZՖ[̵NJkKJ1b1Ji[))Z\C)Z,bVcj[ZkV[.b=kXSm 8P Y D0F)ǜ(sR* RRʜPJKsJI)RjRRj lДXА@*q4MUu}_,QTUוmW,MUUvm[5QTU׵m~MUUveٶm۲n èk۲m먮ۺۺ/T]Ym[u׶uu]mn# G!tBOp*auBCV1J3H1cL1Ƙ@!+(9s9s9s9s1c1c1c1c1c1cLNNPhJ @!))RJSAI)RJJ)RuRJ)"RJ)II)RJ):J)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)TJ)RJ)RJ)`` +IgrBiTRJ%UA(%JJ)RJ*RJ)A(PJ)%RJ(%J TB+RJB)TJ)%A(!BIBHtTR)!RJ)%:RK-RZJtR)RjR VJI%ZI%JI%RK)TRI%R*%ZJRHRJRJ%JJRj%Rj!JI)J)TBIRZJ-JITRIJI)RJK)JJZI)RJ)RK%TJ %RIRK)R@FTZf\y(d@@ 0@P0 A0GќBCOggS]b~u^+)-+**+.+-+,+,+0/,/,*0/-**,-.,+/+**/)/++--+-,.+**...-/*-+'-*+,.0+,++/)./-..+//0(((-+**,,,++*+*":5Ct+:1V3IꙨ1o2 #a #vNYVIK.iܳd)Ԝ֦ $Z=l;_ gC[#%[Rus=*=zscP)n(7|pA.b՟){*Xڠc].##am6[J>t+lMn/{6p$' si);rqF#介Iy `$uB+<%w{C7"aQ] G\]l$YT͆a޻V_=ݮdc7j!-ٶʨmf4^0cdaLRIi];#(tx+年WZ9ziVIY'hkV=S:#YKZMziȇ:kƠXR}P |48Mz#Y$MJeϬ zQxTlHxR:c!V6?[#ꗪC3Ĉ+wۜw$@8K5ł,>'i`CQWy cNT:P#Y$e>Gڨ]񶘥/'qы]v2ó|nk$\R=4F}Zs}0xR/}3bOBt8ͶGr#EZsc Sj?w'bae ѭotYRFsu7#IAn ,PW,VxiiiȼʦJDV+.%@2gQi[[[s7)a*U:?<~$Im_.*=l+ i^22WJSqp $\jX6a{xYc;sž$c)GrpW%!Zo'¯e.+eʳL&K x$ZgNE 1N9m=< HT\FS\0ǝ$qZ/`j'ip6-qprqأ$)smTuflcL9]0O3 k7b@H|i.*i*w%|$!VzǻT \eUg.l}c%dfoST"m]ugG%4%tc)zL3dcIY;܋]^BXV>l+\\H!Z(nuZ{4{$qV)/>P3iMWKE ý|ɐO5#T[NY=.]MuOul'5Vq=Ԏtu#a SX<.n'm)?*gl~u]"VV/X # 9UޏÞA.u'__#N%ߊi#%vE?N TvI\xZˏ~+6sj# kuzU"#fS=.DCc.a$a [uk0۹GnAon;e-qqD$T3>1>TnctgLnFٵYUƢ&$!,\w=rwTOj|D+fn9St#Y$+ !@\(k"G@=dH71$ɩ簦. wY}3 ՘dYN9cRt[V6k#qm?1MW&fްn6Ja$m4(L$aZ̒6) ^b*1fةhiw^kgS$!R4/o])͗&ޫd]6g蓕 aݻczmdĹnP;RE U+;#,ak2Ek 0W˳CQr8-ۻBZaEQ5$:s5]zv]#GFI$mЍCrpYqʙ$!V;x\@b1km6Α|k}AqZ*e5$!V=T]+M᱙(rC2zߨ$h԰Vvbj U׊\&]Jƹ $ܨZ`θW(^m)kkg]믙A}e/<[$dM  ܱww BWMD[=ZPq$匩絃|F~]U&(0ڍ1H~KS>RZoVJP$(a,xlɈ˽V !n։ 8?9=vBF$bSSOs#wZ)Q7=np=()z/TKs1qX#YTVy9 թPZjO %+-nYFZe$\¦V0]Iw83oז8bdvb#!SsT}wB}}1K~_S8SB$Hlz'Tz.JzH9>S$֭y:8$h$]XyLQ+@MX8{  qx9G0j~ $!fTMz%_iVm~[$2#!º0+|15'');ni艘uڛ#YTd:w1~w< N|'EgÛZ`iz;,n'#qZOrE^Ҡ2BwiW8 ɴ 9 c!uZsϒGvmJ:CM8G%#YTxh޼M[ b ri.{%H8w rcc4c[Qwa5MSwwRj:82T OggSb~r-++31/**),00.-.).,+,/--/,2)*.+,,-11,*+.1-*.)+)$aRJ+JmТəUyKƝy X]ӹ@}"Y$yvfFm.qh-83$:!t׽2կj2f;Q[[R㜅%([7$!j@oAҿ4E3sj3J)#iJzdqv2(lrI41af IknKޝ^+ R4Y3aDp#\Z bzJ )HxeABn* ة]!$%MBbeoLD1g{0fc#aNOY1obO% fgٹo[Sm!g1c;{ -$(pX_gȥ Q3 3IQV:`0u$!֓2ǭ䧕 s͡mݝ&;N$a3wTxJש 1,rr~adjθ#Tmsp63=:V=N5-ѝOis'Y6N$Y$òoצxRBv{$ 77{+d?'c4X Ja b#vm^ס'&UU^;OVǶ Scv]~Mg^Smd;W:WFɂՕUR$I&54UЈTI]YK,e_o Qr #!ZnN[S#nDs kkjkBsv@$YTSvy;TJdxzwZW/Dqײz٫P?#aZfY ~zXfP{3dhd{I^#qZ#B|ehhD{*ZwJqŔr$a6gqb\߶6~j歝r[c2΍w#ٮZo8y~L3=NUf*z7v Hn$TC;~~ dUrs8wq̹$!T4Yq}LB+ڥ9_b.ffe#T"i:z|`^-N[&D]1wfu"8˚4$IE:O-嵫~rƞPJ$hDfL&>$(unU_[ŔiZֳsdIoXyA GKh'[!i=$ɩZJVVA łN9SI&؜M3Y({q)A$ٮZEtt,N9Jϳ=Skjw4M"N* ca6OYˮ3C3N3jbB6tΪC$ai͜@ևxD~/oAԅλ->]tu.&؛"$=<8v^ަi[2Z\Xt⸇Į?$YT'C\.\8i36rS24)Y[]f1%j ~jLؗ[VjUY" 2jKC7>:00$\6m_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/song2.mp3000066400000000000000000000222201224420023200220070ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/song3.mp3000066400000000000000000000222201224420023200220100ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir1/000077500000000000000000000000001224420023200217115ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/advanced/subdir1/song4.mp3000066400000000000000000000222201224420023200233620ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir1/song5.mp3000066400000000000000000000222201224420023200233630ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir1/subsubdir/000077500000000000000000000000001224420023200237135ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/advanced/subdir1/subsubdir/song8.mp3000066400000000000000000000222201224420023200253700ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir1/subsubdir/song9.mp3000066400000000000000000000222201224420023200253710ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir2/000077500000000000000000000000001224420023200217125ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/advanced/subdir2/song6.mp3000066400000000000000000000222201224420023200233650ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced/subdir2/song7.mp3000066400000000000000000000222201224420023200233660ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/advanced_cache000066400000000000000000000022631224420023200214110ustar00rootroot00000000000000info_begin mpd_version: 0.15.4 fs_charset: UTF-8 info_end directory: subdir1 mtime: 1288121499 begin: subdir1 songList begin key: song4.mp3 file: subdir1/song4.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 key: song5.mp3 file: subdir1/song5.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 songList end end: subdir1 directory: subdir2 mtime: 1288121499 begin: subdir2 songList begin key: song6.mp3 file: subdir2/song6.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 key: song7.mp3 file: subdir2/song7.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 songList end end: subdir2 songList begin key: song1.mp3 file: /song1.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 key: song2.mp3 file: /song2.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 key: song3.mp3 file: /song3.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 songList end mopidy-0.17.0/tests/data/scanner/empty.wav000066400000000000000000000000701224420023200204450ustar00rootroot00000000000000RIFFWAVEfmt  factdatamopidy-0.17.0/tests/data/scanner/empty/000077500000000000000000000000001224420023200177315ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/empty/.gitignore000066400000000000000000000000001224420023200217070ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/empty_cache000066400000000000000000000001261224420023200207760ustar00rootroot00000000000000info_begin mpd_version: 0.15.4 fs_charset: UTF-8 info_end songList begin songList end mopidy-0.17.0/tests/data/scanner/example.log000066400000000000000000000003161224420023200207310ustar00rootroot00000000000000Exact Audio Copy V1.0 beta 3 from 29. August 2011 EAC extraction logfile from 14. May 2013, 13:26 mopidy-0.17.0/tests/data/scanner/image/000077500000000000000000000000001224420023200176555ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/image/test.png000066400000000000000000000002601224420023200213400ustar00rootroot00000000000000PNG  IHDRZsRGB pHYs  tIME 0UtEXtCommentCreated with GIMPWIDAT8cb@.`bj_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/simple/000077500000000000000000000000001224420023200200645ustar00rootroot00000000000000mopidy-0.17.0/tests/data/scanner/simple/song1.mp3000066400000000000000000000222201224420023200215320ustar00rootroot00000000000000ID3vTIT2 tracknameTPE1nameTALB albumnameTDRC2006TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtrackname name albumname 2006 mopidy-0.17.0/tests/data/scanner/simple/song1.ogg000066400000000000000000000401221224420023200216100ustar00rootroot00000000000000OggSѪJ}vorbis@WOggSѪJ} rvorbis-Xiph.Org libVorbis I 20101101 (Schaufenugget)title=trackname artist=namealbum=albumnamevorbisBCV R!%SJcRR)cP[Gc9F!dSI{O*XJRX)ESLSIR)EcSH!S1esKI %lMtKc1FcZJc1EcRRIs:f%d:Fb|0:B(R-[S-KiasJjc1S(АU@BCV P EQАU@EqqG$BCV@((#IdYeYy/.!I̐SI&)U99dRƘbQΐS 11)N9 "CHd K=b8"A!Ɛs J!rI D9)LJ(I -"眔NJ&RˤB+8XRH)ĔbN1R)ǐR9Řr1 T1H)sN9 d * 2B!+8$iihi(z(y陦zlyiz)k늪j˦ڶ骶ʲn۞ʶnml,ۺyꙦz麪ڲ꺲홦늪+ۦʲʶʲk麢ڮʮmʺʲ۶ 躶ʮ-lBT3MLuU׵mum[3M5]WEueՕu]ue[LuMWeUeYeveWE׵mU}]ue_meY}uu[eWeYe]Y}SU[7]WM}[}am]WUօUu}eu0,뾮00m ëƱ뾮ܾj۾1nƱm+loq,ʾo/ *˺ڲ˺. jںp̲. +ǯ Cնuo 7v@!+8!c* R !T1!cJJI!* dIJhJ(PJKRj-Z JiZj)Rlc2dI(VJi)sLJƠB*JIeIɠ9HRIPJkJJJmJi-ZIRmZ# dAɜRJIZ朔:*J)RA(%JIJ+JJRZk՘RK5ZIPJkS+5PR JiVkj-PBkK*1cmJi[)[XSK5blJ-9ZkJ-R[LXk %JiZJZJ*ZlZ5b))JlX[l5blXR1XsKՔZXK+5kn5R@ eА@` cAhr9)R9'%sB)eA!99B))[(%Z, M Y D ( c*sBcAsA)cA'%B)B( lДXА@` b 1 tR:)LJ'Z )eJ%ZH 2k%bFXb*B(4d%@c9gb9!41*ƜsBc9!9 BsBBA!RJ B)tBR *pQdsBCVy1J9'%F) [cRjb BJX1!b ZvRj-ZCJXk!b5Z{j-ZsιE6' *4d%@ c9b1CJ1Ƙs)s9c9s1s9Ƙs9s9砃9sAs9!t9 *pQdsBCV1RJ)RJRJ)R!RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ) gXI:+ .4d%9'%1tNJI%5A(sRJ)ZjRJb !Z Vk)R()KJ2$ZK9ZjBJRkuRRIZm-Zk-bl%ZkZL[K-bKb1 npHqBCV!2J9眃B!R1砃B!DJ1會B!1 B1B!R: PJ RJBRJ !B(RJ)!J)RJ)%B(RJ)B(RJ)B(RJ)BRJ)RBRJ)RJ(!RJ)RJ %RJ)RJ)!RJ)RJ)#$"l4LQH h  "$OggS.ѪJ}Ѯ/.7957;R]^_[Y^``]]d^ZX]ac`d]\_YY\`^\aceacc\]Z^cbzP*̀l;(_k| 4q~r?fLٜQIf zR*T.d17^X=cXDDga(}HRFǓ-|r7zQ*MrVv7y`Vҋ%٢R? eprd1cz'`MߔzV@eD~FeJOu|Q)Z}^6t&O 41zV`i0{P]dpqAq# #/YWvz+@`{-W[ !$W7S,ŶMJ5qW7ֹډǯȴ zMfqlQi|2x6ͧw[67!>t?pHb独ytQ7;y6z96_(#ꘙָ .w74.˟<9 2ahj꿹$Jug:-v%D=v.:즲w(=ꨋ~!Pb\$\/j>daI$vs`1KTq+$Y0{qd@m625%ӽaloK~]@-MTj  `A@?UN:fKP)"JJփm6ڬikU-~Sd_vx7| 2 C]NukIרxr/@â?vXV->VL6;FMX򺊡P9Uij ^kuW櫓1l`]s;mL$FKoid*^R(A#$OFo:Ii$JC(8Ǭ$_5 \xc;9ٙLsuŽJG< g28ޢ|[wXZ^ Vb?%?k Y` c4+"]󾶚 IbLxدfݰ_#jG9 KuqZ!RdŽo~^Pk<@X٬tOaYѾ{ldL&ϥ֐$ 䴼5**c'OmՒ0S_hBT#c|}4zywaYq|kOx);tI?폼hɇ$DOtFÞ?f+ tz\N!N{y<<гrMn~^ 啕I[(-(MFߥ|3 9.qO_*gۥI\$.1y< ~!. B㲢ݔ9asa7DqEZd%O{Pz'R0R?'G~}r>~ ( Nj$Z\7]MK( +3fыktzJ5 l>YB>إ+\P_w ~Yd2ZHSM*wmlsmػ@JTZ1G HYw'}]g`73)zl".+Hry~xMz\c@H[Lsu頋h b7k- ,. w$#X69Xǎv]p-ӧO9z X3_k?a[6\zRW{rnX\7z(<(NS9$)h:+ ~I=bÂ#s̳z\ @`p"&8Ky[2ʨA^ t`׿;{>vf=q+>9꽄p K5ʗ9켛 ~!XhQD?7Z*ezij".>7{-eȕ0R%LCw~@Z^8hUpR1K'"G:?Vq:: Y"h,zGn>Sֆyd ?[΍*dO$yp{};㪎vu8FU%{x  |::CS mgqyRQTN{4da& *68'%=/_rR; ~_ VJr`gf}ӹsfW1fMl9vLݰ8C&1gE՞4~װkݥm~N8zC ԧd߉l=nH;a윗oz2`xy8n$*H@1i E&Wϧ;[t[{f^SfAb^^@cgc*6;t37CMB,z0\teđ=ZhH7T( o;(0 W}[[mX^;1v HŕI7cN9pV6pȫ׀[=ЄӘ[*_A?s!c}IizDt{eSXHس y榫 .o~Uh6 1'NW:Y81gm]镔2+sxzSdWz] *+]ZMdEk;~ya+ t.A6 ٽ$U=QtYW Q*Zn':&z) VM%XYU8m=̂.s)א g, nNuYp~rbM,z]W @Z6mOSi*~\ @ };FGþqh7vwmZM6ʥݣ߹'Hn'Ɉ L<h:iT˯v|~} V_ԝuq9|~OJuQ[k+F:6yM+R<~jm,;=n`=қqㄫ>z@v tQ'̖fOk+;u+CJJݝL'z&w)̀{'/ йi69P= %ZFЫܧ~! @hֵ-^(\>O\LH'L눂C:=gB.dt*~Hxz]b ,zW E='Ʃ o ?kwq>do6EZx<|MMr1 C@3ܯ.ſdr|w~!H ZU/YMFE-ݳ̿ ;'!ǍIGF="Ƕ[뚆!Ғ$勈͛ll~R@5^ Ziз^Xz (_`@ z'Ϧ5C|Lv]ŋ"yM%h>ba րLCO<{_#1t~@h @dR6:F'gk/uZOeSVkd_[D=կ pI^QNIm1]z @v@1ɾ6 g0Sq& RQ8!z>^C܇4AAp$DlvuOctfs H.Lb6zS}h@Tሱ1[ö]NZXu.X/6;f# Q9"QIny^U6DĔcO>X_PQQoY%wΦ.%cvodi)mx,.  | pAiM@UQTSvSBwqwh PWպ^V6F)i. 9 G8KH%~ |ӟeS 5CfnHvKyIƚHdM)i5d+U8LA{L?pHyT֮^^]Љ긱fNv X8>dXm1DH[Y"cl4HmG~e0*p/Δ>Vad|_$o_z%[S=HhАlJY@*+M$ˋ0аjl fX ~@v ޔ21B IFGޖ4ц&wa~q92=]Չ;ޱY$vQz  bTŨw6l)i2\ l &ivF_J {ERR.A^t*\ݍqv uOcKl2FC]ZɤVa5S r!gher{Beuk5qSUڻ4`{N/W_cЁu>=j;*m1n{A=l}w\EfY$M:  }G1UP{0~_ߏ.dƫZtlNWwo#s>coSwhz  .g[kèqx{L#}͎VpkW|{tm2a)Ot9Iqg ^3?Pf%_~d?`23.b>2{Zu?5,}.3ox?M>] Z2J>V#]v.:=zXU4(>ʊVGFv.!~ݽﵺ^.I"DGG_- x$:uK Q='UkW~ 3A1_/7^%ɔ>'S.-e/Z $:g!y&̉SbYsx)\lS l Zf#ʫ~!a/~Zfuڲԍo&q9w lεZ۵c>~5_=]}MZL~_ժ':ήo0x )S`12Q^ad"oejHI0"Bo1,ܬ<#_&=]=W/q~_)꠽e_t։WIQ:'_I:6r9tMd':n0S]U ĶfWNSUb$wfly>O(fz @AojvivI`EϏ- :jc9.Q&dsYLu&ZM* хee vɏ~?~!j@c坆0icBqfM8A5mF"N.OBMA7bMy9Vsz +'8x4ȔM,ڑߥ:~^ hcB@̬?B=CvIgg=wWVc̤}9z=f Bc 1_%`/ ܄+L2k]  /y'~h}@rtYj|bx剄LyojKC& أ1Me J5w<=~M1J^<:;N٩qPzj m #1̇lb[f>%L$q7])x3a/E72NO^`0.c/>0_D'Q_Pu0?OOkok/'†HMO?$R8ٜD* 72*OO8`FgS~^zSeWŢ5Fn\~4Ds1i]r4 Y,8,eX!J$4&GzdW ĠQ`4RR>u  |?GB": ~^@.kgq.L\`ox]ѫF*T¨<ad:0AʫݰmIY`Ս]&n>μ?OggSѪJ}aD-^[``]\Y\^[X^[[\a`\^ZY_\Z]^`]]]_\X^Z\\\ae\Z_YYzX+ZG1fs)IiE9z)$6_k4C7, @,kxmn+$@įy7qg1` +=5O6u)G9e v=[0ҢHp䐺d, tgI^c<>I04OgvT1_$zS| D73} AO{F-S7CȘN~5$Ewgou(r):̡r}IDz{Ecr9[MB|/ LtcO'#/;t>]a?8HQ~}u~ ٚ]bϤkk,V26֩%Kek'9mLo/e ziu;R/?Z-Ӂj+y3:He8ї ?G~Sd%iUZ -d^I;{X@H隀y"uo.n19"งan0:^O t.nÝΙ_lM_߳'z3$AM|Q|W|[Ҫ{z/)Md: kzYq^m|5}N();;yR-q*.Dq1!t/.:2Y}5ӹ-#E 1"I$~^ DnaOa7Kڻtr|>BXP y.N gB0#'kE{R]ad!3gy1_1 ~a(Po#ޚ͢cg`ȡyd1Uc:'dbݝWiKr*v_NPw˞ z;]Pɖ\+ lwL-||mdX:|92(slqZXqB(`?:w`@ YwO/QjԤUT~ dh-CCL4X]Ξ3+UC/t%nTXȣ>/7d;N>瑵c9 D!ggQ&5X$I4;~_@H W!b`X)ƴ?JscXOn5ƢV \7x8J8tosy9"[};Ĩ|Sbx6rG߼ϫ{ǟ}=t"=~ ( @ョU*nWhb~ \]^lJ^Sw~qd='Mv!W`d;*=/&2_n}m6Tb6])Ey1{/h C[P^_)w ,KU^Ie9az ) +#36\5,rWxJ ˦&25)vHEO>S E]/ =l5m~ BAЌemҖgtb67uۺX^2&^]|Liqb1(7b"\#~9)j|F tFY:)!jcXFP6Qqi?=jKpsrjiG:S,uY"#,y渏zcf juL}am#7zX].N6 O(w[3{hQ-+[>O<&agh¯??Sw^Kq @cy;Ǖqy(#kv@39RK}w[cNByǧV1ٖHO< TR^xw'e6m/G_۵WZtt.q0{RW4Lzm( du,8;TEgxo0g!_4hc#D)ϓtT2mO"}H[^"Cқ晆0!1'`1q.[)Q:@<% ~ h49rRyYjiH6aJG -y뺩\UfLܕ/N `iA_Ð*i.S++yJ6ES,=Z%n-L M$Q;59HC{l9[s]oB@}a_P# VaYuG~KWDY}y_O5bJWc8RUJ7BA3V*kΈܭ!ljzڭOb>fk̗p6f0vR Ƶ/^zˇq.v~!D ."qm.Xs}>{3ɜ297IBP(|pCUr \ S> .NH<5B@w}dtzBbRsޡ-uΝZwVuD)긪EfGR)ˆtt}ᡂvp(fvͣzEdb$#zY#^ʎ3JN 6CIKf)Rfv[?G "oy_U<Y#3x9(y%~agD?Ӥc\>{3q/5+קE~Mt p ]PE<-1UNZ" 7+ƛDC/+( 4崊$.v P!d;\'O:$TЃb(k{gnt8{AA 9k԰<բb?nH‹_ku~! @ bHk6dYP+vKLuєrNkxt8 *8MuՐ-I\`/gF?l<ѧ%b;1z+QםKqazݳ/-p$Pzv=.9'[7_h4ıaC=@sK$:4lJy-zwaQle$~_j%X!8]S͒ݜsqzHtG = 2<"^w[qH ޽܎?a\OggS@ѪJ}.5 \a_]^`a_bH7 z^]cGQJx֬v_q7u3I4/o-AKeOx \p.e},a A | Q ~^ :Km\ t'?\V)eL8I_;He0k/_ HX_ H8" Ps;'y&ҺJ~_ @L)ey*tu8ζ ]Iyb}5_⭢c*0zAJL11q|2طЙ{|v vx`Sh1pM<^{Ñ^Y2کI9QZM8V9ރ)Jrze"e{fS]۩qqLzS}$jU5s{ʒ,'${lYzeH¼ hVHp^8Vg|A M}ﲡWif&zS|AЀG @ַ)oLd%ϧԜL^"\VBs_"!mF/26~.o$;r}cs/~! c0W0Rs_jrCƋMFj,4-zYmÂn=B͡s!vy3 !29v8>6~%z_+x@P< ?D63Mۥ۳iE}[fчK 2v^)Sd^cX!m1Aն55*Kj`nH%74wm~]~ S'gB] l-@4L3Uhڨ|wuHneo5iv[Yȳ*A>x έGKc<%w7ι^#v #K=s_C.}יp4b=yyzP*MC%8k󏝀RE\qM.z}mopidy-0.17.0/tests/data/scanner/simple_cache000066400000000000000000000003271224420023200211340ustar00rootroot00000000000000info_begin mpd_version: 0.15.4 fs_charset: UTF-8 info_end songList begin key: song1.mp3 file: /song1.mp3 Time: 5 Artist: name Title: trackname Album: albumname Track: 01/02 Date: 2006 mtime: 1288121370 songList end mopidy-0.17.0/tests/data/simple_tag_cache000066400000000000000000000003471224420023200203400ustar00rootroot00000000000000info_begin mpd_version: 0.14.2 fs_charset: UTF-8 info_end songList begin key: song1.mp3 file: /song1.mp3 Time: 4 Artist: name AlbumArtist: name Title: trackname Album: albumname Track: 1/2 Date: 2006 mtime: 1272319626 songList end mopidy-0.17.0/tests/data/song1.flac000066400000000000000000000345431224420023200170310ustar00rootroot00000000000000fLaC"pl&>r3( reference libFLAC 1.2.1 20070917 ?~=ӻ/w?o~gfzs>g9gysݹϿ33]9?^ztϥ.ɜ>|3f۝~wMMs{^?gsY󛞶M>}IO/|Od&{399f7_>o$/?ϖ3˙M=̳>O'$s'Zug{/ϧ%w?ξ{z{\}9w2w?7<~y˯?ys?';s>u}׿Ϲss+93?+l9=ɧo>N\|UL?r_s_m_wy6???_tܷouzs&tͺynow2r5s濓漥_yNi=3j/9^<=?|g>yϒ-'Nߙ{_>~?O5|_/>o3f_s?s<\_ӟgs;?S﹙?'{s?:'3yO3<ϻ>Y߽vgY<Z~?ӿ~lܛOw9L$_Ogrrϛ^S^͟$?>O}/w|3OL?=N>K|ys?'og~y~fݟ?7+Lӿ9}OgO%w_}ɓgy}'?9>_g|*|?Iw>jsrs&W'g۾s_[<-Oגs<<9)|3em>[>Oϓ%45gϷ7Lw~O'.:>3wS9yW?s]?o?}62Lv˧yyfRK'|3͟sʗ6?O7߳}ygy=/?e?i>[}fWylOOzM}穻9jgwv99tLJwy~:~~||[yoӓ???k>ӼM_w?o&sy'ݖ3Ͽd?~n&/w=>zL/ϝOۓ_;}~y}'?9?e|;ߟ9Y?'2rwwI}Ot%g3yg?}?^k'}6gI[sK\g3?uK?d]~~'߿fwf缟WsϟOryfӜ7\gɧ?|Ͽ?7-ϙ94i|:}湙|~5^}͛[sOoo-O=r|u?OI$ig$;{&w'9~ue?9>W|w3wyL\'MwϤgs.}yy3d?}?mg'?ڒVz?%y[K6O)9g<>y_~w-3ww'lf2e{_?{OϗvOv~|~7y$ϟӟ秜wyw|}7<}d۵?L~g=K'3O7w=߹|es<}w|wsOV~o5t'?r+eYvO92~y~Y7e|9=f}y̟7<{|yo}{^f<ݞso9r65?NNo'4mM{eIMs[?_Sg$e]^o9{z~Il|?>߿y|<92O\~I;>~|3濺~3}yJ~dLgyܿ<_߼oZ~w?_<>gʛo~[O+gܿ}?oRdߓ3yNg}Y䟟_|s&%O7n}ϙ?_2noY>?ܙ^Լ?rm矹~o3/ng:O>}|~stw79'4gɛ{9O3w6[s3RuOgww?rJYly?i3_lf?˼ϓ?ݜ_w5~_ϟ?vw?;s3|vwϛy?Ov97loϓ俞yO̜9O%7d˟>?-Ϸ}>O??ܟgng?fϳ}3r_s~}^j~of&{s읝/gIΟ;?&N~=~{r~Ϝ)v?RsM?/ts|d>MoϜ?}Ҧwi4N2翙3?ϳΖyolg:'O˖|?_=?7ϙ*~ɓgϞo$y3do&{}3{Ϫ\<{Ϝ?OS9۟>w?/ygwy3>ϼs̗}?|?O|Ӿly{8ssy-rN\Y;黺e|Κyym|Ԟg;/??'oMoLsZgK~yO矿g^~>\s^g?y~]K?m?fnsyϞIL?i-y˿3?ϳ%fy~=zr|{;K糥o3J{w_~Wh]t jUVJUUWJTꪺꪪJRJ]TUU.U}+*R*UTtWJUIUUtvjUUI|U_UUUU*RU+UWTUUU*T˥UJ.UJUjRꫪꪫꪕꮥU*UUUګ_UUuTRꪩuUiUUKU*UU*UJUTUjRJ]]}TUJWJUUUUUUJU_TTWU%UUUUU]JZUU*]UZJU*ժzTRJZIUIJ)tUUuW)JJUZK%UwuU)uJRUjUWWJ*RTWTUUUJU]}uJJJU*UUUU*IwUW_UU֥RU]U]UUWTU]]UUU*.R.wU]UZ]UU*UUW]RTJ_UUUԹUrJJ*ꪤW}%UJJUjUuURUUU*UUW]RU*ꪥW}TU*J]UҪUUuU]WU]TԾUVIuUKTRKKWRWUWUKꪥ)WZuIWuuUUUuUUIUZU]TU)WuRꪪWU]vUK]*Pmopidy-0.17.0/tests/data/song1.mp3000066400000000000000000000222201224420023200166100ustar00rootroot00000000000000ID3vTIT2titleTPE1artistTALBalbumTDRC2010TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtitle artist album 2010 mopidy-0.17.0/tests/data/song1.ogg000066400000000000000000000207371224420023200167000ustar00rootroot00000000000000OggSb~|vorbis@@OggSb~X -vorbisXiph.Org libVorbis I 20090709vorbisBCV R!%SJcRR)cP[Gc9F!dSI{O*XJRX)ESLSIR)EcSH!S1esKI %lMtKc1FcZJc1EcRRIs:f%d:Fb|0:B(R-[S-KiasJjc1S(АU@BCV P EQАU@EqqG$BCV@((#IdYeYy/.FuL*CCc3C LcN4 23Ő2[,.!+(b 9dR"瘔NJQ(K[1Q(eBŌRT@@PhȊ 0)B)s1 1 d)NJ圓Ic1sNJrIɤ`!"0HY&gꉢZgigj뚪ʖ癦gꙦ꺦l.jۮkl+ʺʶKgnkۚjʮm-l,fl-,ڶ*˺/n,lںk,*˾1۶˺.'뙪몮k۪ںl-+۪,+˶,+ۦʲ*˾ʲn,*1̶*˺ʲn nʲ ˬۺ1﫲-, 2>ct]_WmYV}cuaYm[][gn nʭ ˲ڶ̺,.|[ڶ麺nʲ˺.uWF}ն}_e߷_ið,k/뺰/,m+0ۺܾ, ˪۾ҵue}+ p0 "@r)b BB*cR2dI)JIbLJ朔1)J)RZ*Ji-bJPJkJIbL1&%sNJ朔Rk%2(eJ J*b朤:+J*1b VJjc+1Z!KJZm1Z5bLJ朔9*%J*eI 9(b*)9I2ȨZ+Rc)b)CI-Z,bTS'ŒR%[j VJ[k1cK+ZlZ5TcJc1k=bM1ZՖ[̵NJkKJ1b1Ji[))Z\C)Z,bVcj[ZkV[.b=kXSm 8P Y D0F)ǜ(sR* RRʜPJKsJI)RjRRj lДXА@*q4MUu}_,QTUוmW,MUUvm[5QTU׵m~MUUveٶm۲n èk۲m먮ۺۺ/T]Ym[u׶uu]mn# G!tBOp*auBCV1J3H1cL1Ƙ@!+(9s9s9s9s1c1c1c1c1c1cLNNPhJ @!))RJSAI)RJJ)RuRJ)"RJ)II)RJ):J)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)TJ)RJ)RJ)`` +IgrBiTRJ%UA(%JJ)RJ*RJ)A(PJ)%RJ(%J TB+RJB)TJ)%A(!BIBHtTR)!RJ)%:RK-RZJtR)RjR VJI%ZI%JI%RK)TRI%R*%ZJRHRJRJ%JJRj%Rj!JI)J)TBIRZJ-JITRIJI)RJK)JJZI)RJ)RK%TJ %RIRK)R@FTZf\y(d@@ 0@P0 A0GќBCOggS]b~u^+)-+**+.+-+,+,+0/,/,*0/-**,-.,+/+**/)/++--+-,.+**...-/*-+'-*+,.0+,++/)./-..+//0(((-+**,,,++*+*":5Ct+:1V3IꙨ1o2 #a #vNYVIK.iܳd)Ԝ֦ $Z=l;_ gC[#%[Rus=*=zscP)n(7|pA.b՟){*Xڠc].##am6[J>t+lMn/{6p$' si);rqF#介Iy `$uB+<%w{C7"aQ] G\]l$YT͆a޻V_=ݮdc7j!-ٶʨmf4^0cdaLRIi];#(tx+年WZ9ziVIY'hkV=S:#YKZMziȇ:kƠXR}P |48Mz#Y$MJeϬ zQxTlHxR:c!V6?[#ꗪC3Ĉ+wۜw$@8K5ł,>'i`CQWy cNT:P#Y$e>Gڨ]񶘥/'qы]v2ó|nk$\R=4F}Zs}0xR/}3bOBt8ͶGr#EZsc Sj?w'bae ѭotYRFsu7#IAn ,PW,VxiiiȼʦJDV+.%@2gQi[[[s7)a*U:?<~$Im_.*=l+ i^22WJSqp $\jX6a{xYc;sž$c)GrpW%!Zo'¯e.+eʳL&K x$ZgNE 1N9m=< HT\FS\0ǝ$qZ/`j'ip6-qprqأ$)smTuflcL9]0O3 k7b@H|i.*i*w%|$!VzǻT \eUg.l}c%dfoST"m]ugG%4%tc)zL3dcIY;܋]^BXV>l+\\H!Z(nuZ{4{$qV)/>P3iMWKE ý|ɐO5#T[NY=.]MuOul'5Vq=Ԏtu#a SX<.n'm)?*gl~u]"VV/X # 9UޏÞA.u'__#N%ߊi#%vE?N TvI\xZˏ~+6sj# kuzU"#fS=.DCc.a$a [uk0۹GnAon;e-qqD$T3>1>TnctgLnFٵYUƢ&$!,\w=rwTOj|D+fn9St#Y$+ !@\(k"G@=dH71$ɩ簦. wY}3 ՘dYN9cRt[V6k#qm?1MW&fްn6Ja$m4(L$aZ̒6) ^b*1fةhiw^kgS$!R4/o])͗&ޫd]6g蓕 aݻczmdĹnP;RE U+;#,ak2Ek 0W˳CQr8-ۻBZaEQ5$:s5]zv]#GFI$mЍCrpYqʙ$!V;x\@b1km6Α|k}AqZ*e5$!V=T]+M᱙(rC2zߨ$h԰Vvbj U׊\&]Jƹ $ܨZ`θW(^m)kkg]믙A}e/<[$dM  ܱww BWMD[=ZPq$匩絃|F~]U&(0ڍ1H~KS>RZoVJP$(a,xlɈ˽V !n։ 8?9=vBF$bSSOs#wZ)Q7=np=()z/TKs1qX#YTVy9 թPZjO %+-nYFZe$\¦V0]Iw83oז8bdvb#!SsT}wB}}1K~_S8SB$Hlz'Tz.JzH9>S$֭y:8$h$]XyLQ+@MX8{  qx9G0j~ $!fTMz%_iVm~[$2#!º0+|15'');ni艘uڛ#YTd:w1~w< N|'EgÛZ`iz;,n'#qZOrE^Ҡ2BwiW8 ɴ 9 c!uZsϒGvmJ:CM8G%#YTxh޼M[ b ri.{%H8w rcc4c[Qwa5MSwwRj:82T OggSb~r-++31/**),00.-.).,+,/--/,2)*.+,,-11,*+.1-*.)+)$aRJ+JmТəUyKƝy X]ӹ@}"Y$yvfFm.qh-83$:!t׽2կj2f;Q[[R㜅%([7$!j@oAҿ4E3sj3J)#iJzdqv2(lrI41af IknKޝ^+ R4Y3aDp#\Z bzJ )HxeABn* ة]!$%MBbeoLD1g{0fc#aNOY1obO% fgٹo[Sm!g1c;{ -$(pX_gȥ Q3 3IQV:`0u$!֓2ǭ䧕 s͡mݝ&;N$a3wTxJש 1,rr~adjθ#Tmsp63=:V=N5-ѝOis'Y6N$Y$òoצxRBv{$ 77{+d?'c4X Ja b#vm^ס'&UU^;OVǶ Scv]~Mg^Smd;W:WFɂՕUR$I&54UЈTI]YK,e_o Qr #!ZnN[S#nDs kkjkBsv@$YTSvy;TJdxzwZW/Dqײz٫P?#aZfY ~zXfP{3dhd{I^#qZ#B|ehhD{*ZwJqŔr$a6gqb\߶6~j歝r[c2΍w#ٮZo8y~L3=NUf*z7v Hn$TC;~~ dUrs8wq̹$!T4Yq}LB+ڥ9_b.ffe#T"i:z|`^-N[&D]1wfu"8˚4$IE:O-嵫~rƞPJ$hDfL&>$(unU_[ŔiZֳsdIoXyA GKh'[!i=$ɩZJVVA łN9SI&؜M3Y({q)A$ٮZEtt,N9Jϳ=Skjw4M"N* ca6OYˮ3C3N3jbB6tΪC$ai͜@ևxD~/oAԅλ->]tu.&؛"$=<8v^ަi[2Z\Xt⸇Į?$YT'C\.\8i36rS24)Y[]f1%j ~jLؗ[VjUY" 2jKC7>:00$\6mr3( reference libFLAC 1.2.1 20070917 ?~=ӻ/w?o~gfzs>g9gysݹϿ33]9?^ztϥ.ɜ>|3f۝~wMMs{^?gsY󛞶M>}IO/|Od&{399f7_>o$/?ϖ3˙M=̳>O'$s'Zug{/ϧ%w?ξ{z{\}9w2w?7<~y˯?ys?';s>u}׿Ϲss+93?+l9=ɧo>N\|UL?r_s_m_wy6???_tܷouzs&tͺynow2r5s濓漥_yNi=3j/9^<=?|g>yϒ-'Nߙ{_>~?O5|_/>o3f_s?s<\_ӟgs;?S﹙?'{s?:'3yO3<ϻ>Y߽vgY<Z~?ӿ~lܛOw9L$_Ogrrϛ^S^͟$?>O}/w|3OL?=N>K|ys?'og~y~fݟ?7+Lӿ9}OgO%w_}ɓgy}'?9>_g|*|?Iw>jsrs&W'g۾s_[<-Oגs<<9)|3em>[>Oϓ%45gϷ7Lw~O'.:>3wS9yW?s]?o?}62Lv˧yyfRK'|3͟sʗ6?O7߳}ygy=/?e?i>[}fWylOOzM}穻9jgwv99tLJwy~:~~||[yoӓ???k>ӼM_w?o&sy'ݖ3Ͽd?~n&/w=>zL/ϝOۓ_;}~y}'?9?e|;ߟ9Y?'2rwwI}Ot%g3yg?}?^k'}6gI[sK\g3?uK?d]~~'߿fwf缟WsϟOryfӜ7\gɧ?|Ͽ?7-ϙ94i|:}湙|~5^}͛[sOoo-O=r|u?OI$ig$;{&w'9~ue?9>W|w3wyL\'MwϤgs.}yy3d?}?mg'?ڒVz?%y[K6O)9g<>y_~w-3ww'lf2e{_?{OϗvOv~|~7y$ϟӟ秜wyw|}7<}d۵?L~g=K'3O7w=߹|es<}w|wsOV~o5t'?r+eYvO92~y~Y7e|9=f}y̟7<{|yo}{^f<ݞso9r65?NNo'4mM{eIMs[?_Sg$e]^o9{z~Il|?>߿y|<92O\~I;>~|3濺~3}yJ~dLgyܿ<_߼oZ~w?_<>gʛo~[O+gܿ}?oRdߓ3yNg}Y䟟_|s&%O7n}ϙ?_2noY>?ܙ^Լ?rm矹~o3/ng:O>}|~stw79'4gɛ{9O3w6[s3RuOgww?rJYly?i3_lf?˼ϓ?ݜ_w5~_ϟ?vw?;s3|vwϛy?Ov97loϓ俞yO̜9O%7d˟>?-Ϸ}>O??ܟgng?fϳ}3r_s~}^j~of&{s읝/gIΟ;?&N~=~{r~Ϝ)v?RsM?/ts|d>MoϜ?}Ҧwi4N2翙3?ϳΖyolg:'O˖|?_=?7ϙ*~ɓgϞo$y3do&{}3{Ϫ\<{Ϝ?OS9۟>w?/ygwy3>ϼs̗}?|?O|Ӿly{8ssy-rN\Y;黺e|Κyym|Ԟg;/??'oMoLsZgK~yO矿g^~>\s^g?y~]K?m?fnsyϞIL?i-y˿3?ϳ%fy~=zr|{;K糥o3J{w_~Wh]t jUVJUUWJTꪺꪪJRJ]TUU.U}+*R*UTtWJUIUUtvjUUI|U_UUUU*RU+UWTUUU*T˥UJ.UJUjRꫪꪫꪕꮥU*UUUګ_UUuTRꪩuUiUUKU*UU*UJUTUjRJ]]}TUJWJUUUUUUJU_TTWU%UUUUU]JZUU*]UZJU*ժzTRJZIUIJ)tUUuW)JJUZK%UwuU)uJRUjUWWJ*RTWTUUUJU]}uJJJU*UUUU*IwUW_UU֥RU]U]UUWTU]]UUU*.R.wU]UZ]UU*UUW]RTJ_UUUԹUrJJ*ꪤW}%UJJUjUuURUUU*UUW]RU*ꪥW}TU*J]UҪUUuU]WU]TԾUVIuUKTRKKWRWUWUKꪥ)WZuIWuuUUUuUUIUZU]TU)WuRꪪWU]vUK]*Pmopidy-0.17.0/tests/data/song2.mp3000066400000000000000000000222201224420023200166110ustar00rootroot00000000000000ID3vTIT2titleTPE1artistTALBalbumTDRC2010TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtitle artist album 2010 mopidy-0.17.0/tests/data/song2.ogg000066400000000000000000000207371224420023200167010ustar00rootroot00000000000000OggSb~|vorbis@@OggSb~X -vorbisXiph.Org libVorbis I 20090709vorbisBCV R!%SJcRR)cP[Gc9F!dSI{O*XJRX)ESLSIR)EcSH!S1esKI %lMtKc1FcZJc1EcRRIs:f%d:Fb|0:B(R-[S-KiasJjc1S(АU@BCV P EQАU@EqqG$BCV@((#IdYeYy/.FuL*CCc3C LcN4 23Ő2[,.!+(b 9dR"瘔NJQ(K[1Q(eBŌRT@@PhȊ 0)B)s1 1 d)NJ圓Ic1sNJrIɤ`!"0HY&gꉢZgigj뚪ʖ癦gꙦ꺦l.jۮkl+ʺʶKgnkۚjʮm-l,fl-,ڶ*˺/n,lںk,*˾1۶˺.'뙪몮k۪ںl-+۪,+˶,+ۦʲ*˾ʲn,*1̶*˺ʲn nʲ ˬۺ1﫲-, 2>ct]_WmYV}cuaYm[][gn nʭ ˲ڶ̺,.|[ڶ麺nʲ˺.uWF}ն}_e߷_ið,k/뺰/,m+0ۺܾ, ˪۾ҵue}+ p0 "@r)b BB*cR2dI)JIbLJ朔1)J)RZ*Ji-bJPJkJIbL1&%sNJ朔Rk%2(eJ J*b朤:+J*1b VJjc+1Z!KJZm1Z5bLJ朔9*%J*eI 9(b*)9I2ȨZ+Rc)b)CI-Z,bTS'ŒR%[j VJ[k1cK+ZlZ5TcJc1k=bM1ZՖ[̵NJkKJ1b1Ji[))Z\C)Z,bVcj[ZkV[.b=kXSm 8P Y D0F)ǜ(sR* RRʜPJKsJI)RjRRj lДXА@*q4MUu}_,QTUוmW,MUUvm[5QTU׵m~MUUveٶm۲n èk۲m먮ۺۺ/T]Ym[u׶uu]mn# G!tBOp*auBCV1J3H1cL1Ƙ@!+(9s9s9s9s1c1c1c1c1c1cLNNPhJ @!))RJSAI)RJJ)RuRJ)"RJ)II)RJ):J)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)TJ)RJ)RJ)`` +IgrBiTRJ%UA(%JJ)RJ*RJ)A(PJ)%RJ(%J TB+RJB)TJ)%A(!BIBHtTR)!RJ)%:RK-RZJtR)RjR VJI%ZI%JI%RK)TRI%R*%ZJRHRJRJ%JJRj%Rj!JI)J)TBIRZJ-JITRIJI)RJK)JJZI)RJ)RK%TJ %RIRK)R@FTZf\y(d@@ 0@P0 A0GќBCOggS]b~u^+)-+**+.+-+,+,+0/,/,*0/-**,-.,+/+**/)/++--+-,.+**...-/*-+'-*+,.0+,++/)./-..+//0(((-+**,,,++*+*":5Ct+:1V3IꙨ1o2 #a #vNYVIK.iܳd)Ԝ֦ $Z=l;_ gC[#%[Rus=*=zscP)n(7|pA.b՟){*Xڠc].##am6[J>t+lMn/{6p$' si);rqF#介Iy `$uB+<%w{C7"aQ] G\]l$YT͆a޻V_=ݮdc7j!-ٶʨmf4^0cdaLRIi];#(tx+年WZ9ziVIY'hkV=S:#YKZMziȇ:kƠXR}P |48Mz#Y$MJeϬ zQxTlHxR:c!V6?[#ꗪC3Ĉ+wۜw$@8K5ł,>'i`CQWy cNT:P#Y$e>Gڨ]񶘥/'qы]v2ó|nk$\R=4F}Zs}0xR/}3bOBt8ͶGr#EZsc Sj?w'bae ѭotYRFsu7#IAn ,PW,VxiiiȼʦJDV+.%@2gQi[[[s7)a*U:?<~$Im_.*=l+ i^22WJSqp $\jX6a{xYc;sž$c)GrpW%!Zo'¯e.+eʳL&K x$ZgNE 1N9m=< HT\FS\0ǝ$qZ/`j'ip6-qprqأ$)smTuflcL9]0O3 k7b@H|i.*i*w%|$!VzǻT \eUg.l}c%dfoST"m]ugG%4%tc)zL3dcIY;܋]^BXV>l+\\H!Z(nuZ{4{$qV)/>P3iMWKE ý|ɐO5#T[NY=.]MuOul'5Vq=Ԏtu#a SX<.n'm)?*gl~u]"VV/X # 9UޏÞA.u'__#N%ߊi#%vE?N TvI\xZˏ~+6sj# kuzU"#fS=.DCc.a$a [uk0۹GnAon;e-qqD$T3>1>TnctgLnFٵYUƢ&$!,\w=rwTOj|D+fn9St#Y$+ !@\(k"G@=dH71$ɩ簦. wY}3 ՘dYN9cRt[V6k#qm?1MW&fްn6Ja$m4(L$aZ̒6) ^b*1fةhiw^kgS$!R4/o])͗&ޫd]6g蓕 aݻczmdĹnP;RE U+;#,ak2Ek 0W˳CQr8-ۻBZaEQ5$:s5]zv]#GFI$mЍCrpYqʙ$!V;x\@b1km6Α|k}AqZ*e5$!V=T]+M᱙(rC2zߨ$h԰Vvbj U׊\&]Jƹ $ܨZ`θW(^m)kkg]믙A}e/<[$dM  ܱww BWMD[=ZPq$匩絃|F~]U&(0ڍ1H~KS>RZoVJP$(a,xlɈ˽V !n։ 8?9=vBF$bSSOs#wZ)Q7=np=()z/TKs1qX#YTVy9 թPZjO %+-nYFZe$\¦V0]Iw83oז8bdvb#!SsT}wB}}1K~_S8SB$Hlz'Tz.JzH9>S$֭y:8$h$]XyLQ+@MX8{  qx9G0j~ $!fTMz%_iVm~[$2#!º0+|15'');ni艘uڛ#YTd:w1~w< N|'EgÛZ`iz;,n'#qZOrE^Ҡ2BwiW8 ɴ 9 c!uZsϒGvmJ:CM8G%#YTxh޼M[ b ri.{%H8w rcc4c[Qwa5MSwwRj:82T OggSb~r-++31/**),00.-.).,+,/--/,2)*.+,,-11,*+.1-*.)+)$aRJ+JmТəUyKƝy X]ӹ@}"Y$yvfFm.qh-83$:!t׽2կj2f;Q[[R㜅%([7$!j@oAҿ4E3sj3J)#iJzdqv2(lrI41af IknKޝ^+ R4Y3aDp#\Z bzJ )HxeABn* ة]!$%MBbeoLD1g{0fc#aNOY1obO% fgٹo[Sm!g1c;{ -$(pX_gȥ Q3 3IQV:`0u$!֓2ǭ䧕 s͡mݝ&;N$a3wTxJש 1,rr~adjθ#Tmsp63=:V=N5-ѝOis'Y6N$Y$òoצxRBv{$ 77{+d?'c4X Ja b#vm^ס'&UU^;OVǶ Scv]~Mg^Smd;W:WFɂՕUR$I&54UЈTI]YK,e_o Qr #!ZnN[S#nDs kkjkBsv@$YTSvy;TJdxzwZW/Dqײz٫P?#aZfY ~zXfP{3dhd{I^#qZ#B|ehhD{*ZwJqŔr$a6gqb\߶6~j歝r[c2΍w#ٮZo8y~L3=NUf*z7v Hn$TC;~~ dUrs8wq̹$!T4Yq}LB+ڥ9_b.ffe#T"i:z|`^-N[&D]1wfu"8˚4$IE:O-嵫~rƞPJ$hDfL&>$(unU_[ŔiZֳsdIoXyA GKh'[!i=$ɩZJVVA łN9SI&؜M3Y({q)A$ٮZEtt,N9Jϳ=Skjw4M"N* ca6OYˮ3C3N3jbB6tΪC$ai͜@ևxD~/oAԅλ->]tu.&؛"$=<8v^ަi[2Z\Xt⸇Į?$YT'C\.\8i36rS24)Y[]f1%j ~jLؗ[VjUY" 2jKC7>:00$\6mr3( reference libFLAC 1.2.1 20070917 ?~=ӻ/w?o~gfzs>g9gysݹϿ33]9?^ztϥ.ɜ>|3f۝~wMMs{^?gsY󛞶M>}IO/|Od&{399f7_>o$/?ϖ3˙M=̳>O'$s'Zug{/ϧ%w?ξ{z{\}9w2w?7<~y˯?ys?';s>u}׿Ϲss+93?+l9=ɧo>N\|UL?r_s_m_wy6???_tܷouzs&tͺynow2r5s濓漥_yNi=3j/9^<=?|g>yϒ-'Nߙ{_>~?O5|_/>o3f_s?s<\_ӟgs;?S﹙?'{s?:'3yO3<ϻ>Y߽vgY<Z~?ӿ~lܛOw9L$_Ogrrϛ^S^͟$?>O}/w|3OL?=N>K|ys?'og~y~fݟ?7+Lӿ9}OgO%w_}ɓgy}'?9>_g|*|?Iw>jsrs&W'g۾s_[<-Oגs<<9)|3em>[>Oϓ%45gϷ7Lw~O'.:>3wS9yW?s]?o?}62Lv˧yyfRK'|3͟sʗ6?O7߳}ygy=/?e?i>[}fWylOOzM}穻9jgwv99tLJwy~:~~||[yoӓ???k>ӼM_w?o&sy'ݖ3Ͽd?~n&/w=>zL/ϝOۓ_;}~y}'?9?e|;ߟ9Y?'2rwwI}Ot%g3yg?}?^k'}6gI[sK\g3?uK?d]~~'߿fwf缟WsϟOryfӜ7\gɧ?|Ͽ?7-ϙ94i|:}湙|~5^}͛[sOoo-O=r|u?OI$ig$;{&w'9~ue?9>W|w3wyL\'MwϤgs.}yy3d?}?mg'?ڒVz?%y[K6O)9g<>y_~w-3ww'lf2e{_?{OϗvOv~|~7y$ϟӟ秜wyw|}7<}d۵?L~g=K'3O7w=߹|es<}w|wsOV~o5t'?r+eYvO92~y~Y7e|9=f}y̟7<{|yo}{^f<ݞso9r65?NNo'4mM{eIMs[?_Sg$e]^o9{z~Il|?>߿y|<92O\~I;>~|3濺~3}yJ~dLgyܿ<_߼oZ~w?_<>gʛo~[O+gܿ}?oRdߓ3yNg}Y䟟_|s&%O7n}ϙ?_2noY>?ܙ^Լ?rm矹~o3/ng:O>}|~stw79'4gɛ{9O3w6[s3RuOgww?rJYly?i3_lf?˼ϓ?ݜ_w5~_ϟ?vw?;s3|vwϛy?Ov97loϓ俞yO̜9O%7d˟>?-Ϸ}>O??ܟgng?fϳ}3r_s~}^j~of&{s읝/gIΟ;?&N~=~{r~Ϝ)v?RsM?/ts|d>MoϜ?}Ҧwi4N2翙3?ϳΖyolg:'O˖|?_=?7ϙ*~ɓgϞo$y3do&{}3{Ϫ\<{Ϝ?OS9۟>w?/ygwy3>ϼs̗}?|?O|Ӿly{8ssy-rN\Y;黺e|Κyym|Ԟg;/??'oMoLsZgK~yO矿g^~>\s^g?y~]K?m?fnsyϞIL?i-y˿3?ϳ%fy~=zr|{;K糥o3J{w_~Wh]t jUVJUUWJTꪺꪪJRJ]TUU.U}+*R*UTtWJUIUUtvjUUI|U_UUUU*RU+UWTUUU*T˥UJ.UJUjRꫪꪫꪕꮥU*UUUګ_UUuTRꪩuUiUUKU*UU*UJUTUjRJ]]}TUJWJUUUUUUJU_TTWU%UUUUU]JZUU*]UZJU*ժzTRJZIUIJ)tUUuW)JJUZK%UwuU)uJRUjUWWJ*RTWTUUUJU]}uJJJU*UUUU*IwUW_UU֥RU]U]UUWTU]]UUU*.R.wU]UZ]UU*UUW]RTJ_UUUԹUrJJ*ꪤW}%UJJUjUuURUUU*UUW]RU*ꪥW}TU*J]UҪUUuU]WU]TԾUVIuUKTRKKWRWUWUKꪥ)WZuIWuuUUUuUUIUZU]TU)WuRꪪWU]vUK]*Pmopidy-0.17.0/tests/data/song3.mp3000066400000000000000000000222201224420023200166120ustar00rootroot00000000000000ID3vTIT2titleTPE1artistTALBalbumTDRC2010TRCK01/02HXingA  ""%)),00577<_ѣ;DNwتĪ5giԷ}n{(n(D^AI11Jy{ui <|;w4fk}~NA&. >D!*Poцz4;B8lʮQVwygxjr{ʷ0g(, ((z8L $fFVmA&DbqAFI`M`d1LC#; ޥvvmHݻQL.IMJf*"g?=!uF9')bC%dH4<#:q98oEd:PvM]V>v~yVˏ뫿W[(aB: ,6if<7!"cH'-h|16(V$> BE˴SJ/PnjݩsxVfx]M|ӥ͔bq T-h  0ӹM݌M89\R#;ޖw5,:,,X(EklUu uZ;Tvҥt ̪fy(~8,;]:hY*{<2M)(4;8Rk/_P'C0Ԋ?^Jgu(f(YVy+$r5AoGN7\e8(28L7f^Y`H$ ,@dF04ER'zX֑f4ZiIzT彡1S {UVgz;[0qvʯgtDP"=esF֚G.Kd/9:FtPYْW5W}\83lӅT xa/';­gMmV(8L_捡'v.ހms@0` z&u9`+A1e Eq [ܷ -pĊY̓3JUEVuMVxw'(f8L8 H gqbWbVxZSY琔}rlpiq&PM˅@9X`BC rm~mXV^.g2->(r(dVh9ѵK%X駦I(I9X aF&E"PJR*Qd{%  > hVS*K,ql[hm?Fnt^cW]PfkSC³VxGu.ed|{%e4Ѻ$ *R /~:FTiQbKUOV`e`R':uAH5ҝ`x:( v8R i@T*:*t6 Jtkg#QY?23^L@_4H!Pq[:!١Q*I(h\dXM`BVxlN8. ;\e%[(r8RL C=v6ShG& !U+}6QTa `>f&`{BK nm|R\ v@WaVx/$ ֩*pZ$T FM^;(/Ar8 $fei%vgo~n)UlًnQ/wNU)9udGojާg?$W˟(4݃n\\ٷSkRY\DYDFLĮ&A29R8Lѫ@]A$UlQQ(t|ۄH5+`qqz|Rog( ^BL:^oOD-cs!vNMw(U*#bx O"hP6,5i"~L*GޤZ$yT-\ީA7*gy'Ո씼!l! ( 88 *pW gsA9j |6%bVYjT7W+Zu4ޚԎ3]֭UVxӓhwwJqSRk9eYHh9ٗzo7w <<"y>Z8 -ċ {±c+T9 j1e{q& eEFAEw)( ^P톏Adb"S"QT"*W!$!VPg(1}9Jp`j DjfHTu/f؏ `P1gJ@xQU Rgxʟ;6z{(!zBLls1:FN"2): a!=FJ;H "ei[4QI',, <Sg.co=qw8S¶MEFƨ{CMI|TKs˃}<*LК/hvK!V?!wGd (bIJJ0@u,y3Q8H|bwIYBY:8$P!gYBn^~} {ֵBjVwvU9P1RӢVy (Yb8L "/Tuµ܌@O<^A^!A`q-;J A5,@,asAаrw\tț|\TB$0`vr0ugx 6\zRCNUm!r" 7((q~RLP$p'HCFOFttig(겏Rh4<\S=,`H2XyީG)weloʁIr0Q3h,,IVz$nRU7 YEˁTTFnhCj(1z8(+ MJǔ 64 Tfļ+Hy'$"4znq/#TKrwp#.\V.|d'53%2= 1T8=bGHal-0N BQnPP$elf m__U=noz bŕVwziIKIf(fBLʌ6JN'ZԽ*ʓ O-;niJp|ZF `dr+y{AV":ׄf 6$Pz`b܄.pLVxxnLɖnt/TR(xҘ(ٺ8P 9LVա2mX֥*U8dCR:G HI ʁ[i}i`L}6bV{%sc,-P5"y7A"(.a:F`2aJAmN>@H[s,%"aAׁ1 Fs-s+Uqj\X&(K2 WWﮘ>][IFX0ӻ\!K痮[ڴtW^(ySA H%2qMaETԙe1L8+t>_@ Ejj=c^i&:zPȨ*&.%Wjm `B3-L7E`wsKbXX:O 8a 5#Cb H <A &+gG"0,R)')0`}2?bz?/(ՍpHe*VC:<ߍ/Ԩc3C.jLAME3.98.2UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU*HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUeHUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUTAGtitle artist album 2010 mopidy-0.17.0/tests/data/song3.ogg000066400000000000000000000207371224420023200167020ustar00rootroot00000000000000OggSb~|vorbis@@OggSb~X -vorbisXiph.Org libVorbis I 20090709vorbisBCV R!%SJcRR)cP[Gc9F!dSI{O*XJRX)ESLSIR)EcSH!S1esKI %lMtKc1FcZJc1EcRRIs:f%d:Fb|0:B(R-[S-KiasJjc1S(АU@BCV P EQАU@EqqG$BCV@((#IdYeYy/.FuL*CCc3C LcN4 23Ő2[,.!+(b 9dR"瘔NJQ(K[1Q(eBŌRT@@PhȊ 0)B)s1 1 d)NJ圓Ic1sNJrIɤ`!"0HY&gꉢZgigj뚪ʖ癦gꙦ꺦l.jۮkl+ʺʶKgnkۚjʮm-l,fl-,ڶ*˺/n,lںk,*˾1۶˺.'뙪몮k۪ںl-+۪,+˶,+ۦʲ*˾ʲn,*1̶*˺ʲn nʲ ˬۺ1﫲-, 2>ct]_WmYV}cuaYm[][gn nʭ ˲ڶ̺,.|[ڶ麺nʲ˺.uWF}ն}_e߷_ið,k/뺰/,m+0ۺܾ, ˪۾ҵue}+ p0 "@r)b BB*cR2dI)JIbLJ朔1)J)RZ*Ji-bJPJkJIbL1&%sNJ朔Rk%2(eJ J*b朤:+J*1b VJjc+1Z!KJZm1Z5bLJ朔9*%J*eI 9(b*)9I2ȨZ+Rc)b)CI-Z,bTS'ŒR%[j VJ[k1cK+ZlZ5TcJc1k=bM1ZՖ[̵NJkKJ1b1Ji[))Z\C)Z,bVcj[ZkV[.b=kXSm 8P Y D0F)ǜ(sR* RRʜPJKsJI)RjRRj lДXА@*q4MUu}_,QTUוmW,MUUvm[5QTU׵m~MUUveٶm۲n èk۲m먮ۺۺ/T]Ym[u׶uu]mn# G!tBOp*auBCV1J3H1cL1Ƙ@!+(9s9s9s9s1c1c1c1c1c1cLNNPhJ @!))RJSAI)RJJ)RuRJ)"RJ)II)RJ):J)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)RJ)TJ)RJ)RJ)`` +IgrBiTRJ%UA(%JJ)RJ*RJ)A(PJ)%RJ(%J TB+RJB)TJ)%A(!BIBHtTR)!RJ)%:RK-RZJtR)RjR VJI%ZI%JI%RK)TRI%R*%ZJRHRJRJ%JJRj%Rj!JI)J)TBIRZJ-JITRIJI)RJK)JJZI)RJ)RK%TJ %RIRK)R@FTZf\y(d@@ 0@P0 A0GќBCOggS]b~u^+)-+**+.+-+,+,+0/,/,*0/-**,-.,+/+**/)/++--+-,.+**...-/*-+'-*+,.0+,++/)./-..+//0(((-+**,,,++*+*":5Ct+:1V3IꙨ1o2 #a #vNYVIK.iܳd)Ԝ֦ $Z=l;_ gC[#%[Rus=*=zscP)n(7|pA.b՟){*Xڠc].##am6[J>t+lMn/{6p$' si);rqF#介Iy `$uB+<%w{C7"aQ] G\]l$YT͆a޻V_=ݮdc7j!-ٶʨmf4^0cdaLRIi];#(tx+年WZ9ziVIY'hkV=S:#YKZMziȇ:kƠXR}P |48Mz#Y$MJeϬ zQxTlHxR:c!V6?[#ꗪC3Ĉ+wۜw$@8K5ł,>'i`CQWy cNT:P#Y$e>Gڨ]񶘥/'qы]v2ó|nk$\R=4F}Zs}0xR/}3bOBt8ͶGr#EZsc Sj?w'bae ѭotYRFsu7#IAn ,PW,VxiiiȼʦJDV+.%@2gQi[[[s7)a*U:?<~$Im_.*=l+ i^22WJSqp $\jX6a{xYc;sž$c)GrpW%!Zo'¯e.+eʳL&K x$ZgNE 1N9m=< HT\FS\0ǝ$qZ/`j'ip6-qprqأ$)smTuflcL9]0O3 k7b@H|i.*i*w%|$!VzǻT \eUg.l}c%dfoST"m]ugG%4%tc)zL3dcIY;܋]^BXV>l+\\H!Z(nuZ{4{$qV)/>P3iMWKE ý|ɐO5#T[NY=.]MuOul'5Vq=Ԏtu#a SX<.n'm)?*gl~u]"VV/X # 9UޏÞA.u'__#N%ߊi#%vE?N TvI\xZˏ~+6sj# kuzU"#fS=.DCc.a$a [uk0۹GnAon;e-qqD$T3>1>TnctgLnFٵYUƢ&$!,\w=rwTOj|D+fn9St#Y$+ !@\(k"G@=dH71$ɩ簦. wY}3 ՘dYN9cRt[V6k#qm?1MW&fްn6Ja$m4(L$aZ̒6) ^b*1fةhiw^kgS$!R4/o])͗&ޫd]6g蓕 aݻczmdĹnP;RE U+;#,ak2Ek 0W˳CQr8-ۻBZaEQ5$:s5]zv]#GFI$mЍCrpYqʙ$!V;x\@b1km6Α|k}AqZ*e5$!V=T]+M᱙(rC2zߨ$h԰Vvbj U׊\&]Jƹ $ܨZ`θW(^m)kkg]믙A}e/<[$dM  ܱww BWMD[=ZPq$匩絃|F~]U&(0ڍ1H~KS>RZoVJP$(a,xlɈ˽V !n։ 8?9=vBF$bSSOs#wZ)Q7=np=()z/TKs1qX#YTVy9 թPZjO %+-nYFZe$\¦V0]Iw83oז8bdvb#!SsT}wB}}1K~_S8SB$Hlz'Tz.JzH9>S$֭y:8$h$]XyLQ+@MX8{  qx9G0j~ $!fTMz%_iVm~[$2#!º0+|15'');ni艘uڛ#YTd:w1~w< N|'EgÛZ`iz;,n'#qZOrE^Ҡ2BwiW8 ɴ 9 c!uZsϒGvmJ:CM8G%#YTxh޼M[ b ri.{%H8w rcc4c[Qwa5MSwwRj:82T OggSb~r-++31/**),00.-.).,+,/--/,2)*.+,,-11,*+.1-*.)+)$aRJ+JmТəUyKƝy X]ӹ@}"Y$yvfFm.qh-83$:!t׽2կj2f;Q[[R㜅%([7$!j@oAҿ4E3sj3J)#iJzdqv2(lrI41af IknKޝ^+ R4Y3aDp#\Z bzJ )HxeABn* ة]!$%MBbeoLD1g{0fc#aNOY1obO% fgٹo[Sm!g1c;{ -$(pX_gȥ Q3 3IQV:`0u$!֓2ǭ䧕 s͡mݝ&;N$a3wTxJש 1,rr~adjθ#Tmsp63=:V=N5-ѝOis'Y6N$Y$òoצxRBv{$ 77{+d?'c4X Ja b#vm^ס'&UU^;OVǶ Scv]~Mg^Smd;W:WFɂՕUR$I&54UЈTI]YK,e_o Qr #!ZnN[S#nDs kkjkBsv@$YTSvy;TJdxzwZW/Dqײz٫P?#aZfY ~zXfP{3dhd{I^#qZ#B|ehhD{*ZwJqŔr$a6gqb\߶6~j歝r[c2΍w#ٮZo8y~L3=NUf*z7v Hn$TC;~~ dUrs8wq̹$!T4Yq}LB+ڥ9_b.ffe#T"i:z|`^-N[&D]1wfu"8˚4$IE:O-嵫~rƞPJ$hDfL&>$(unU_[ŔiZֳsdIoXyA GKh'[!i=$ɩZJVVA łN9SI&؜M3Y({q)A$ٮZEtt,N9Jϳ=Skjw4M"N* ca6OYˮ3C3N3jbB6tΪC$ai͜@ևxD~/oAԅλ->]tu.&؛"$=<8v^ަi[2Z\Xt⸇Į?$YT'C\.\8i36rS24)Y[]f1%j ~jLؗ[VjUY" 2jKC7>:00$\6m.+)'] = \ expected_handler (handler, kwargs) = self.dispatcher._find_handler( 'known_command an_arg') self.assertEqual(handler, expected_handler) self.assertIn('arg1', kwargs) self.assertEqual(kwargs['arg1'], 'an_arg') def test_handling_unknown_request_yields_error(self): result = self.dispatcher.handle_request('an unhandled request') self.assertEqual(result[0], 'ACK [5@0] {} unknown command "an"') def test_handling_known_request(self): expected = 'magic' request_handlers['known request'] = lambda x: expected result = self.dispatcher.handle_request('known request') self.assertIn('OK', result) self.assertIn(expected, result) mopidy-0.17.0/tests/frontends/mpd/exception_test.py000066400000000000000000000036041224420023200224250ustar00rootroot00000000000000from __future__ import unicode_literals import unittest from mopidy.frontends.mpd.exceptions import ( MpdAckError, MpdPermissionError, MpdUnknownCommand, MpdSystemError, MpdNotImplemented) class MpdExceptionsTest(unittest.TestCase): def test_key_error_wrapped_in_mpd_ack_error(self): try: try: raise KeyError('Track X not found') except KeyError as e: raise MpdAckError(e[0]) except MpdAckError as e: self.assertEqual(e.message, 'Track X not found') def test_mpd_not_implemented_is_a_mpd_ack_error(self): try: raise MpdNotImplemented except MpdAckError as e: self.assertEqual(e.message, 'Not implemented') def test_get_mpd_ack_with_default_values(self): e = MpdAckError('A description') self.assertEqual(e.get_mpd_ack(), 'ACK [0@0] {} A description') def test_get_mpd_ack_with_values(self): try: raise MpdAckError('A description', index=7, command='foo') except MpdAckError as e: self.assertEqual(e.get_mpd_ack(), 'ACK [0@7] {foo} A description') def test_mpd_unknown_command(self): try: raise MpdUnknownCommand(command='play') except MpdAckError as e: self.assertEqual( e.get_mpd_ack(), 'ACK [5@0] {} unknown command "play"') def test_mpd_system_error(self): try: raise MpdSystemError('foo') except MpdSystemError as e: self.assertEqual( e.get_mpd_ack(), 'ACK [52@0] {} foo') def test_mpd_permission_error(self): try: raise MpdPermissionError(command='foo') except MpdPermissionError as e: self.assertEqual( e.get_mpd_ack(), 'ACK [4@0] {foo} you don\'t have permission for "foo"') mopidy-0.17.0/tests/frontends/mpd/protocol/000077500000000000000000000000001224420023200206545ustar00rootroot00000000000000mopidy-0.17.0/tests/frontends/mpd/protocol/__init__.py000066400000000000000000000044651224420023200227760ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest import pykka from mopidy import core from mopidy.backends import dummy from mopidy.frontends.mpd import session class MockConnection(mock.Mock): def __init__(self, *args, **kwargs): super(MockConnection, self).__init__(*args, **kwargs) self.host = mock.sentinel.host self.port = mock.sentinel.port self.response = [] def queue_send(self, data): lines = (line for line in data.split('\n') if line) self.response.extend(lines) class BaseTestCase(unittest.TestCase): def get_config(self): return { 'mpd': { 'password': None, } } def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.connection = MockConnection() self.session = session.MpdSession( self.connection, config=self.get_config(), core=self.core) self.dispatcher = self.session.dispatcher self.context = self.dispatcher.context def tearDown(self): pykka.ActorRegistry.stop_all() def sendRequest(self, request): self.connection.response = [] request = '%s\n' % request.encode('utf-8') self.session.on_receive({'received': request}) return self.connection.response def assertNoResponse(self): self.assertEqual([], self.connection.response) def assertInResponse(self, value): self.assertIn( value, self.connection.response, 'Did not find %s in %s' % ( repr(value), repr(self.connection.response))) def assertOnceInResponse(self, value): matched = len([r for r in self.connection.response if r == value]) self.assertEqual( 1, matched, 'Expected to find %s once in %s' % ( repr(value), repr(self.connection.response))) def assertNotInResponse(self, value): self.assertNotIn( value, self.connection.response, 'Found %s in %s' % ( repr(value), repr(self.connection.response))) def assertEqualResponse(self, value): self.assertEqual(1, len(self.connection.response)) self.assertEqual(value, self.connection.response[0]) mopidy-0.17.0/tests/frontends/mpd/protocol/audio_output_test.py000066400000000000000000000030111224420023200250010ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class AudioOutputHandlerTest(protocol.BaseTestCase): def test_enableoutput(self): self.core.playback.mute = False self.sendRequest('enableoutput "0"') self.assertInResponse('OK') self.assertEqual(self.core.playback.mute.get(), True) def test_enableoutput_unknown_outputid(self): self.sendRequest('enableoutput "7"') self.assertInResponse('ACK [50@0] {enableoutput} No such audio output') def test_disableoutput(self): self.core.playback.mute = True self.sendRequest('disableoutput "0"') self.assertInResponse('OK') self.assertEqual(self.core.playback.mute.get(), False) def test_disableoutput_unknown_outputid(self): self.sendRequest('disableoutput "7"') self.assertInResponse( 'ACK [50@0] {disableoutput} No such audio output') def test_outputs_when_unmuted(self): self.core.playback.mute = False self.sendRequest('outputs') self.assertInResponse('outputid: 0') self.assertInResponse('outputname: Mute') self.assertInResponse('outputenabled: 0') self.assertInResponse('OK') def test_outputs_when_muted(self): self.core.playback.mute = True self.sendRequest('outputs') self.assertInResponse('outputid: 0') self.assertInResponse('outputname: Mute') self.assertInResponse('outputenabled: 1') self.assertInResponse('OK') mopidy-0.17.0/tests/frontends/mpd/protocol/authentication_test.py000066400000000000000000000043451224420023200253120ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class AuthenticationActiveTest(protocol.BaseTestCase): def get_config(self): config = super(AuthenticationActiveTest, self).get_config() config['mpd']['password'] = 'topsecret' return config def test_authentication_with_valid_password_is_accepted(self): self.sendRequest('password "topsecret"') self.assertTrue(self.dispatcher.authenticated) self.assertInResponse('OK') def test_authentication_with_invalid_password_is_not_accepted(self): self.sendRequest('password "secret"') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse('ACK [3@0] {password} incorrect password') def test_anything_when_not_authenticated_should_fail(self): self.sendRequest('any request at all') self.assertFalse(self.dispatcher.authenticated) self.assertEqualResponse( u'ACK [4@0] {any} you don\'t have permission for "any"') def test_close_is_allowed_without_authentication(self): self.sendRequest('close') self.assertFalse(self.dispatcher.authenticated) def test_commands_is_allowed_without_authentication(self): self.sendRequest('commands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_notcommands_is_allowed_without_authentication(self): self.sendRequest('notcommands') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') def test_ping_is_allowed_without_authentication(self): self.sendRequest('ping') self.assertFalse(self.dispatcher.authenticated) self.assertInResponse('OK') class AuthenticationInactiveTest(protocol.BaseTestCase): def test_authentication_with_anything_when_password_check_turned_off(self): self.sendRequest('any request at all') self.assertTrue(self.dispatcher.authenticated) self.assertEqualResponse('ACK [5@0] {} unknown command "any"') def test_any_password_is_not_accepted_when_password_check_turned_off(self): self.sendRequest('password "secret"') self.assertEqualResponse('ACK [3@0] {password} incorrect password') mopidy-0.17.0/tests/frontends/mpd/protocol/channels_test.py000066400000000000000000000015231224420023200240610ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class ChannelsHandlerTest(protocol.BaseTestCase): def test_subscribe(self): self.sendRequest('subscribe "topic"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_unsubscribe(self): self.sendRequest('unsubscribe "topic"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_channels(self): self.sendRequest('channels') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_readmessages(self): self.sendRequest('readmessages') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sendmessage(self): self.sendRequest('sendmessage "topic" "a message"') self.assertEqualResponse('ACK [0@0] {} Not implemented') mopidy-0.17.0/tests/frontends/mpd/protocol/command_list_test.py000066400000000000000000000051471224420023200247450ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class CommandListsTest(protocol.BaseTestCase): def test_command_list_begin(self): response = self.sendRequest('command_list_begin') self.assertEquals([], response) def test_command_list_end(self): self.sendRequest('command_list_begin') self.sendRequest('command_list_end') self.assertInResponse('OK') def test_command_list_end_without_start_first_is_an_unknown_command(self): self.sendRequest('command_list_end') self.assertEqualResponse( 'ACK [5@0] {} unknown command "command_list_end"') def test_command_list_with_ping(self): self.sendRequest('command_list_begin') self.assertTrue(self.dispatcher.command_list_receiving) self.assertFalse(self.dispatcher.command_list_ok) self.assertEqual([], self.dispatcher.command_list) self.sendRequest('ping') self.assertIn('ping', self.dispatcher.command_list) self.sendRequest('command_list_end') self.assertInResponse('OK') self.assertFalse(self.dispatcher.command_list_receiving) self.assertFalse(self.dispatcher.command_list_ok) self.assertEqual([], self.dispatcher.command_list) def test_command_list_with_error_returns_ack_with_correct_index(self): self.sendRequest('command_list_begin') self.sendRequest('play') # Known command self.sendRequest('paly') # Unknown command self.sendRequest('command_list_end') self.assertEqualResponse('ACK [5@1] {} unknown command "paly"') def test_command_list_ok_begin(self): response = self.sendRequest('command_list_ok_begin') self.assertEquals([], response) def test_command_list_ok_with_ping(self): self.sendRequest('command_list_ok_begin') self.assertTrue(self.dispatcher.command_list_receiving) self.assertTrue(self.dispatcher.command_list_ok) self.assertEqual([], self.dispatcher.command_list) self.sendRequest('ping') self.assertIn('ping', self.dispatcher.command_list) self.sendRequest('command_list_end') self.assertInResponse('list_OK') self.assertInResponse('OK') self.assertFalse(self.dispatcher.command_list_receiving) self.assertFalse(self.dispatcher.command_list_ok) self.assertEqual([], self.dispatcher.command_list) # FIXME this should also include the special handling of idle within a # command list. That is that once a idle/noidle command is found inside a # commad list, the rest of the list seems to be ignored. mopidy-0.17.0/tests/frontends/mpd/protocol/connection_test.py000066400000000000000000000015241224420023200244260ustar00rootroot00000000000000from __future__ import unicode_literals from mock import patch from tests.frontends.mpd import protocol class ConnectionHandlerTest(protocol.BaseTestCase): def test_close_closes_the_client_connection(self): with patch.object(self.session, 'close') as close_mock: self.sendRequest('close') close_mock.assertEqualResponsecalled_once_with() self.assertEqualResponse('OK') def test_empty_request(self): self.sendRequest('') self.assertEqualResponse('OK') self.sendRequest(' ') self.assertEqualResponse('OK') def test_kill(self): self.sendRequest('kill') self.assertEqualResponse( 'ACK [4@0] {kill} you don\'t have permission for "kill"') def test_ping(self): self.sendRequest('ping') self.assertEqualResponse('OK') mopidy-0.17.0/tests/frontends/mpd/protocol/current_playlist_test.py000066400000000000000000000503521224420023200256750ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.models import Track from tests.frontends.mpd import protocol class CurrentPlaylistHandlerTest(protocol.BaseTestCase): def test_add(self): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('add "dummy://foo"') self.assertEqual(len(self.core.tracklist.tracks.get()), 6) self.assertEqual(self.core.tracklist.tracks.get()[5], needle) self.assertEqualResponse('OK') def test_add_with_uri_not_found_in_library_should_ack(self): self.sendRequest('add "dummy://foo"') self.assertEqualResponse( 'ACK [50@0] {add} directory or file not found') def test_add_with_empty_uri_should_add_all_known_tracks_and_ok(self): self.sendRequest('add ""') # TODO check that we add all tracks (we currently don't) self.assertInResponse('OK') def test_addid_without_songpos(self): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('addid "dummy://foo"') self.assertEqual(len(self.core.tracklist.tracks.get()), 6) self.assertEqual(self.core.tracklist.tracks.get()[5], needle) self.assertInResponse( 'Id: %d' % self.core.tracklist.tl_tracks.get()[5].tlid) self.assertInResponse('OK') def test_addid_with_empty_uri_acks(self): self.sendRequest('addid ""') self.assertEqualResponse('ACK [50@0] {addid} No such song') def test_addid_with_songpos(self): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('addid "dummy://foo" "3"') self.assertEqual(len(self.core.tracklist.tracks.get()), 6) self.assertEqual(self.core.tracklist.tracks.get()[3], needle) self.assertInResponse( 'Id: %d' % self.core.tracklist.tl_tracks.get()[3].tlid) self.assertInResponse('OK') def test_addid_with_songpos_out_of_bounds_should_ack(self): needle = Track(uri='dummy://foo') self.backend.library.dummy_library = [ Track(), Track(), needle, Track()] self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('addid "dummy://foo" "6"') self.assertEqualResponse('ACK [2@0] {addid} Bad song index') def test_addid_with_uri_not_found_in_library_should_ack(self): self.sendRequest('addid "dummy://foo"') self.assertEqualResponse('ACK [50@0] {addid} No such song') def test_clear(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('clear') self.assertEqual(len(self.core.tracklist.tracks.get()), 0) self.assertEqual(self.core.playback.current_track.get(), None) self.assertInResponse('OK') def test_delete_songpos(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest( 'delete "%d"' % self.core.tracklist.tl_tracks.get()[2].tlid) self.assertEqual(len(self.core.tracklist.tracks.get()), 4) self.assertInResponse('OK') def test_delete_songpos_out_of_bounds(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('delete "5"') self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.assertEqualResponse('ACK [2@0] {delete} Bad song index') def test_delete_open_range(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('delete "1:"') self.assertEqual(len(self.core.tracklist.tracks.get()), 1) self.assertInResponse('OK') def test_delete_closed_range(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('delete "1:3"') self.assertEqual(len(self.core.tracklist.tracks.get()), 3) self.assertInResponse('OK') def test_delete_range_out_of_bounds(self): self.core.tracklist.add( [Track(), Track(), Track(), Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.sendRequest('delete "5:7"') self.assertEqual(len(self.core.tracklist.tracks.get()), 5) self.assertEqualResponse('ACK [2@0] {delete} Bad song index') def test_deleteid(self): self.core.tracklist.add([Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.sendRequest('deleteid "1"') self.assertEqual(len(self.core.tracklist.tracks.get()), 1) self.assertInResponse('OK') def test_deleteid_does_not_exist(self): self.core.tracklist.add([Track(), Track()]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.sendRequest('deleteid "12345"') self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.assertEqualResponse('ACK [50@0] {deleteid} No such song') def test_move_songpos(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('move "1" "0"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[1].name, 'a') self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_move_open_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('move "2:" "0"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'c') self.assertEqual(tracks[1].name, 'd') self.assertEqual(tracks[2].name, 'e') self.assertEqual(tracks[3].name, 'f') self.assertEqual(tracks[4].name, 'a') self.assertEqual(tracks[5].name, 'b') self.assertInResponse('OK') def test_move_closed_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('move "1:3" "0"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'b') self.assertEqual(tracks[1].name, 'c') self.assertEqual(tracks[2].name, 'a') self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_moveid(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('moveid "4" "2"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'b') self.assertEqual(tracks[2].name, 'e') self.assertEqual(tracks[3].name, 'c') self.assertEqual(tracks[4].name, 'd') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_moveid_with_tlid_not_found_in_tracklist_should_ack(self): self.sendRequest('moveid "9" "0"') self.assertEqualResponse( 'ACK [50@0] {moveid} No such song') def test_playlist_returns_same_as_playlistinfo(self): playlist_response = self.sendRequest('playlist') playlistinfo_response = self.sendRequest('playlistinfo') self.assertEqual(playlist_response, playlistinfo_response) def test_playlistfind(self): self.sendRequest('playlistfind "tag" "needle"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistfind_by_filename_not_in_tracklist(self): self.sendRequest('playlistfind "filename" "file:///dev/null"') self.assertEqualResponse('OK') def test_playlistfind_by_filename_without_quotes(self): self.sendRequest('playlistfind filename "file:///dev/null"') self.assertEqualResponse('OK') def test_playlistfind_by_filename_in_tracklist(self): self.core.tracklist.add([Track(uri='file:///exists')]) self.sendRequest('playlistfind filename "file:///exists"') self.assertInResponse('file: file:///exists') self.assertInResponse('Id: 0') self.assertInResponse('Pos: 0') self.assertInResponse('OK') def test_playlistid_without_songid(self): self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid') self.assertInResponse('Title: a') self.assertInResponse('Title: b') self.assertInResponse('OK') def test_playlistid_with_songid(self): self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid "1"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Id: 0') self.assertInResponse('Title: b') self.assertInResponse('Id: 1') self.assertInResponse('OK') def test_playlistid_with_not_existing_songid_fails(self): self.core.tracklist.add([Track(name='a'), Track(name='b')]) self.sendRequest('playlistid "25"') self.assertEqualResponse('ACK [50@0] {playlistid} No such song') def test_playlistinfo_without_songpos_or_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('playlistinfo') self.assertInResponse('Title: a') self.assertInResponse('Pos: 0') self.assertInResponse('Title: b') self.assertInResponse('Pos: 1') self.assertInResponse('Title: c') self.assertInResponse('Pos: 2') self.assertInResponse('Title: d') self.assertInResponse('Pos: 3') self.assertInResponse('Title: e') self.assertInResponse('Pos: 4') self.assertInResponse('Title: f') self.assertInResponse('Pos: 5') self.assertInResponse('OK') def test_playlistinfo_with_songpos(self): # Make the track's CPID not match the playlist position self.core.tracklist.tlid = 17 self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('playlistinfo "4"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Pos: 0') self.assertNotInResponse('Title: b') self.assertNotInResponse('Pos: 1') self.assertNotInResponse('Title: c') self.assertNotInResponse('Pos: 2') self.assertNotInResponse('Title: d') self.assertNotInResponse('Pos: 3') self.assertInResponse('Title: e') self.assertInResponse('Pos: 4') self.assertNotInResponse('Title: f') self.assertNotInResponse('Pos: 5') self.assertInResponse('OK') def test_playlistinfo_with_negative_songpos_same_as_playlistinfo(self): response1 = self.sendRequest('playlistinfo "-1"') response2 = self.sendRequest('playlistinfo') self.assertEqual(response1, response2) def test_playlistinfo_with_open_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('playlistinfo "2:"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Pos: 0') self.assertNotInResponse('Title: b') self.assertNotInResponse('Pos: 1') self.assertInResponse('Title: c') self.assertInResponse('Pos: 2') self.assertInResponse('Title: d') self.assertInResponse('Pos: 3') self.assertInResponse('Title: e') self.assertInResponse('Pos: 4') self.assertInResponse('Title: f') self.assertInResponse('Pos: 5') self.assertInResponse('OK') def test_playlistinfo_with_closed_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('playlistinfo "2:4"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Title: b') self.assertInResponse('Title: c') self.assertInResponse('Title: d') self.assertNotInResponse('Title: e') self.assertNotInResponse('Title: f') self.assertInResponse('OK') def test_playlistinfo_with_too_high_start_of_range_returns_arg_error(self): self.sendRequest('playlistinfo "10:20"') self.assertEqualResponse('ACK [2@0] {playlistinfo} Bad song index') def test_playlistinfo_with_too_high_end_of_range_returns_ok(self): self.sendRequest('playlistinfo "0:20"') self.assertInResponse('OK') def test_playlistsearch(self): self.sendRequest('playlistsearch "any" "needle"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistsearch_without_quotes(self): self.sendRequest('playlistsearch any "needle"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_plchanges_with_lower_version_returns_changes(self): self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges "0"') self.assertInResponse('Title: a') self.assertInResponse('Title: b') self.assertInResponse('Title: c') self.assertInResponse('OK') def test_plchanges_with_equal_version_returns_nothing(self): self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.assertEqual(self.core.tracklist.version.get(), 1) self.sendRequest('plchanges "1"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Title: b') self.assertNotInResponse('Title: c') self.assertInResponse('OK') def test_plchanges_with_greater_version_returns_nothing(self): self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.assertEqual(self.core.tracklist.version.get(), 1) self.sendRequest('plchanges "2"') self.assertNotInResponse('Title: a') self.assertNotInResponse('Title: b') self.assertNotInResponse('Title: c') self.assertInResponse('OK') def test_plchanges_with_minus_one_returns_entire_playlist(self): self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges "-1"') self.assertInResponse('Title: a') self.assertInResponse('Title: b') self.assertInResponse('Title: c') self.assertInResponse('OK') def test_plchanges_without_quotes_works(self): self.core.tracklist.add( [Track(name='a'), Track(name='b'), Track(name='c')]) self.sendRequest('plchanges 0') self.assertInResponse('Title: a') self.assertInResponse('Title: b') self.assertInResponse('Title: c') self.assertInResponse('OK') def test_plchangesposid(self): self.core.tracklist.add([Track(), Track(), Track()]) self.sendRequest('plchangesposid "0"') tl_tracks = self.core.tracklist.tl_tracks.get() self.assertInResponse('cpos: 0') self.assertInResponse('Id: %d' % tl_tracks[0].tlid) self.assertInResponse('cpos: 2') self.assertInResponse('Id: %d' % tl_tracks[1].tlid) self.assertInResponse('cpos: 2') self.assertInResponse('Id: %d' % tl_tracks[2].tlid) self.assertInResponse('OK') def test_shuffle_without_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.core.tracklist.version.get() self.sendRequest('shuffle') self.assertLess(version, self.core.tracklist.version.get()) self.assertInResponse('OK') def test_shuffle_with_open_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.core.tracklist.version.get() self.sendRequest('shuffle "4:"') self.assertLess(version, self.core.tracklist.version.get()) tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'b') self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[3].name, 'd') self.assertInResponse('OK') def test_shuffle_with_closed_range(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) version = self.core.tracklist.version.get() self.sendRequest('shuffle "1:3"') self.assertLess(version, self.core.tracklist.version.get()) tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'e') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_swap(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('swap "1" "4"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'e') self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'b') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_swapid(self): self.core.tracklist.add([ Track(name='a'), Track(name='b'), Track(name='c'), Track(name='d'), Track(name='e'), Track(name='f'), ]) self.sendRequest('swapid "1" "4"') tracks = self.core.tracklist.tracks.get() self.assertEqual(tracks[0].name, 'a') self.assertEqual(tracks[1].name, 'e') self.assertEqual(tracks[2].name, 'c') self.assertEqual(tracks[3].name, 'd') self.assertEqual(tracks[4].name, 'b') self.assertEqual(tracks[5].name, 'f') self.assertInResponse('OK') def test_swapid_with_first_id_unknown_should_ack(self): self.core.tracklist.add([Track()]) self.sendRequest('swapid "0" "4"') self.assertEqualResponse( 'ACK [50@0] {swapid} No such song') def test_swapid_with_second_id_unknown_should_ack(self): self.core.tracklist.add([Track()]) self.sendRequest('swapid "4" "0"') self.assertEqualResponse( 'ACK [50@0] {swapid} No such song') mopidy-0.17.0/tests/frontends/mpd/protocol/idle_test.py000066400000000000000000000155521224420023200232120ustar00rootroot00000000000000from __future__ import unicode_literals from mock import patch from mopidy.frontends.mpd.protocol.status import SUBSYSTEMS from tests.frontends.mpd import protocol class IdleHandlerTest(protocol.BaseTestCase): def idleEvent(self, subsystem): self.session.on_idle(subsystem) def assertEqualEvents(self, events): self.assertEqual(set(events), self.context.events) def assertEqualSubscriptions(self, events): self.assertEqual(set(events), self.context.subscriptions) def assertNoEvents(self): self.assertEqualEvents([]) def assertNoSubscriptions(self): self.assertEqualSubscriptions([]) def test_base_state(self): self.assertNoSubscriptions() self.assertNoEvents() self.assertNoResponse() def test_idle(self): self.sendRequest('idle') self.assertEqualSubscriptions(SUBSYSTEMS) self.assertNoEvents() self.assertNoResponse() def test_idle_disables_timeout(self): self.sendRequest('idle') self.connection.disable_timeout.assert_called_once_with() def test_noidle(self): self.sendRequest('noidle') self.assertNoSubscriptions() self.assertNoEvents() self.assertNoResponse() def test_idle_player(self): self.sendRequest('idle player') self.assertEqualSubscriptions(['player']) self.assertNoEvents() self.assertNoResponse() def test_idle_player_playlist(self): self.sendRequest('idle player playlist') self.assertEqualSubscriptions(['player', 'playlist']) self.assertNoEvents() self.assertNoResponse() def test_idle_then_noidle(self): self.sendRequest('idle') self.sendRequest('noidle') self.assertNoSubscriptions() self.assertNoEvents() self.assertOnceInResponse('OK') def test_idle_then_noidle_enables_timeout(self): self.sendRequest('idle') self.sendRequest('noidle') self.connection.enable_timeout.assert_called_once_with() def test_idle_then_play(self): with patch.object(self.session, 'stop') as stop_mock: self.sendRequest('idle') self.sendRequest('play') stop_mock.assert_called_once_with() def test_idle_then_idle(self): with patch.object(self.session, 'stop') as stop_mock: self.sendRequest('idle') self.sendRequest('idle') stop_mock.assert_called_once_with() def test_idle_player_then_play(self): with patch.object(self.session, 'stop') as stop_mock: self.sendRequest('idle player') self.sendRequest('play') stop_mock.assert_called_once_with() def test_idle_then_player(self): self.sendRequest('idle') self.idleEvent('player') self.assertNoSubscriptions() self.assertNoEvents() self.assertOnceInResponse('changed: player') self.assertOnceInResponse('OK') def test_idle_player_then_event_player(self): self.sendRequest('idle player') self.idleEvent('player') self.assertNoSubscriptions() self.assertNoEvents() self.assertOnceInResponse('changed: player') self.assertOnceInResponse('OK') def test_idle_player_then_noidle(self): self.sendRequest('idle player') self.sendRequest('noidle') self.assertNoSubscriptions() self.assertNoEvents() self.assertOnceInResponse('OK') def test_idle_player_playlist_then_noidle(self): self.sendRequest('idle player playlist') self.sendRequest('noidle') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('OK') def test_idle_player_playlist_then_player(self): self.sendRequest('idle player playlist') self.idleEvent('player') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('changed: player') self.assertNotInResponse('changed: playlist') self.assertOnceInResponse('OK') def test_idle_playlist_then_player(self): self.sendRequest('idle playlist') self.idleEvent('player') self.assertEqualEvents(['player']) self.assertEqualSubscriptions(['playlist']) self.assertNoResponse() def test_idle_playlist_then_player_then_playlist(self): self.sendRequest('idle playlist') self.idleEvent('player') self.idleEvent('playlist') self.assertNoEvents() self.assertNoSubscriptions() self.assertNotInResponse('changed: player') self.assertOnceInResponse('changed: playlist') self.assertOnceInResponse('OK') def test_player(self): self.idleEvent('player') self.assertEqualEvents(['player']) self.assertNoSubscriptions() self.assertNoResponse() def test_player_then_idle_player(self): self.idleEvent('player') self.sendRequest('idle player') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('changed: player') self.assertNotInResponse('changed: playlist') self.assertOnceInResponse('OK') def test_player_then_playlist(self): self.idleEvent('player') self.idleEvent('playlist') self.assertEqualEvents(['player', 'playlist']) self.assertNoSubscriptions() self.assertNoResponse() def test_player_then_idle(self): self.idleEvent('player') self.sendRequest('idle') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('changed: player') self.assertOnceInResponse('OK') def test_player_then_playlist_then_idle(self): self.idleEvent('player') self.idleEvent('playlist') self.sendRequest('idle') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('changed: player') self.assertOnceInResponse('changed: playlist') self.assertOnceInResponse('OK') def test_player_then_idle_playlist(self): self.idleEvent('player') self.sendRequest('idle playlist') self.assertEqualEvents(['player']) self.assertEqualSubscriptions(['playlist']) self.assertNoResponse() def test_player_then_idle_playlist_then_noidle(self): self.idleEvent('player') self.sendRequest('idle playlist') self.sendRequest('noidle') self.assertNoEvents() self.assertNoSubscriptions() self.assertOnceInResponse('OK') def test_player_then_playlist_then_idle_playlist(self): self.idleEvent('player') self.idleEvent('playlist') self.sendRequest('idle playlist') self.assertNoEvents() self.assertNoSubscriptions() self.assertNotInResponse('changed: player') self.assertOnceInResponse('changed: playlist') self.assertOnceInResponse('OK') mopidy-0.17.0/tests/frontends/mpd/protocol/music_db_test.py000066400000000000000000001053711224420023200240610ustar00rootroot00000000000000from __future__ import unicode_literals import unittest from mopidy.frontends.mpd.protocol import music_db from mopidy.models import Album, Artist, SearchResult, Track from tests.frontends.mpd import protocol class QueryFromMpdSearchFormatTest(unittest.TestCase): def test_dates_are_extracted(self): result = music_db._query_from_mpd_search_format( 'Date "1974-01-02" Date "1975"') self.assertEqual(result['date'][0], '1974-01-02') self.assertEqual(result['date'][1], '1975') # TODO Test more mappings class QueryFromMpdListFormatTest(unittest.TestCase): pass # TODO class MusicDatabaseHandlerTest(protocol.BaseTestCase): def test_count(self): self.sendRequest('count "artist" "needle"') self.assertInResponse('songs: 0') self.assertInResponse('playtime: 0') self.assertInResponse('OK') def test_count_without_quotes(self): self.sendRequest('count artist "needle"') self.assertInResponse('songs: 0') self.assertInResponse('playtime: 0') self.assertInResponse('OK') def test_count_with_multiple_pairs(self): self.sendRequest('count "artist" "foo" "album" "bar"') self.assertInResponse('songs: 0') self.assertInResponse('playtime: 0') self.assertInResponse('OK') def test_count_correct_length(self): # Count the lone track self.backend.library.dummy_find_exact_result = SearchResult( tracks=[ Track(uri='dummy:a', name="foo", date="2001", length=4000), ]) self.sendRequest('count "title" "foo"') self.assertInResponse('songs: 1') self.assertInResponse('playtime: 4') self.assertInResponse('OK') # Count multiple tracks self.backend.library.dummy_find_exact_result = SearchResult( tracks=[ Track(uri='dummy:b', date="2001", length=50000), Track(uri='dummy:c', date="2001", length=600000), ]) self.sendRequest('count "date" "2001"') self.assertInResponse('songs: 2') self.assertInResponse('playtime: 650') self.assertInResponse('OK') def test_findadd(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(uri='dummy:a', name='A')]) self.assertEqual(self.core.tracklist.length.get(), 0) self.sendRequest('findadd "title" "A"') self.assertEqual(self.core.tracklist.length.get(), 1) self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') self.assertInResponse('OK') def test_searchadd(self): self.backend.library.dummy_search_result = SearchResult( tracks=[Track(uri='dummy:a', name='A')]) self.assertEqual(self.core.tracklist.length.get(), 0) self.sendRequest('searchadd "title" "a"') self.assertEqual(self.core.tracklist.length.get(), 1) self.assertEqual(self.core.tracklist.tracks.get()[0].uri, 'dummy:a') self.assertInResponse('OK') def test_searchaddpl_appends_to_existing_playlist(self): playlist = self.core.playlists.create('my favs').get() playlist = playlist.copy(tracks=[ Track(uri='dummy:x', name='X'), Track(uri='dummy:y', name='y'), ]) self.core.playlists.save(playlist) self.backend.library.dummy_search_result = SearchResult( tracks=[Track(uri='dummy:a', name='A')]) playlists = self.core.playlists.filter(name='my favs').get() self.assertEqual(len(playlists), 1) self.assertEqual(len(playlists[0].tracks), 2) self.sendRequest('searchaddpl "my favs" "title" "a"') playlists = self.core.playlists.filter(name='my favs').get() self.assertEqual(len(playlists), 1) self.assertEqual(len(playlists[0].tracks), 3) self.assertEqual(playlists[0].tracks[0].uri, 'dummy:x') self.assertEqual(playlists[0].tracks[1].uri, 'dummy:y') self.assertEqual(playlists[0].tracks[2].uri, 'dummy:a') self.assertInResponse('OK') def test_searchaddpl_creates_missing_playlist(self): self.backend.library.dummy_search_result = SearchResult( tracks=[Track(uri='dummy:a', name='A')]) self.assertEqual( len(self.core.playlists.filter(name='my favs').get()), 0) self.sendRequest('searchaddpl "my favs" "title" "a"') playlists = self.core.playlists.filter(name='my favs').get() self.assertEqual(len(playlists), 1) self.assertEqual(playlists[0].tracks[0].uri, 'dummy:a') self.assertInResponse('OK') def test_listall_without_uri(self): self.sendRequest('listall') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_listall_with_uri(self): self.sendRequest('listall "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_listallinfo_without_uri(self): self.sendRequest('listallinfo') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_listallinfo_with_uri(self): self.sendRequest('listallinfo "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_lsinfo_without_path_returns_same_as_listplaylists(self): lsinfo_response = self.sendRequest('lsinfo') listplaylists_response = self.sendRequest('listplaylists') self.assertEqual(lsinfo_response, listplaylists_response) def test_lsinfo_with_empty_path_returns_same_as_listplaylists(self): lsinfo_response = self.sendRequest('lsinfo ""') listplaylists_response = self.sendRequest('listplaylists') self.assertEqual(lsinfo_response, listplaylists_response) def test_lsinfo_for_root_returns_same_as_listplaylists(self): lsinfo_response = self.sendRequest('lsinfo "/"') listplaylists_response = self.sendRequest('listplaylists') self.assertEqual(lsinfo_response, listplaylists_response) def test_update_without_uri(self): self.sendRequest('update') self.assertInResponse('updating_db: 0') self.assertInResponse('OK') def test_update_with_uri(self): self.sendRequest('update "file:///dev/urandom"') self.assertInResponse('updating_db: 0') self.assertInResponse('OK') def test_rescan_without_uri(self): self.sendRequest('rescan') self.assertInResponse('updating_db: 0') self.assertInResponse('OK') def test_rescan_with_uri(self): self.sendRequest('rescan "file:///dev/urandom"') self.assertInResponse('updating_db: 0') self.assertInResponse('OK') class MusicDatabaseFindTest(protocol.BaseTestCase): def test_find_includes_fake_artist_and_album_tracks(self): self.backend.library.dummy_find_exact_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A', date='2001')], artists=[Artist(uri='dummy:artist:b', name='B')], tracks=[Track(uri='dummy:track:c', name='C')]) self.sendRequest('find "any" "foo"') self.assertInResponse('file: dummy:artist:b') self.assertInResponse('Title: Artist: B') self.assertInResponse('file: dummy:album:a') self.assertInResponse('Title: Album: A') self.assertInResponse('Date: 2001') self.assertInResponse('file: dummy:track:c') self.assertInResponse('Title: C') self.assertInResponse('OK') def test_find_artist_does_not_include_fake_artist_tracks(self): self.backend.library.dummy_find_exact_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A', date='2001')], artists=[Artist(uri='dummy:artist:b', name='B')], tracks=[Track(uri='dummy:track:c', name='C')]) self.sendRequest('find "artist" "foo"') self.assertNotInResponse('file: dummy:artist:b') self.assertNotInResponse('Title: Artist: B') self.assertInResponse('file: dummy:album:a') self.assertInResponse('Title: Album: A') self.assertInResponse('Date: 2001') self.assertInResponse('file: dummy:track:c') self.assertInResponse('Title: C') self.assertInResponse('OK') def test_find_albumartist_does_not_include_fake_artist_tracks(self): self.backend.library.dummy_find_exact_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A', date='2001')], artists=[Artist(uri='dummy:artist:b', name='B')], tracks=[Track(uri='dummy:track:c', name='C')]) self.sendRequest('find "albumartist" "foo"') self.assertNotInResponse('file: dummy:artist:b') self.assertNotInResponse('Title: Artist: B') self.assertInResponse('file: dummy:album:a') self.assertInResponse('Title: Album: A') self.assertInResponse('Date: 2001') self.assertInResponse('file: dummy:track:c') self.assertInResponse('Title: C') self.assertInResponse('OK') def test_find_artist_and_album_does_not_include_fake_tracks(self): self.backend.library.dummy_find_exact_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A', date='2001')], artists=[Artist(uri='dummy:artist:b', name='B')], tracks=[Track(uri='dummy:track:c', name='C')]) self.sendRequest('find "artist" "foo" "album" "bar"') self.assertNotInResponse('file: dummy:artist:b') self.assertNotInResponse('Title: Artist: B') self.assertNotInResponse('file: dummy:album:a') self.assertNotInResponse('Title: Album: A') self.assertNotInResponse('Date: 2001') self.assertInResponse('file: dummy:track:c') self.assertInResponse('Title: C') self.assertInResponse('OK') def test_find_album(self): self.sendRequest('find "album" "what"') self.assertInResponse('OK') def test_find_album_without_quotes(self): self.sendRequest('find album "what"') self.assertInResponse('OK') def test_find_artist(self): self.sendRequest('find "artist" "what"') self.assertInResponse('OK') def test_find_artist_without_quotes(self): self.sendRequest('find artist "what"') self.assertInResponse('OK') def test_find_albumartist(self): self.sendRequest('find "albumartist" "what"') self.assertInResponse('OK') def test_find_albumartist_without_quotes(self): self.sendRequest('find albumartist "what"') self.assertInResponse('OK') def test_find_composer(self): self.sendRequest('find "composer" "what"') self.assertInResponse('OK') def test_find_composer_without_quotes(self): self.sendRequest('find composer "what"') self.assertInResponse('OK') def test_find_performer(self): self.sendRequest('find "performer" "what"') self.assertInResponse('OK') def test_find_performer_without_quotes(self): self.sendRequest('find performer "what"') self.assertInResponse('OK') def test_find_filename(self): self.sendRequest('find "filename" "afilename"') self.assertInResponse('OK') def test_find_filename_without_quotes(self): self.sendRequest('find filename "afilename"') self.assertInResponse('OK') def test_find_file(self): self.sendRequest('find "file" "afilename"') self.assertInResponse('OK') def test_find_file_without_quotes(self): self.sendRequest('find file "afilename"') self.assertInResponse('OK') def test_find_title(self): self.sendRequest('find "title" "what"') self.assertInResponse('OK') def test_find_title_without_quotes(self): self.sendRequest('find title "what"') self.assertInResponse('OK') def test_find_track_no(self): self.sendRequest('find "track" "10"') self.assertInResponse('OK') def test_find_track_no_without_quotes(self): self.sendRequest('find track "10"') self.assertInResponse('OK') def test_find_track_no_without_filter_value(self): self.sendRequest('find "track" ""') self.assertInResponse('OK') def test_find_genre(self): self.sendRequest('find "genre" "what"') self.assertInResponse('OK') def test_find_genre_without_quotes(self): self.sendRequest('find genre "what"') self.assertInResponse('OK') def test_find_date(self): self.sendRequest('find "date" "2002-01-01"') self.assertInResponse('OK') def test_find_date_without_quotes(self): self.sendRequest('find date "2002-01-01"') self.assertInResponse('OK') def test_find_date_with_capital_d_and_incomplete_date(self): self.sendRequest('find Date "2005"') self.assertInResponse('OK') def test_find_else_should_fail(self): self.sendRequest('find "somethingelse" "what"') self.assertEqualResponse('ACK [2@0] {find} incorrect arguments') def test_find_album_and_artist(self): self.sendRequest('find album "album_what" artist "artist_what"') self.assertInResponse('OK') def test_find_without_filter_value(self): self.sendRequest('find "album" ""') self.assertInResponse('OK') class MusicDatabaseListTest(protocol.BaseTestCase): def test_list(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[ Track(uri='dummy:a', name='A', artists=[ Artist(name='A Artist')])]) self.sendRequest('list "artist" "artist" "foo"') self.assertInResponse('Artist: A Artist') self.assertInResponse('OK') def test_list_foo_returns_ack(self): self.sendRequest('list "foo"') self.assertEqualResponse('ACK [2@0] {list} incorrect arguments') ### Artist def test_list_artist_with_quotes(self): self.sendRequest('list "artist"') self.assertInResponse('OK') def test_list_artist_without_quotes(self): self.sendRequest('list artist') self.assertInResponse('OK') def test_list_artist_without_quotes_and_capitalized(self): self.sendRequest('list Artist') self.assertInResponse('OK') def test_list_artist_with_query_of_one_token(self): self.sendRequest('list "artist" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_artist_with_unknown_field_in_query_returns_ack(self): self.sendRequest('list "artist" "foo" "bar"') self.assertEqualResponse('ACK [2@0] {list} not able to parse args') def test_list_artist_by_artist(self): self.sendRequest('list "artist" "artist" "anartist"') self.assertInResponse('OK') def test_list_artist_by_album(self): self.sendRequest('list "artist" "album" "analbum"') self.assertInResponse('OK') def test_list_artist_by_full_date(self): self.sendRequest('list "artist" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_artist_by_year(self): self.sendRequest('list "artist" "date" "2001"') self.assertInResponse('OK') def test_list_artist_by_genre(self): self.sendRequest('list "artist" "genre" "agenre"') self.assertInResponse('OK') def test_list_artist_by_artist_and_album(self): self.sendRequest( 'list "artist" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_artist_without_filter_value(self): self.sendRequest('list "artist" "artist" ""') self.assertInResponse('OK') def test_list_artist_should_not_return_artists_without_names(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(artists=[Artist(name='')])]) self.sendRequest('list "artist"') self.assertNotInResponse('Artist: ') self.assertInResponse('OK') ### Albumartist def test_list_albumartist_with_quotes(self): self.sendRequest('list "albumartist"') self.assertInResponse('OK') def test_list_albumartist_without_quotes(self): self.sendRequest('list albumartist') self.assertInResponse('OK') def test_list_albumartist_without_quotes_and_capitalized(self): self.sendRequest('list Albumartist') self.assertInResponse('OK') def test_list_albumartist_with_query_of_one_token(self): self.sendRequest('list "albumartist" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_albumartist_with_unknown_field_in_query_returns_ack(self): self.sendRequest('list "albumartist" "foo" "bar"') self.assertEqualResponse('ACK [2@0] {list} not able to parse args') def test_list_albumartist_by_artist(self): self.sendRequest('list "albumartist" "artist" "anartist"') self.assertInResponse('OK') def test_list_albumartist_by_album(self): self.sendRequest('list "albumartist" "album" "analbum"') self.assertInResponse('OK') def test_list_albumartist_by_full_date(self): self.sendRequest('list "albumartist" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_albumartist_by_year(self): self.sendRequest('list "albumartist" "date" "2001"') self.assertInResponse('OK') def test_list_albumartist_by_genre(self): self.sendRequest('list "albumartist" "genre" "agenre"') self.assertInResponse('OK') def test_list_albumartist_by_artist_and_album(self): self.sendRequest( 'list "albumartist" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_albumartist_without_filter_value(self): self.sendRequest('list "albumartist" "artist" ""') self.assertInResponse('OK') def test_list_albumartist_should_not_return_artists_without_names(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(album=Album(artists=[Artist(name='')]))]) self.sendRequest('list "albumartist"') self.assertNotInResponse('Artist: ') self.assertNotInResponse('Albumartist: ') self.assertNotInResponse('Composer: ') self.assertNotInResponse('Performer: ') self.assertInResponse('OK') ### Composer def test_list_composer_with_quotes(self): self.sendRequest('list "composer"') self.assertInResponse('OK') def test_list_composer_without_quotes(self): self.sendRequest('list composer') self.assertInResponse('OK') def test_list_composer_without_quotes_and_capitalized(self): self.sendRequest('list Composer') self.assertInResponse('OK') def test_list_composer_with_query_of_one_token(self): self.sendRequest('list "composer" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_composer_with_unknown_field_in_query_returns_ack(self): self.sendRequest('list "composer" "foo" "bar"') self.assertEqualResponse('ACK [2@0] {list} not able to parse args') def test_list_composer_by_artist(self): self.sendRequest('list "composer" "artist" "anartist"') self.assertInResponse('OK') def test_list_composer_by_album(self): self.sendRequest('list "composer" "album" "analbum"') self.assertInResponse('OK') def test_list_composer_by_full_date(self): self.sendRequest('list "composer" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_composer_by_year(self): self.sendRequest('list "composer" "date" "2001"') self.assertInResponse('OK') def test_list_composer_by_genre(self): self.sendRequest('list "composer" "genre" "agenre"') self.assertInResponse('OK') def test_list_composer_by_artist_and_album(self): self.sendRequest( 'list "composer" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_composer_without_filter_value(self): self.sendRequest('list "composer" "artist" ""') self.assertInResponse('OK') def test_list_composer_should_not_return_artists_without_names(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(composers=[Artist(name='')])]) self.sendRequest('list "composer"') self.assertNotInResponse('Artist: ') self.assertNotInResponse('Albumartist: ') self.assertNotInResponse('Composer: ') self.assertNotInResponse('Performer: ') self.assertInResponse('OK') ### Performer def test_list_performer_with_quotes(self): self.sendRequest('list "performer"') self.assertInResponse('OK') def test_list_performer_without_quotes(self): self.sendRequest('list performer') self.assertInResponse('OK') def test_list_performer_without_quotes_and_capitalized(self): self.sendRequest('list Albumartist') self.assertInResponse('OK') def test_list_performer_with_query_of_one_token(self): self.sendRequest('list "performer" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_performer_with_unknown_field_in_query_returns_ack(self): self.sendRequest('list "performer" "foo" "bar"') self.assertEqualResponse('ACK [2@0] {list} not able to parse args') def test_list_performer_by_artist(self): self.sendRequest('list "performer" "artist" "anartist"') self.assertInResponse('OK') def test_list_performer_by_album(self): self.sendRequest('list "performer" "album" "analbum"') self.assertInResponse('OK') def test_list_performer_by_full_date(self): self.sendRequest('list "performer" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_performer_by_year(self): self.sendRequest('list "performer" "date" "2001"') self.assertInResponse('OK') def test_list_performer_by_genre(self): self.sendRequest('list "performer" "genre" "agenre"') self.assertInResponse('OK') def test_list_performer_by_artist_and_album(self): self.sendRequest( 'list "performer" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_performer_without_filter_value(self): self.sendRequest('list "performer" "artist" ""') self.assertInResponse('OK') def test_list_performer_should_not_return_artists_without_names(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(performers=[Artist(name='')])]) self.sendRequest('list "performer"') self.assertNotInResponse('Artist: ') self.assertNotInResponse('Albumartist: ') self.assertNotInResponse('Composer: ') self.assertNotInResponse('Performer: ') self.assertInResponse('OK') ### Album def test_list_album_with_quotes(self): self.sendRequest('list "album"') self.assertInResponse('OK') def test_list_album_without_quotes(self): self.sendRequest('list album') self.assertInResponse('OK') def test_list_album_without_quotes_and_capitalized(self): self.sendRequest('list Album') self.assertInResponse('OK') def test_list_album_with_artist_name(self): self.sendRequest('list "album" "anartist"') self.assertInResponse('OK') def test_list_album_with_artist_name_without_filter_value(self): self.sendRequest('list "album" ""') self.assertInResponse('OK') def test_list_album_by_artist(self): self.sendRequest('list "album" "artist" "anartist"') self.assertInResponse('OK') def test_list_album_by_album(self): self.sendRequest('list "album" "album" "analbum"') self.assertInResponse('OK') def test_list_album_by_albumartist(self): self.sendRequest('list "album" "albumartist" "anartist"') self.assertInResponse('OK') def test_list_album_by_composer(self): self.sendRequest('list "album" "composer" "anartist"') self.assertInResponse('OK') def test_list_album_by_performer(self): self.sendRequest('list "album" "performer" "anartist"') self.assertInResponse('OK') def test_list_album_by_full_date(self): self.sendRequest('list "album" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_album_by_year(self): self.sendRequest('list "album" "date" "2001"') self.assertInResponse('OK') def test_list_album_by_genre(self): self.sendRequest('list "album" "genre" "agenre"') self.assertInResponse('OK') def test_list_album_by_artist_and_album(self): self.sendRequest( 'list "album" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_album_without_filter_value(self): self.sendRequest('list "album" "artist" ""') self.assertInResponse('OK') def test_list_album_should_not_return_albums_without_names(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(album=Album(name=''))]) self.sendRequest('list "album"') self.assertNotInResponse('Album: ') self.assertInResponse('OK') ### Date def test_list_date_with_quotes(self): self.sendRequest('list "date"') self.assertInResponse('OK') def test_list_date_without_quotes(self): self.sendRequest('list date') self.assertInResponse('OK') def test_list_date_without_quotes_and_capitalized(self): self.sendRequest('list Date') self.assertInResponse('OK') def test_list_date_with_query_of_one_token(self): self.sendRequest('list "date" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_date_by_artist(self): self.sendRequest('list "date" "artist" "anartist"') self.assertInResponse('OK') def test_list_date_by_album(self): self.sendRequest('list "date" "album" "analbum"') self.assertInResponse('OK') def test_list_date_by_full_date(self): self.sendRequest('list "date" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_date_by_year(self): self.sendRequest('list "date" "date" "2001"') self.assertInResponse('OK') def test_list_date_by_genre(self): self.sendRequest('list "date" "genre" "agenre"') self.assertInResponse('OK') def test_list_date_by_artist_and_album(self): self.sendRequest('list "date" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_date_without_filter_value(self): self.sendRequest('list "date" "artist" ""') self.assertInResponse('OK') def test_list_date_should_not_return_blank_dates(self): self.backend.library.dummy_find_exact_result = SearchResult( tracks=[Track(date='')]) self.sendRequest('list "date"') self.assertNotInResponse('Date: ') self.assertInResponse('OK') ### Genre def test_list_genre_with_quotes(self): self.sendRequest('list "genre"') self.assertInResponse('OK') def test_list_genre_without_quotes(self): self.sendRequest('list genre') self.assertInResponse('OK') def test_list_genre_without_quotes_and_capitalized(self): self.sendRequest('list Genre') self.assertInResponse('OK') def test_list_genre_with_query_of_one_token(self): self.sendRequest('list "genre" "anartist"') self.assertEqualResponse( 'ACK [2@0] {list} should be "Album" for 3 arguments') def test_list_genre_by_artist(self): self.sendRequest('list "genre" "artist" "anartist"') self.assertInResponse('OK') def test_list_genre_by_album(self): self.sendRequest('list "genre" "album" "analbum"') self.assertInResponse('OK') def test_list_genre_by_full_date(self): self.sendRequest('list "genre" "date" "2001-01-01"') self.assertInResponse('OK') def test_list_genre_by_year(self): self.sendRequest('list "genre" "date" "2001"') self.assertInResponse('OK') def test_list_genre_by_genre(self): self.sendRequest('list "genre" "genre" "agenre"') self.assertInResponse('OK') def test_list_genre_by_artist_and_album(self): self.sendRequest( 'list "genre" "artist" "anartist" "album" "analbum"') self.assertInResponse('OK') def test_list_genre_without_filter_value(self): self.sendRequest('list "genre" "artist" ""') self.assertInResponse('OK') class MusicDatabaseSearchTest(protocol.BaseTestCase): def test_search(self): self.backend.library.dummy_search_result = SearchResult( albums=[Album(uri='dummy:album:a', name='A')], artists=[Artist(uri='dummy:artist:b', name='B')], tracks=[Track(uri='dummy:track:c', name='C')]) self.sendRequest('search "any" "foo"') self.assertInResponse('file: dummy:album:a') self.assertInResponse('Title: Album: A') self.assertInResponse('file: dummy:artist:b') self.assertInResponse('Title: Artist: B') self.assertInResponse('file: dummy:track:c') self.assertInResponse('Title: C') self.assertInResponse('OK') def test_search_album(self): self.sendRequest('search "album" "analbum"') self.assertInResponse('OK') def test_search_album_without_quotes(self): self.sendRequest('search album "analbum"') self.assertInResponse('OK') def test_search_album_without_filter_value(self): self.sendRequest('search "album" ""') self.assertInResponse('OK') def test_search_artist(self): self.sendRequest('search "artist" "anartist"') self.assertInResponse('OK') def test_search_artist_without_quotes(self): self.sendRequest('search artist "anartist"') self.assertInResponse('OK') def test_search_artist_without_filter_value(self): self.sendRequest('search "artist" ""') self.assertInResponse('OK') def test_search_albumartist(self): self.sendRequest('search "albumartist" "analbumartist"') self.assertInResponse('OK') def test_search_albumartist_without_quotes(self): self.sendRequest('search albumartist "analbumartist"') self.assertInResponse('OK') def test_search_albumartist_without_filter_value(self): self.sendRequest('search "albumartist" ""') self.assertInResponse('OK') def test_search_composer(self): self.sendRequest('search "composer" "acomposer"') self.assertInResponse('OK') def test_search_composer_without_quotes(self): self.sendRequest('search composer "acomposer"') self.assertInResponse('OK') def test_search_composer_without_filter_value(self): self.sendRequest('search "composer" ""') self.assertInResponse('OK') def test_search_performer(self): self.sendRequest('search "performer" "aperformer"') self.assertInResponse('OK') def test_search_performer_without_quotes(self): self.sendRequest('search performer "aperformer"') self.assertInResponse('OK') def test_search_performer_without_filter_value(self): self.sendRequest('search "performer" ""') self.assertInResponse('OK') def test_search_filename(self): self.sendRequest('search "filename" "afilename"') self.assertInResponse('OK') def test_search_filename_without_quotes(self): self.sendRequest('search filename "afilename"') self.assertInResponse('OK') def test_search_filename_without_filter_value(self): self.sendRequest('search "filename" ""') self.assertInResponse('OK') def test_search_file(self): self.sendRequest('search "file" "afilename"') self.assertInResponse('OK') def test_search_file_without_quotes(self): self.sendRequest('search file "afilename"') self.assertInResponse('OK') def test_search_file_without_filter_value(self): self.sendRequest('search "file" ""') self.assertInResponse('OK') def test_search_title(self): self.sendRequest('search "title" "atitle"') self.assertInResponse('OK') def test_search_title_without_quotes(self): self.sendRequest('search title "atitle"') self.assertInResponse('OK') def test_search_title_without_filter_value(self): self.sendRequest('search "title" ""') self.assertInResponse('OK') def test_search_any(self): self.sendRequest('search "any" "anything"') self.assertInResponse('OK') def test_search_any_without_quotes(self): self.sendRequest('search any "anything"') self.assertInResponse('OK') def test_search_any_without_filter_value(self): self.sendRequest('search "any" ""') self.assertInResponse('OK') def test_search_track_no(self): self.sendRequest('search "track" "10"') self.assertInResponse('OK') def test_search_track_no_without_quotes(self): self.sendRequest('search track "10"') self.assertInResponse('OK') def test_search_track_no_without_filter_value(self): self.sendRequest('search "track" ""') self.assertInResponse('OK') def test_search_genre(self): self.sendRequest('search "genre" "agenre"') self.assertInResponse('OK') def test_search_genre_without_quotes(self): self.sendRequest('search genre "agenre"') self.assertInResponse('OK') def test_search_genre_without_filter_value(self): self.sendRequest('search "genre" ""') self.assertInResponse('OK') def test_search_date(self): self.sendRequest('search "date" "2002-01-01"') self.assertInResponse('OK') def test_search_date_without_quotes(self): self.sendRequest('search date "2002-01-01"') self.assertInResponse('OK') def test_search_date_with_capital_d_and_incomplete_date(self): self.sendRequest('search Date "2005"') self.assertInResponse('OK') def test_search_date_without_filter_value(self): self.sendRequest('search "date" ""') self.assertInResponse('OK') def test_search_comment(self): self.sendRequest('search "comment" "acomment"') self.assertInResponse('OK') def test_search_comment_without_quotes(self): self.sendRequest('search comment "acomment"') self.assertInResponse('OK') def test_search_comment_without_filter_value(self): self.sendRequest('search "comment" ""') self.assertInResponse('OK') def test_search_else_should_fail(self): self.sendRequest('search "sometype" "something"') self.assertEqualResponse('ACK [2@0] {search} incorrect arguments') mopidy-0.17.0/tests/frontends/mpd/protocol/playback_test.py000066400000000000000000000411011224420023200240500ustar00rootroot00000000000000from __future__ import unicode_literals import unittest from mopidy.core import PlaybackState from mopidy.models import Track from tests.frontends.mpd import protocol PAUSED = PlaybackState.PAUSED PLAYING = PlaybackState.PLAYING STOPPED = PlaybackState.STOPPED class PlaybackOptionsHandlerTest(protocol.BaseTestCase): def test_consume_off(self): self.sendRequest('consume "0"') self.assertFalse(self.core.tracklist.consume.get()) self.assertInResponse('OK') def test_consume_off_without_quotes(self): self.sendRequest('consume 0') self.assertFalse(self.core.tracklist.consume.get()) self.assertInResponse('OK') def test_consume_on(self): self.sendRequest('consume "1"') self.assertTrue(self.core.tracklist.consume.get()) self.assertInResponse('OK') def test_consume_on_without_quotes(self): self.sendRequest('consume 1') self.assertTrue(self.core.tracklist.consume.get()) self.assertInResponse('OK') def test_crossfade(self): self.sendRequest('crossfade "10"') self.assertInResponse('ACK [0@0] {} Not implemented') def test_random_off(self): self.sendRequest('random "0"') self.assertFalse(self.core.tracklist.random.get()) self.assertInResponse('OK') def test_random_off_without_quotes(self): self.sendRequest('random 0') self.assertFalse(self.core.tracklist.random.get()) self.assertInResponse('OK') def test_random_on(self): self.sendRequest('random "1"') self.assertTrue(self.core.tracklist.random.get()) self.assertInResponse('OK') def test_random_on_without_quotes(self): self.sendRequest('random 1') self.assertTrue(self.core.tracklist.random.get()) self.assertInResponse('OK') def test_repeat_off(self): self.sendRequest('repeat "0"') self.assertFalse(self.core.tracklist.repeat.get()) self.assertInResponse('OK') def test_repeat_off_without_quotes(self): self.sendRequest('repeat 0') self.assertFalse(self.core.tracklist.repeat.get()) self.assertInResponse('OK') def test_repeat_on(self): self.sendRequest('repeat "1"') self.assertTrue(self.core.tracklist.repeat.get()) self.assertInResponse('OK') def test_repeat_on_without_quotes(self): self.sendRequest('repeat 1') self.assertTrue(self.core.tracklist.repeat.get()) self.assertInResponse('OK') def test_setvol_below_min(self): self.sendRequest('setvol "-10"') self.assertEqual(0, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_min(self): self.sendRequest('setvol "0"') self.assertEqual(0, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_middle(self): self.sendRequest('setvol "50"') self.assertEqual(50, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_max(self): self.sendRequest('setvol "100"') self.assertEqual(100, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_above_max(self): self.sendRequest('setvol "110"') self.assertEqual(100, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_plus_is_ignored(self): self.sendRequest('setvol "+10"') self.assertEqual(10, self.core.playback.volume.get()) self.assertInResponse('OK') def test_setvol_without_quotes(self): self.sendRequest('setvol 50') self.assertEqual(50, self.core.playback.volume.get()) self.assertInResponse('OK') def test_single_off(self): self.sendRequest('single "0"') self.assertFalse(self.core.tracklist.single.get()) self.assertInResponse('OK') def test_single_off_without_quotes(self): self.sendRequest('single 0') self.assertFalse(self.core.tracklist.single.get()) self.assertInResponse('OK') def test_single_on(self): self.sendRequest('single "1"') self.assertTrue(self.core.tracklist.single.get()) self.assertInResponse('OK') def test_single_on_without_quotes(self): self.sendRequest('single 1') self.assertTrue(self.core.tracklist.single.get()) self.assertInResponse('OK') def test_replay_gain_mode_off(self): self.sendRequest('replay_gain_mode "off"') self.assertInResponse('ACK [0@0] {} Not implemented') def test_replay_gain_mode_track(self): self.sendRequest('replay_gain_mode "track"') self.assertInResponse('ACK [0@0] {} Not implemented') def test_replay_gain_mode_album(self): self.sendRequest('replay_gain_mode "album"') self.assertInResponse('ACK [0@0] {} Not implemented') def test_replay_gain_status_default(self): self.sendRequest('replay_gain_status') self.assertInResponse('OK') self.assertInResponse('off') @unittest.SkipTest def test_replay_gain_status_off(self): pass @unittest.SkipTest def test_replay_gain_status_track(self): pass @unittest.SkipTest def test_replay_gain_status_album(self): pass class PlaybackControlHandlerTest(protocol.BaseTestCase): def test_next(self): self.sendRequest('next') self.assertInResponse('OK') def test_pause_off(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.sendRequest('pause "1"') self.sendRequest('pause "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_pause_on(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.sendRequest('pause "1"') self.assertEqual(PAUSED, self.core.playback.state.get()) self.assertInResponse('OK') def test_pause_toggle(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') self.sendRequest('pause') self.assertEqual(PAUSED, self.core.playback.state.get()) self.assertInResponse('OK') self.sendRequest('pause') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_without_pos(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos_without_quotes(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('play 0') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_play_with_pos_out_of_bounds(self): self.core.tracklist.add([]) self.sendRequest('play "0"') self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertInResponse('ACK [2@0] {play} Bad song index') def test_play_minus_one_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.core.playback.current_track.get(), None) self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.sendRequest('play "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertEqual( 'dummy:a', self.core.playback.current_track.get().uri) self.assertInResponse('OK') def test_play_minus_one_plays_current_track_if_current_track_is_set(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.current_track.get(), None) self.core.playback.play() self.core.playback.next() self.core.playback.stop() self.assertNotEqual(self.core.playback.current_track.get(), None) self.sendRequest('play "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertEqual( 'dummy:b', self.core.playback.current_track.get().uri) self.assertInResponse('OK') def test_play_minus_one_on_empty_playlist_does_not_ack(self): self.core.tracklist.clear() self.sendRequest('play "-1"') self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertEqual(None, self.core.playback.current_track.get()) self.assertInResponse('OK') def test_play_minus_is_ignored_if_playing(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertEquals(PLAYING, self.core.playback.state.get()) self.sendRequest('play "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_play_minus_one_resumes_if_paused(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertEquals(PLAYING, self.core.playback.state.get()) self.core.playback.pause() self.assertEquals(PAUSED, self.core.playback.state.get()) self.sendRequest('play "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_playid(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid "0"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_playid_without_quotes(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid 0') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertInResponse('OK') def test_playid_minus_1_plays_first_in_playlist_if_no_current_track(self): self.assertEqual(self.core.playback.current_track.get(), None) self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.sendRequest('playid "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertEqual( 'dummy:a', self.core.playback.current_track.get().uri) self.assertInResponse('OK') def test_playid_minus_1_plays_current_track_if_current_track_is_set(self): self.core.tracklist.add([Track(uri='dummy:a'), Track(uri='dummy:b')]) self.assertEqual(self.core.playback.current_track.get(), None) self.core.playback.play() self.core.playback.next() self.core.playback.stop() self.assertNotEqual(None, self.core.playback.current_track.get()) self.sendRequest('playid "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertEqual( 'dummy:b', self.core.playback.current_track.get().uri) self.assertInResponse('OK') def test_playid_minus_one_on_empty_playlist_does_not_ack(self): self.core.tracklist.clear() self.sendRequest('playid "-1"') self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertEqual(None, self.core.playback.current_track.get()) self.assertInResponse('OK') def test_playid_minus_is_ignored_if_playing(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertEquals(PLAYING, self.core.playback.state.get()) self.sendRequest('playid "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_playid_minus_one_resumes_if_paused(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.seek(30000) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertEquals(PLAYING, self.core.playback.state.get()) self.core.playback.pause() self.assertEquals(PAUSED, self.core.playback.state.get()) self.sendRequest('playid "-1"') self.assertEqual(PLAYING, self.core.playback.state.get()) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_playid_which_does_not_exist(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.sendRequest('playid "12345"') self.assertInResponse('ACK [50@0] {playid} No such song') def test_previous(self): self.sendRequest('previous') self.assertInResponse('OK') def test_seek_in_current_track(self): seek_track = Track(uri='dummy:a', length=40000) self.core.tracklist.add([seek_track]) self.core.playback.play() self.sendRequest('seek "0" "30"') self.assertEqual(self.core.playback.current_track.get(), seek_track) self.assertGreaterEqual(self.core.playback.time_position, 30000) self.assertInResponse('OK') def test_seek_in_another_track(self): seek_track = Track(uri='dummy:b', length=40000) self.core.tracklist.add( [Track(uri='dummy:a', length=40000), seek_track]) self.core.playback.play() self.assertNotEqual(self.core.playback.current_track.get(), seek_track) self.sendRequest('seek "1" "30"') self.assertEqual(self.core.playback.current_track.get(), seek_track) self.assertInResponse('OK') def test_seek_without_quotes(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.sendRequest('seek 0 30') self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_seekid_in_current_track(self): seek_track = Track(uri='dummy:a', length=40000) self.core.tracklist.add([seek_track]) self.core.playback.play() self.sendRequest('seekid "0" "30"') self.assertEqual(self.core.playback.current_track.get(), seek_track) self.assertGreaterEqual( self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_seekid_in_another_track(self): seek_track = Track(uri='dummy:b', length=40000) self.core.tracklist.add( [Track(uri='dummy:a', length=40000), seek_track]) self.core.playback.play() self.sendRequest('seekid "1" "30"') self.assertEqual(1, self.core.playback.current_tl_track.get().tlid) self.assertEqual(seek_track, self.core.playback.current_track.get()) self.assertInResponse('OK') def test_seekcur_absolute_value(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.sendRequest('seekcur "30"') self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_seekcur_positive_diff(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(10000) self.assertGreaterEqual(self.core.playback.time_position.get(), 10000) self.sendRequest('seekcur "+20"') self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) self.assertInResponse('OK') def test_seekcur_negative_diff(self): self.core.tracklist.add([Track(uri='dummy:a', length=40000)]) self.core.playback.play() self.core.playback.seek(30000) self.assertGreaterEqual(self.core.playback.time_position.get(), 30000) self.sendRequest('seekcur "-20"') self.assertLessEqual(self.core.playback.time_position.get(), 15000) self.assertInResponse('OK') def test_stop(self): self.sendRequest('stop') self.assertEqual(STOPPED, self.core.playback.state.get()) self.assertInResponse('OK') mopidy-0.17.0/tests/frontends/mpd/protocol/reflection_test.py000066400000000000000000000061631224420023200244250ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class ReflectionHandlerTest(protocol.BaseTestCase): def test_config_is_not_allowed_across_the_network(self): self.sendRequest('config') self.assertEqualResponse( 'ACK [4@0] {config} you don\'t have permission for "config"') def test_commands_returns_list_of_all_commands(self): self.sendRequest('commands') # Check if some random commands are included self.assertInResponse('command: commands') self.assertInResponse('command: play') self.assertInResponse('command: status') # Check if commands you do not have access to are not present self.assertNotInResponse('command: config') self.assertNotInResponse('command: kill') # Check if the blacklisted commands are not present self.assertNotInResponse('command: command_list_begin') self.assertNotInResponse('command: command_list_ok_begin') self.assertNotInResponse('command: command_list_end') self.assertNotInResponse('command: idle') self.assertNotInResponse('command: noidle') self.assertNotInResponse('command: sticker') self.assertInResponse('OK') def test_decoders(self): self.sendRequest('decoders') self.assertInResponse('OK') def test_notcommands_returns_only_config_and_kill_and_ok(self): response = self.sendRequest('notcommands') self.assertEqual(3, len(response)) self.assertInResponse('command: config') self.assertInResponse('command: kill') self.assertInResponse('OK') def test_tagtypes(self): self.sendRequest('tagtypes') self.assertInResponse('OK') def test_urlhandlers(self): self.sendRequest('urlhandlers') self.assertInResponse('OK') self.assertInResponse('handler: dummy') class ReflectionWhenNotAuthedTest(protocol.BaseTestCase): def get_config(self): config = super(ReflectionWhenNotAuthedTest, self).get_config() config['mpd']['password'] = 'topsecret' return config def test_commands_show_less_if_auth_required_and_not_authed(self): self.sendRequest('commands') # Not requiring auth self.assertInResponse('command: close') self.assertInResponse('command: commands') self.assertInResponse('command: notcommands') self.assertInResponse('command: password') self.assertInResponse('command: ping') # Requiring auth self.assertNotInResponse('command: play') self.assertNotInResponse('command: status') def test_notcommands_returns_more_if_auth_required_and_not_authed(self): self.sendRequest('notcommands') # Not requiring auth self.assertNotInResponse('command: close') self.assertNotInResponse('command: commands') self.assertNotInResponse('command: notcommands') self.assertNotInResponse('command: password') self.assertNotInResponse('command: ping') # Requiring auth self.assertInResponse('command: play') self.assertInResponse('command: status') mopidy-0.17.0/tests/frontends/mpd/protocol/regression_test.py000066400000000000000000000125411224420023200244500ustar00rootroot00000000000000from __future__ import unicode_literals import random from mopidy.models import Track from tests.frontends.mpd import protocol class IssueGH17RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues/17 How to reproduce: - Play a playlist where one track cannot be played - Turn on random mode - Press next until you get to the unplayable track """ def test(self): self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:error'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f'), ]) random.seed(1) # Playlist order: abcfde self.sendRequest('play') self.assertEquals( 'dummy:a', self.core.playback.current_track.get().uri) self.sendRequest('random "1"') self.sendRequest('next') self.assertEquals( 'dummy:b', self.core.playback.current_track.get().uri) self.sendRequest('next') # Should now be at track 'c', but playback fails and it skips ahead self.assertEquals( 'dummy:f', self.core.playback.current_track.get().uri) self.sendRequest('next') self.assertEquals( 'dummy:d', self.core.playback.current_track.get().uri) self.sendRequest('next') self.assertEquals( 'dummy:e', self.core.playback.current_track.get().uri) class IssueGH18RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues/18 How to reproduce: Play, random on, next, random off, next, next. At this point it gives the same song over and over. """ def test(self): self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) random.seed(1) self.sendRequest('play') self.sendRequest('random "1"') self.sendRequest('next') self.sendRequest('random "0"') self.sendRequest('next') self.sendRequest('next') tl_track_1 = self.core.playback.current_tl_track.get() self.sendRequest('next') tl_track_2 = self.core.playback.current_tl_track.get() self.sendRequest('next') tl_track_3 = self.core.playback.current_tl_track.get() self.assertNotEqual(tl_track_1, tl_track_2) self.assertNotEqual(tl_track_2, tl_track_3) class IssueGH22RegressionTest(protocol.BaseTestCase): """ The issue: http://github.com/mopidy/mopidy/issues/22 How to reproduce: Play, random on, remove all tracks from the current playlist (as in "delete" each one, not "clear"). Alternatively: Play, random on, remove a random track from the current playlist, press next until it crashes. """ def test(self): self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) random.seed(1) self.sendRequest('play') self.sendRequest('random "1"') self.sendRequest('deleteid "1"') self.sendRequest('deleteid "2"') self.sendRequest('deleteid "3"') self.sendRequest('deleteid "4"') self.sendRequest('deleteid "5"') self.sendRequest('deleteid "6"') self.sendRequest('status') class IssueGH69RegressionTest(protocol.BaseTestCase): """ The issue: https://github.com/mopidy/mopidy/issues/69 How to reproduce: Play track, stop, clear current playlist, load a new playlist, status. The status response now contains "song: None". """ def test(self): self.core.playlists.create('foo') self.core.tracklist.add([ Track(uri='dummy:a'), Track(uri='dummy:b'), Track(uri='dummy:c'), Track(uri='dummy:d'), Track(uri='dummy:e'), Track(uri='dummy:f')]) self.sendRequest('play') self.sendRequest('stop') self.sendRequest('clear') self.sendRequest('load "foo"') self.assertNotInResponse('song: None') class IssueGH113RegressionTest(protocol.BaseTestCase): """ The issue: https://github.com/mopidy/mopidy/issues/113 How to reproduce: - Have a playlist with a name contining backslashes, like "all lart spotify:track:\w\{22\} pastes". - Try to load the playlist with the backslashes in the playlist name escaped. """ def test(self): self.core.playlists.create( u'all lart spotify:track:\w\{22\} pastes') self.sendRequest('lsinfo "/"') self.assertInResponse( u'playlist: all lart spotify:track:\w\{22\} pastes') self.sendRequest( r'listplaylistinfo "all lart spotify:track:\\w\\{22\\} pastes"') self.assertInResponse('OK') class IssueGH137RegressionTest(protocol.BaseTestCase): """ The issue: https://github.com/mopidy/mopidy/issues/137 How to reproduce: - Send "list" query with mismatching quotes """ def test(self): self.sendRequest( u'list Date Artist "Anita Ward" ' u'Album "This Is Remixed Hits - Mashups & Rare 12" Mixes"') self.assertInResponse('ACK [2@0] {list} Invalid unquoted character') mopidy-0.17.0/tests/frontends/mpd/protocol/status_test.py000066400000000000000000000022731224420023200236140ustar00rootroot00000000000000from __future__ import unicode_literals from mopidy.models import Track from tests.frontends.mpd import protocol class StatusHandlerTest(protocol.BaseTestCase): def test_clearerror(self): self.sendRequest('clearerror') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_currentsong(self): track = Track() self.core.tracklist.add([track]) self.core.playback.play() self.sendRequest('currentsong') self.assertInResponse('file: ') self.assertInResponse('Time: 0') self.assertInResponse('Artist: ') self.assertInResponse('Title: ') self.assertInResponse('Album: ') self.assertInResponse('Track: 0') self.assertNotInResponse('Date: ') self.assertInResponse('Pos: 0') self.assertInResponse('Id: 0') self.assertInResponse('OK') def test_currentsong_without_song(self): self.sendRequest('currentsong') self.assertInResponse('OK') def test_stats_command(self): self.sendRequest('stats') self.assertInResponse('OK') def test_status_command(self): self.sendRequest('status') self.assertInResponse('OK') mopidy-0.17.0/tests/frontends/mpd/protocol/stickers_test.py000066400000000000000000000024221224420023200241140ustar00rootroot00000000000000from __future__ import unicode_literals from tests.frontends.mpd import protocol class StickersHandlerTest(protocol.BaseTestCase): def test_sticker_get(self): self.sendRequest( 'sticker get "song" "file:///dev/urandom" "a_name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sticker_set(self): self.sendRequest( 'sticker set "song" "file:///dev/urandom" "a_name" "a_value"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sticker_delete_with_name(self): self.sendRequest( 'sticker delete "song" "file:///dev/urandom" "a_name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sticker_delete_without_name(self): self.sendRequest( 'sticker delete "song" "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sticker_list(self): self.sendRequest( 'sticker list "song" "file:///dev/urandom"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_sticker_find(self): self.sendRequest( 'sticker find "song" "file:///dev/urandom" "a_name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') mopidy-0.17.0/tests/frontends/mpd/protocol/stored_playlists_test.py000066400000000000000000000207121224420023200256730ustar00rootroot00000000000000from __future__ import unicode_literals import datetime from mopidy.models import Track, Playlist from tests.frontends.mpd import protocol class PlaylistsHandlerTest(protocol.BaseTestCase): def test_listplaylist(self): self.backend.playlists.playlists = [ Playlist( name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist "name"') self.assertInResponse('file: dummy:a') self.assertInResponse('OK') def test_listplaylist_without_quotes(self): self.backend.playlists.playlists = [ Playlist( name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylist name') self.assertInResponse('file: dummy:a') self.assertInResponse('OK') def test_listplaylist_fails_if_no_playlist_is_found(self): self.sendRequest('listplaylist "name"') self.assertEqualResponse('ACK [50@0] {listplaylist} No such playlist') def test_listplaylist_duplicate(self): playlist1 = Playlist(name='a', uri='dummy:a1', tracks=[Track(uri='b')]) playlist2 = Playlist(name='a', uri='dummy:a2', tracks=[Track(uri='c')]) self.backend.playlists.playlists = [playlist1, playlist2] self.sendRequest('listplaylist "a [2]"') self.assertInResponse('file: c') self.assertInResponse('OK') def test_listplaylistinfo(self): self.backend.playlists.playlists = [ Playlist( name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo "name"') self.assertInResponse('file: dummy:a') self.assertInResponse('Track: 0') self.assertNotInResponse('Pos: 0') self.assertInResponse('OK') def test_listplaylistinfo_without_quotes(self): self.backend.playlists.playlists = [ Playlist( name='name', uri='dummy:name', tracks=[Track(uri='dummy:a')])] self.sendRequest('listplaylistinfo name') self.assertInResponse('file: dummy:a') self.assertInResponse('Track: 0') self.assertNotInResponse('Pos: 0') self.assertInResponse('OK') def test_listplaylistinfo_fails_if_no_playlist_is_found(self): self.sendRequest('listplaylistinfo "name"') self.assertEqualResponse( 'ACK [50@0] {listplaylistinfo} No such playlist') def test_listplaylistinfo_duplicate(self): playlist1 = Playlist(name='a', uri='dummy:a1', tracks=[Track(uri='b')]) playlist2 = Playlist(name='a', uri='dummy:a2', tracks=[Track(uri='c')]) self.backend.playlists.playlists = [playlist1, playlist2] self.sendRequest('listplaylistinfo "a [2]"') self.assertInResponse('file: c') self.assertInResponse('Track: 0') self.assertNotInResponse('Pos: 0') self.assertInResponse('OK') def test_listplaylists(self): last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) self.backend.playlists.playlists = [ Playlist(name='a', uri='dummy:a', last_modified=last_modified)] self.sendRequest('listplaylists') self.assertInResponse('playlist: a') # Date without microseconds and with time zone information self.assertInResponse('Last-Modified: 2001-03-17T13:41:17Z') self.assertInResponse('OK') def test_listplaylists_duplicate(self): playlist1 = Playlist(name='a', uri='dummy:a1') playlist2 = Playlist(name='a', uri='dummy:a2') self.backend.playlists.playlists = [playlist1, playlist2] self.sendRequest('listplaylists') self.assertInResponse('playlist: a') self.assertInResponse('playlist: a [2]') self.assertInResponse('OK') def test_listplaylists_ignores_playlists_without_name(self): last_modified = datetime.datetime(2001, 3, 17, 13, 41, 17, 12345) self.backend.playlists.playlists = [ Playlist(name='', uri='dummy:', last_modified=last_modified)] self.sendRequest('listplaylists') self.assertNotInResponse('playlist: ') self.assertInResponse('OK') def test_listplaylists_replaces_newline_with_space(self): self.backend.playlists.playlists = [ Playlist(name='a\n', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a\n') self.assertInResponse('OK') def test_listplaylists_replaces_carriage_return_with_space(self): self.backend.playlists.playlists = [ Playlist(name='a\r', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a\r') self.assertInResponse('OK') def test_listplaylists_replaces_forward_slash_with_space(self): self.backend.playlists.playlists = [ Playlist(name='a/', uri='dummy:')] self.sendRequest('listplaylists') self.assertInResponse('playlist: a ') self.assertNotInResponse('playlist: a/') self.assertInResponse('OK') def test_load_appends_to_tracklist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.backend.playlists.playlists = [ Playlist(name='A-list', uri='dummy:A-list', tracks=[ Track(uri='c'), Track(uri='d'), Track(uri='e')])] self.sendRequest('load "A-list"') tracks = self.core.tracklist.tracks.get() self.assertEqual(5, len(tracks)) self.assertEqual('a', tracks[0].uri) self.assertEqual('b', tracks[1].uri) self.assertEqual('c', tracks[2].uri) self.assertEqual('d', tracks[3].uri) self.assertEqual('e', tracks[4].uri) self.assertInResponse('OK') def test_load_with_range_loads_part_of_playlist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.backend.playlists.playlists = [ Playlist(name='A-list', uri='dummy:A-list', tracks=[ Track(uri='c'), Track(uri='d'), Track(uri='e')])] self.sendRequest('load "A-list" "1:2"') tracks = self.core.tracklist.tracks.get() self.assertEqual(3, len(tracks)) self.assertEqual('a', tracks[0].uri) self.assertEqual('b', tracks[1].uri) self.assertEqual('d', tracks[2].uri) self.assertInResponse('OK') def test_load_with_range_without_end_loads_rest_of_playlist(self): self.core.tracklist.add([Track(uri='a'), Track(uri='b')]) self.assertEqual(len(self.core.tracklist.tracks.get()), 2) self.backend.playlists.playlists = [ Playlist(name='A-list', uri='dummy:A-list', tracks=[ Track(uri='c'), Track(uri='d'), Track(uri='e')])] self.sendRequest('load "A-list" "1:"') tracks = self.core.tracklist.tracks.get() self.assertEqual(4, len(tracks)) self.assertEqual('a', tracks[0].uri) self.assertEqual('b', tracks[1].uri) self.assertEqual('d', tracks[2].uri) self.assertEqual('e', tracks[3].uri) self.assertInResponse('OK') def test_load_unknown_playlist_acks(self): self.sendRequest('load "unknown playlist"') self.assertEqual(0, len(self.core.tracklist.tracks.get())) self.assertEqualResponse('ACK [50@0] {load} No such playlist') def test_playlistadd(self): self.sendRequest('playlistadd "name" "dummy:a"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistclear(self): self.sendRequest('playlistclear "name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistdelete(self): self.sendRequest('playlistdelete "name" "5"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_playlistmove(self): self.sendRequest('playlistmove "name" "5" "10"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_rename(self): self.sendRequest('rename "old_name" "new_name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_rm(self): self.sendRequest('rm "name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') def test_save(self): self.sendRequest('save "name"') self.assertEqualResponse('ACK [0@0] {} Not implemented') mopidy-0.17.0/tests/frontends/mpd/status_test.py000066400000000000000000000165411224420023200217560ustar00rootroot00000000000000from __future__ import unicode_literals import unittest import pykka from mopidy import core from mopidy.backends import dummy from mopidy.core import PlaybackState from mopidy.frontends.mpd import dispatcher from mopidy.frontends.mpd.protocol import status from mopidy.models import Track PAUSED = PlaybackState.PAUSED PLAYING = PlaybackState.PLAYING STOPPED = PlaybackState.STOPPED # FIXME migrate to using protocol.BaseTestCase instead of status.stats # directly? class StatusHandlerTest(unittest.TestCase): def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.dispatcher = dispatcher.MpdDispatcher(core=self.core) self.context = self.dispatcher.context def tearDown(self): pykka.ActorRegistry.stop_all() def test_stats_method(self): result = status.stats(self.context) self.assertIn('artists', result) self.assertGreaterEqual(int(result['artists']), 0) self.assertIn('albums', result) self.assertGreaterEqual(int(result['albums']), 0) self.assertIn('songs', result) self.assertGreaterEqual(int(result['songs']), 0) self.assertIn('uptime', result) self.assertGreaterEqual(int(result['uptime']), 0) self.assertIn('db_playtime', result) self.assertGreaterEqual(int(result['db_playtime']), 0) self.assertIn('db_update', result) self.assertGreaterEqual(int(result['db_update']), 0) self.assertIn('playtime', result) self.assertGreaterEqual(int(result['playtime']), 0) def test_status_method_contains_volume_with_na_value(self): result = dict(status.status(self.context)) self.assertIn('volume', result) self.assertEqual(int(result['volume']), -1) def test_status_method_contains_volume(self): self.core.playback.volume = 17 result = dict(status.status(self.context)) self.assertIn('volume', result) self.assertEqual(int(result['volume']), 17) def test_status_method_contains_repeat_is_0(self): result = dict(status.status(self.context)) self.assertIn('repeat', result) self.assertEqual(int(result['repeat']), 0) def test_status_method_contains_repeat_is_1(self): self.core.tracklist.repeat = 1 result = dict(status.status(self.context)) self.assertIn('repeat', result) self.assertEqual(int(result['repeat']), 1) def test_status_method_contains_random_is_0(self): result = dict(status.status(self.context)) self.assertIn('random', result) self.assertEqual(int(result['random']), 0) def test_status_method_contains_random_is_1(self): self.core.tracklist.random = 1 result = dict(status.status(self.context)) self.assertIn('random', result) self.assertEqual(int(result['random']), 1) def test_status_method_contains_single(self): result = dict(status.status(self.context)) self.assertIn('single', result) self.assertIn(int(result['single']), (0, 1)) def test_status_method_contains_consume_is_0(self): result = dict(status.status(self.context)) self.assertIn('consume', result) self.assertEqual(int(result['consume']), 0) def test_status_method_contains_consume_is_1(self): self.core.tracklist.consume = 1 result = dict(status.status(self.context)) self.assertIn('consume', result) self.assertEqual(int(result['consume']), 1) def test_status_method_contains_playlist(self): result = dict(status.status(self.context)) self.assertIn('playlist', result) self.assertIn(int(result['playlist']), xrange(0, 2 ** 31 - 1)) def test_status_method_contains_playlistlength(self): result = dict(status.status(self.context)) self.assertIn('playlistlength', result) self.assertGreaterEqual(int(result['playlistlength']), 0) def test_status_method_contains_xfade(self): result = dict(status.status(self.context)) self.assertIn('xfade', result) self.assertGreaterEqual(int(result['xfade']), 0) def test_status_method_contains_state_is_play(self): self.core.playback.state = PLAYING result = dict(status.status(self.context)) self.assertIn('state', result) self.assertEqual(result['state'], 'play') def test_status_method_contains_state_is_stop(self): self.core.playback.state = STOPPED result = dict(status.status(self.context)) self.assertIn('state', result) self.assertEqual(result['state'], 'stop') def test_status_method_contains_state_is_pause(self): self.core.playback.state = PLAYING self.core.playback.state = PAUSED result = dict(status.status(self.context)) self.assertIn('state', result) self.assertEqual(result['state'], 'pause') def test_status_method_when_playlist_loaded_contains_song(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('song', result) self.assertGreaterEqual(int(result['song']), 0) def test_status_method_when_playlist_loaded_contains_tlid_as_songid(self): self.core.tracklist.add([Track(uri='dummy:a')]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('songid', result) self.assertEqual(int(result['songid']), 0) def test_status_method_when_playing_contains_time_with_no_length(self): self.core.tracklist.add([Track(uri='dummy:a', length=None)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('time', result) (position, total) = result['time'].split(':') position = int(position) total = int(total) self.assertLessEqual(position, total) def test_status_method_when_playing_contains_time_with_length(self): self.core.tracklist.add([Track(uri='dummy:a', length=10000)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('time', result) (position, total) = result['time'].split(':') position = int(position) total = int(total) self.assertLessEqual(position, total) def test_status_method_when_playing_contains_elapsed(self): self.core.tracklist.add([Track(uri='dummy:a', length=60000)]) self.core.playback.play() self.core.playback.pause() self.core.playback.seek(59123) result = dict(status.status(self.context)) self.assertIn('elapsed', result) self.assertEqual(result['elapsed'], '59.123') def test_status_method_when_starting_playing_contains_elapsed_zero(self): self.core.tracklist.add([Track(uri='dummy:a', length=10000)]) self.core.playback.play() self.core.playback.pause() result = dict(status.status(self.context)) self.assertIn('elapsed', result) self.assertEqual(result['elapsed'], '0.000') def test_status_method_when_playing_contains_bitrate(self): self.core.tracklist.add([Track(uri='dummy:a', bitrate=320)]) self.core.playback.play() result = dict(status.status(self.context)) self.assertIn('bitrate', result) self.assertEqual(int(result['bitrate']), 320) mopidy-0.17.0/tests/frontends/mpd/translator_test.py000066400000000000000000000334051224420023200226220ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import os import unittest from mopidy.utils.path import mtime, uri_to_path from mopidy.frontends.mpd import translator, protocol from mopidy.models import Album, Artist, TlTrack, Playlist, Track class TrackMpdFormatTest(unittest.TestCase): track = Track( uri='a uri', artists=[Artist(name='an artist')], name='a name', album=Album(name='an album', num_tracks=13, artists=[Artist(name='an other artist')]), track_no=7, composers=[Artist(name='a composer')], performers=[Artist(name='a performer')], genre='a genre', date=datetime.date(1977, 1, 1), disc_no='1', comment='a comment', length=137000, ) def setUp(self): self.media_dir = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): mtime.undo_fake() def test_track_to_mpd_format_for_empty_track(self): result = translator.track_to_mpd_format(Track()) self.assertIn(('file', ''), result) self.assertIn(('Time', 0), result) self.assertIn(('Artist', ''), result) self.assertIn(('Title', ''), result) self.assertIn(('Album', ''), result) self.assertIn(('Track', 0), result) self.assertNotIn(('Date', ''), result) self.assertEqual(len(result), 6) def test_track_to_mpd_format_with_position(self): result = translator.track_to_mpd_format(Track(), position=1) self.assertNotIn(('Pos', 1), result) def test_track_to_mpd_format_with_tlid(self): result = translator.track_to_mpd_format(TlTrack(1, Track())) self.assertNotIn(('Id', 1), result) def test_track_to_mpd_format_with_position_and_tlid(self): result = translator.track_to_mpd_format( TlTrack(2, Track()), position=1) self.assertIn(('Pos', 1), result) self.assertIn(('Id', 2), result) def test_track_to_mpd_format_for_nonempty_track(self): result = translator.track_to_mpd_format( TlTrack(122, self.track), position=9) self.assertIn(('file', 'a uri'), result) self.assertIn(('Time', 137), result) self.assertIn(('Artist', 'an artist'), result) self.assertIn(('Title', 'a name'), result) self.assertIn(('Album', 'an album'), result) self.assertIn(('AlbumArtist', 'an other artist'), result) self.assertIn(('Composer', 'a composer'), result) self.assertIn(('Performer', 'a performer'), result) self.assertIn(('Genre', 'a genre'), result) self.assertIn(('Track', '7/13'), result) self.assertIn(('Date', datetime.date(1977, 1, 1)), result) self.assertIn(('Disc', '1'), result) self.assertIn(('Comment', 'a comment'), result) self.assertIn(('Pos', 9), result) self.assertIn(('Id', 122), result) self.assertEqual(len(result), 15) def test_track_to_mpd_format_musicbrainz_trackid(self): track = self.track.copy(musicbrainz_id='foo') result = translator.track_to_mpd_format(track) self.assertIn(('MUSICBRAINZ_TRACKID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_albumid(self): album = self.track.album.copy(musicbrainz_id='foo') track = self.track.copy(album=album) result = translator.track_to_mpd_format(track) self.assertIn(('MUSICBRAINZ_ALBUMID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_albumartistid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') album = self.track.album.copy(artists=[artist]) track = self.track.copy(album=album) result = translator.track_to_mpd_format(track) self.assertIn(('MUSICBRAINZ_ALBUMARTISTID', 'foo'), result) def test_track_to_mpd_format_musicbrainz_artistid(self): artist = list(self.track.artists)[0].copy(musicbrainz_id='foo') track = self.track.copy(artists=[artist]) result = translator.track_to_mpd_format(track) self.assertIn(('MUSICBRAINZ_ARTISTID', 'foo'), result) def test_artists_to_mpd_format(self): artists = [Artist(name='ABBA'), Artist(name='Beatles')] translated = translator.artists_to_mpd_format(artists) self.assertEqual(translated, 'ABBA, Beatles') def test_artists_to_mpd_format_artist_with_no_name(self): artists = [Artist(name=None)] translated = translator.artists_to_mpd_format(artists) self.assertEqual(translated, '') class PlaylistMpdFormatTest(unittest.TestCase): def test_mpd_format(self): playlist = Playlist(tracks=[ Track(track_no=1), Track(track_no=2), Track(track_no=3)]) result = translator.playlist_to_mpd_format(playlist) self.assertEqual(len(result), 3) def test_mpd_format_with_range(self): playlist = Playlist(tracks=[ Track(track_no=1), Track(track_no=2), Track(track_no=3)]) result = translator.playlist_to_mpd_format(playlist, 1, 2) self.assertEqual(len(result), 1) self.assertEqual(dict(result[0])['Track'], 2) class TracksToTagCacheFormatTest(unittest.TestCase): def setUp(self): self.media_dir = '/dir/subdir' mtime.set_fake_time(1234567) def tearDown(self): mtime.undo_fake() def translate(self, track): base_path = self.media_dir.encode('utf-8') result = dict(translator.track_to_mpd_format(track)) result['file'] = uri_to_path(result['file'])[len(base_path) + 1:] result['key'] = os.path.basename(result['file']) result['mtime'] = mtime('') return translator.order_mpd_track_info(result.items()) def consume_headers(self, result): self.assertEqual(('info_begin',), result[0]) self.assertEqual(('mpd_version', protocol.VERSION), result[1]) self.assertEqual(('fs_charset', protocol.ENCODING), result[2]) self.assertEqual(('info_end',), result[3]) return result[4:] def consume_song_list(self, result): self.assertEqual(('songList begin',), result[0]) for i, row in enumerate(result): if row == ('songList end',): return result[1:i], result[i + 1:] self.fail("Couldn't find songList end in result") def consume_directory(self, result): self.assertEqual('directory', result[0][0]) self.assertEqual(('mtime', mtime('.')), result[1]) self.assertEqual(('begin', os.path.split(result[0][1])[1]), result[2]) directory = result[2][1] for i, row in enumerate(result): if row == ('end', directory): return result[3:i], result[i + 1:] self.fail("Couldn't find end %s in result" % directory) def test_empty_tag_cache_has_header(self): result = translator.tracks_to_tag_cache_format([], self.media_dir) result = self.consume_headers(result) def test_empty_tag_cache_has_song_list(self): result = translator.tracks_to_tag_cache_format([], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) def test_tag_cache_has_header(self): track = Track(uri='file:///dir/subdir/song.mp3') result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) def test_tag_cache_has_song_list(self): track = Track(uri='file:///dir/subdir/song.mp3') result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) self.assert_(song_list) self.assertEqual(len(result), 0) def test_tag_cache_has_formated_track(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) self.assertEqual(formated, song_list) self.assertEqual(len(result), 0) def test_tag_cache_has_formated_track_with_key_and_mtime(self): track = Track(uri='file:///dir/subdir/song.mp3') formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) self.assertEqual(formated, song_list) self.assertEqual(len(result), 0) def test_tag_cache_supports_directories(self): track = Track(uri='file:///dir/subdir/folder/song.mp3') formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) dir_data, result = self.consume_directory(result) song_list, result = self.consume_song_list(result) self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) song_list, result = self.consume_song_list(dir_data) self.assertEqual(len(result), 0) self.assertEqual(formated, song_list) def test_tag_cache_diretory_header_is_right(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) dir_data, result = self.consume_directory(result) self.assertEqual(('directory', 'folder/sub'), dir_data[0]) self.assertEqual(('mtime', mtime('.')), dir_data[1]) self.assertEqual(('begin', 'sub'), dir_data[2]) def test_tag_cache_suports_sub_directories(self): track = Track(uri='file:///dir/subdir/folder/sub/song.mp3') formated = self.translate(track) result = translator.tracks_to_tag_cache_format([track], self.media_dir) result = self.consume_headers(result) dir_data, result = self.consume_directory(result) song_list, result = self.consume_song_list(result) self.assertEqual(len(song_list), 0) self.assertEqual(len(result), 0) dir_data, result = self.consume_directory(dir_data) song_list, result = self.consume_song_list(result) self.assertEqual(len(result), 0) self.assertEqual(len(song_list), 0) song_list, result = self.consume_song_list(dir_data) self.assertEqual(len(result), 0) self.assertEqual(formated, song_list) def test_tag_cache_supports_multiple_tracks(self): tracks = [ Track(uri='file:///dir/subdir/song1.mp3'), Track(uri='file:///dir/subdir/song2.mp3'), ] formated = [] formated.extend(self.translate(tracks[0])) formated.extend(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks, self.media_dir) result = self.consume_headers(result) song_list, result = self.consume_song_list(result) self.assertEqual(formated, song_list) self.assertEqual(len(result), 0) def test_tag_cache_supports_multiple_tracks_in_dirs(self): tracks = [ Track(uri='file:///dir/subdir/song1.mp3'), Track(uri='file:///dir/subdir/folder/song2.mp3'), ] formated = [] formated.append(self.translate(tracks[0])) formated.append(self.translate(tracks[1])) result = translator.tracks_to_tag_cache_format(tracks, self.media_dir) result = self.consume_headers(result) dir_data, result = self.consume_directory(result) song_list, song_result = self.consume_song_list(dir_data) self.assertEqual(formated[1], song_list) self.assertEqual(len(song_result), 0) song_list, result = self.consume_song_list(result) self.assertEqual(len(result), 0) self.assertEqual(formated[0], song_list) class TracksToDirectoryTreeTest(unittest.TestCase): def setUp(self): self.media_dir = '/root' def test_no_tracks_gives_emtpy_tree(self): tree = translator.tracks_to_directory_tree([], self.media_dir) self.assertEqual(tree, ({}, [])) def test_top_level_files(self): tracks = [ Track(uri='file:///root/file1.mp3'), Track(uri='file:///root/file2.mp3'), Track(uri='file:///root/file3.mp3'), ] tree = translator.tracks_to_directory_tree(tracks, self.media_dir) self.assertEqual(tree, ({}, tracks)) def test_single_file_in_subdir(self): tracks = [Track(uri='file:///root/dir/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ({'dir': ({}, tracks)}, []) self.assertEqual(tree, expected) def test_single_file_in_sub_subdir(self): tracks = [Track(uri='file:///root/dir1/dir2/file1.mp3')] tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ({'dir1': ({'dir1/dir2': ({}, tracks)}, [])}, []) self.assertEqual(tree, expected) def test_complex_file_structure(self): tracks = [ Track(uri='file:///root/file1.mp3'), Track(uri='file:///root/dir1/file2.mp3'), Track(uri='file:///root/dir1/file3.mp3'), Track(uri='file:///root/dir2/file4.mp3'), Track(uri='file:///root/dir2/sub/file5.mp3'), ] tree = translator.tracks_to_directory_tree(tracks, self.media_dir) expected = ( { 'dir1': ({}, [tracks[1], tracks[2]]), 'dir2': ( { 'dir2/sub': ({}, [tracks[4]]) }, [tracks[3]] ), }, [tracks[0]] ) self.assertEqual(tree, expected) mopidy-0.17.0/tests/help_test.py000066400000000000000000000014351224420023200165750ustar00rootroot00000000000000from __future__ import unicode_literals import os import subprocess import sys import unittest import mopidy class HelpTest(unittest.TestCase): def test_help_has_mopidy_options(self): mopidy_dir = os.path.dirname(mopidy.__file__) args = [sys.executable, mopidy_dir, '--help'] process = subprocess.Popen( args, env={'PYTHONPATH': os.path.join(mopidy_dir, '..')}, stdout=subprocess.PIPE) output = process.communicate()[0] self.assertIn('--version', output) self.assertIn('--help', output) self.assertIn('--quiet', output) self.assertIn('--verbose', output) self.assertIn('--save-debug-log', output) self.assertIn('--config', output) self.assertIn('--option', output) mopidy-0.17.0/tests/models_test.py000066400000000000000000001041251224420023200171300ustar00rootroot00000000000000from __future__ import unicode_literals import datetime import json import unittest from mopidy.models import ( Artist, Album, TlTrack, Track, Playlist, SearchResult, ModelJSONEncoder, model_json_decoder) class GenericCopyTest(unittest.TestCase): def compare(self, orig, other): self.assertEqual(orig, other) self.assertNotEqual(id(orig), id(other)) def test_copying_track(self): track = Track() self.compare(track, track.copy()) def test_copying_artist(self): artist = Artist() self.compare(artist, artist.copy()) def test_copying_album(self): album = Album() self.compare(album, album.copy()) def test_copying_playlist(self): playlist = Playlist() self.compare(playlist, playlist.copy()) def test_copying_track_with_basic_values(self): track = Track(name='foo', uri='bar') copy = track.copy(name='baz') self.assertEqual('baz', copy.name) self.assertEqual('bar', copy.uri) def test_copying_track_with_missing_values(self): track = Track(uri='bar') copy = track.copy(name='baz') self.assertEqual('baz', copy.name) self.assertEqual('bar', copy.uri) def test_copying_track_with_private_internal_value(self): artist1 = Artist(name='foo') artist2 = Artist(name='bar') track = Track(artists=[artist1]) copy = track.copy(artists=[artist2]) self.assertIn(artist2, copy.artists) def test_copying_track_with_invalid_key(self): test = lambda: Track().copy(invalid_key=True) self.assertRaises(TypeError, test) class ArtistTest(unittest.TestCase): def test_uri(self): uri = 'an_uri' artist = Artist(uri=uri) self.assertEqual(artist.uri, uri) self.assertRaises(AttributeError, setattr, artist, 'uri', None) def test_name(self): name = 'a name' artist = Artist(name=name) self.assertEqual(artist.name, name) self.assertRaises(AttributeError, setattr, artist, 'name', None) def test_musicbrainz_id(self): mb_id = 'mb-id' artist = Artist(musicbrainz_id=mb_id) self.assertEqual(artist.musicbrainz_id, mb_id) self.assertRaises( AttributeError, setattr, artist, 'musicbrainz_id', None) def test_invalid_kwarg(self): test = lambda: Artist(foo='baz') self.assertRaises(TypeError, test) def test_invalid_kwarg_with_name_matching_method(self): test = lambda: Artist(copy='baz') self.assertRaises(TypeError, test) test = lambda: Artist(serialize='baz') self.assertRaises(TypeError, test) def test_repr(self): self.assertEquals( "Artist(name=u'name', uri=u'uri')", repr(Artist(uri='uri', name='name'))) def test_serialize(self): self.assertDictEqual( {'__model__': 'Artist', 'uri': 'uri', 'name': 'name'}, Artist(uri='uri', name='name').serialize()) def test_serialize_falsy_values(self): self.assertDictEqual( {'__model__': 'Artist', 'uri': '', 'name': None}, Artist(uri='', name=None).serialize()) def test_to_json_and_back(self): artist1 = Artist(uri='uri', name='name') serialized = json.dumps(artist1, cls=ModelJSONEncoder) artist2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(artist1, artist2) def test_to_json_and_back_with_unknown_field(self): artist = Artist(uri='uri', name='name').serialize() artist['foo'] = 'foo' serialized = json.dumps(artist) test = lambda: json.loads(serialized, object_hook=model_json_decoder) self.assertRaises(TypeError, test) def test_to_json_and_back_with_field_matching_method(self): artist = Artist(uri='uri', name='name').serialize() artist['copy'] = 'foo' serialized = json.dumps(artist) test = lambda: json.loads(serialized, object_hook=model_json_decoder) self.assertRaises(TypeError, test) def test_to_json_and_back_with_field_matching_internal_field(self): artist = Artist(uri='uri', name='name').serialize() artist['__mro__'] = 'foo' serialized = json.dumps(artist) test = lambda: json.loads(serialized, object_hook=model_json_decoder) self.assertRaises(TypeError, test) def test_eq_name(self): artist1 = Artist(name='name') artist2 = Artist(name='name') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) def test_eq_uri(self): artist1 = Artist(uri='uri') artist2 = Artist(uri='uri') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) def test_eq_musibrainz_id(self): artist1 = Artist(musicbrainz_id='id') artist2 = Artist(musicbrainz_id='id') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) def test_eq(self): artist1 = Artist(uri='uri', name='name', musicbrainz_id='id') artist2 = Artist(uri='uri', name='name', musicbrainz_id='id') self.assertEqual(artist1, artist2) self.assertEqual(hash(artist1), hash(artist2)) def test_eq_none(self): self.assertNotEqual(Artist(), None) def test_eq_other(self): self.assertNotEqual(Artist(), 'other') def test_ne_name(self): artist1 = Artist(name='name1') artist2 = Artist(name='name2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) def test_ne_uri(self): artist1 = Artist(uri='uri1') artist2 = Artist(uri='uri2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) def test_ne_musicbrainz_id(self): artist1 = Artist(musicbrainz_id='id1') artist2 = Artist(musicbrainz_id='id2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) def test_ne(self): artist1 = Artist(uri='uri1', name='name1', musicbrainz_id='id1') artist2 = Artist(uri='uri2', name='name2', musicbrainz_id='id2') self.assertNotEqual(artist1, artist2) self.assertNotEqual(hash(artist1), hash(artist2)) class AlbumTest(unittest.TestCase): def test_uri(self): uri = 'an_uri' album = Album(uri=uri) self.assertEqual(album.uri, uri) self.assertRaises(AttributeError, setattr, album, 'uri', None) def test_name(self): name = 'a name' album = Album(name=name) self.assertEqual(album.name, name) self.assertRaises(AttributeError, setattr, album, 'name', None) def test_artists(self): artist = Artist() album = Album(artists=[artist]) self.assertIn(artist, album.artists) self.assertRaises(AttributeError, setattr, album, 'artists', None) def test_num_tracks(self): num_tracks = 11 album = Album(num_tracks=num_tracks) self.assertEqual(album.num_tracks, num_tracks) self.assertRaises(AttributeError, setattr, album, 'num_tracks', None) def test_num_discs(self): num_discs = 2 album = Album(num_discs=num_discs) self.assertEqual(album.num_discs, num_discs) self.assertRaises(AttributeError, setattr, album, 'num_discs', None) def test_date(self): date = '1977-01-01' album = Album(date=date) self.assertEqual(album.date, date) self.assertRaises(AttributeError, setattr, album, 'date', None) def test_musicbrainz_id(self): mb_id = 'mb-id' album = Album(musicbrainz_id=mb_id) self.assertEqual(album.musicbrainz_id, mb_id) self.assertRaises( AttributeError, setattr, album, 'musicbrainz_id', None) def test_images(self): image = 'data:foobar' album = Album(images=[image]) self.assertIn(image, album.images) self.assertRaises(AttributeError, setattr, album, 'images', None) def test_invalid_kwarg(self): test = lambda: Album(foo='baz') self.assertRaises(TypeError, test) def test_repr_without_artists(self): self.assertEquals( "Album(artists=[], images=[], name=u'name', uri=u'uri')", repr(Album(uri='uri', name='name'))) def test_repr_with_artists(self): self.assertEquals( "Album(artists=[Artist(name=u'foo')], images=[], name=u'name', " "uri=u'uri')", repr(Album(uri='uri', name='name', artists=[Artist(name='foo')]))) def test_serialize_without_artists(self): self.assertDictEqual( {'__model__': 'Album', 'uri': 'uri', 'name': 'name'}, Album(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( {'__model__': 'Album', 'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, Album(uri='uri', name='name', artists=[artist]).serialize()) def test_serialize_with_images(self): image = 'data:foobar' self.assertDictEqual( {'__model__': 'Album', 'uri': 'uri', 'name': 'name', 'images': [image]}, Album(uri='uri', name='name', images=[image]).serialize()) def test_to_json_and_back(self): album1 = Album(uri='uri', name='name', artists=[Artist(name='foo')]) serialized = json.dumps(album1, cls=ModelJSONEncoder) album2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(album1, album2) def test_eq_name(self): album1 = Album(name='name') album2 = Album(name='name') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_uri(self): album1 = Album(uri='uri') album2 = Album(uri='uri') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_artists(self): artists = [Artist()] album1 = Album(artists=artists) album2 = Album(artists=artists) self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_artists_order(self): artist1 = Artist(name='name1') artist2 = Artist(name='name2') album1 = Album(artists=[artist1, artist2]) album2 = Album(artists=[artist2, artist1]) self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_num_tracks(self): album1 = Album(num_tracks=2) album2 = Album(num_tracks=2) self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_date(self): date = '1977-01-01' album1 = Album(date=date) album2 = Album(date=date) self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_musibrainz_id(self): album1 = Album(musicbrainz_id='id') album2 = Album(musicbrainz_id='id') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq(self): artists = [Artist()] album1 = Album( name='name', uri='uri', artists=artists, num_tracks=2, musicbrainz_id='id') album2 = Album( name='name', uri='uri', artists=artists, num_tracks=2, musicbrainz_id='id') self.assertEqual(album1, album2) self.assertEqual(hash(album1), hash(album2)) def test_eq_none(self): self.assertNotEqual(Album(), None) def test_eq_other(self): self.assertNotEqual(Album(), 'other') def test_ne_name(self): album1 = Album(name='name1') album2 = Album(name='name2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne_uri(self): album1 = Album(uri='uri1') album2 = Album(uri='uri2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne_artists(self): album1 = Album(artists=[Artist(name='name1')]) album2 = Album(artists=[Artist(name='name2')]) self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne_num_tracks(self): album1 = Album(num_tracks=1) album2 = Album(num_tracks=2) self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne_date(self): album1 = Album(date='1977-01-01') album2 = Album(date='1977-01-02') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne_musicbrainz_id(self): album1 = Album(musicbrainz_id='id1') album2 = Album(musicbrainz_id='id2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) def test_ne(self): album1 = Album( name='name1', uri='uri1', artists=[Artist(name='name1')], num_tracks=1, musicbrainz_id='id1') album2 = Album( name='name2', uri='uri2', artists=[Artist(name='name2')], num_tracks=2, musicbrainz_id='id2') self.assertNotEqual(album1, album2) self.assertNotEqual(hash(album1), hash(album2)) class TrackTest(unittest.TestCase): def test_uri(self): uri = 'an_uri' track = Track(uri=uri) self.assertEqual(track.uri, uri) self.assertRaises(AttributeError, setattr, track, 'uri', None) def test_name(self): name = 'a name' track = Track(name=name) self.assertEqual(track.name, name) self.assertRaises(AttributeError, setattr, track, 'name', None) def test_artists(self): artists = [Artist(name='name1'), Artist(name='name2')] track = Track(artists=artists) self.assertEqual(set(track.artists), set(artists)) self.assertRaises(AttributeError, setattr, track, 'artists', None) def test_album(self): album = Album() track = Track(album=album) self.assertEqual(track.album, album) self.assertRaises(AttributeError, setattr, track, 'album', None) def test_track_no(self): track_no = 7 track = Track(track_no=track_no) self.assertEqual(track.track_no, track_no) self.assertRaises(AttributeError, setattr, track, 'track_no', None) def test_disc_no(self): disc_no = 2 track = Track(disc_no=disc_no) self.assertEqual(track.disc_no, disc_no) self.assertRaises(AttributeError, setattr, track, 'disc_no', None) def test_date(self): date = '1977-01-01' track = Track(date=date) self.assertEqual(track.date, date) self.assertRaises(AttributeError, setattr, track, 'date', None) def test_length(self): length = 137000 track = Track(length=length) self.assertEqual(track.length, length) self.assertRaises(AttributeError, setattr, track, 'length', None) def test_bitrate(self): bitrate = 160 track = Track(bitrate=bitrate) self.assertEqual(track.bitrate, bitrate) self.assertRaises(AttributeError, setattr, track, 'bitrate', None) def test_musicbrainz_id(self): mb_id = 'mb-id' track = Track(musicbrainz_id=mb_id) self.assertEqual(track.musicbrainz_id, mb_id) self.assertRaises( AttributeError, setattr, track, 'musicbrainz_id', None) def test_invalid_kwarg(self): test = lambda: Track(foo='baz') self.assertRaises(TypeError, test) def test_repr_without_artists(self): self.assertEquals( "Track(artists=[], composers=[], name=u'name', " "performers=[], uri=u'uri')", repr(Track(uri='uri', name='name'))) def test_repr_with_artists(self): self.assertEquals( "Track(artists=[Artist(name=u'foo')], composers=[], name=u'name', " "performers=[], uri=u'uri')", repr(Track(uri='uri', name='name', artists=[Artist(name='foo')]))) def test_serialize_without_artists(self): self.assertDictEqual( {'__model__': 'Track', 'uri': 'uri', 'name': 'name'}, Track(uri='uri', name='name').serialize()) def test_serialize_with_artists(self): artist = Artist(name='foo') self.assertDictEqual( {'__model__': 'Track', 'uri': 'uri', 'name': 'name', 'artists': [artist.serialize()]}, Track(uri='uri', name='name', artists=[artist]).serialize()) def test_serialize_with_album(self): album = Album(name='foo') self.assertDictEqual( {'__model__': 'Track', 'uri': 'uri', 'name': 'name', 'album': album.serialize()}, Track(uri='uri', name='name', album=album).serialize()) def test_to_json_and_back(self): track1 = Track( uri='uri', name='name', album=Album(name='foo'), artists=[Artist(name='foo')]) serialized = json.dumps(track1, cls=ModelJSONEncoder) track2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(track1, track2) def test_eq_uri(self): track1 = Track(uri='uri1') track2 = Track(uri='uri1') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_name(self): track1 = Track(name='name1') track2 = Track(name='name1') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_artists(self): artists = [Artist()] track1 = Track(artists=artists) track2 = Track(artists=artists) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_artists_order(self): artist1 = Artist(name='name1') artist2 = Artist(name='name2') track1 = Track(artists=[artist1, artist2]) track2 = Track(artists=[artist2, artist1]) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_album(self): album = Album() track1 = Track(album=album) track2 = Track(album=album) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_track_no(self): track1 = Track(track_no=1) track2 = Track(track_no=1) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_date(self): date = '1977-01-01' track1 = Track(date=date) track2 = Track(date=date) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_length(self): track1 = Track(length=100) track2 = Track(length=100) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_bitrate(self): track1 = Track(bitrate=100) track2 = Track(bitrate=100) self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_musibrainz_id(self): track1 = Track(musicbrainz_id='id') track2 = Track(musicbrainz_id='id') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq(self): date = '1977-01-01' artists = [Artist()] album = Album() track1 = Track( uri='uri', name='name', artists=artists, album=album, track_no=1, date=date, length=100, bitrate=100, musicbrainz_id='id') track2 = Track( uri='uri', name='name', artists=artists, album=album, track_no=1, date=date, length=100, bitrate=100, musicbrainz_id='id') self.assertEqual(track1, track2) self.assertEqual(hash(track1), hash(track2)) def test_eq_none(self): self.assertNotEqual(Track(), None) def test_eq_other(self): self.assertNotEqual(Track(), 'other') def test_ne_uri(self): track1 = Track(uri='uri1') track2 = Track(uri='uri2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_name(self): track1 = Track(name='name1') track2 = Track(name='name2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_artists(self): track1 = Track(artists=[Artist(name='name1')]) track2 = Track(artists=[Artist(name='name2')]) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_album(self): track1 = Track(album=Album(name='name1')) track2 = Track(album=Album(name='name2')) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_track_no(self): track1 = Track(track_no=1) track2 = Track(track_no=2) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_date(self): track1 = Track(date='1977-01-01') track2 = Track(date='1977-01-02') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_length(self): track1 = Track(length=100) track2 = Track(length=200) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_bitrate(self): track1 = Track(bitrate=100) track2 = Track(bitrate=200) self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne_musicbrainz_id(self): track1 = Track(musicbrainz_id='id1') track2 = Track(musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) def test_ne(self): track1 = Track( uri='uri1', name='name1', artists=[Artist(name='name1')], album=Album(name='name1'), track_no=1, date='1977-01-01', length=100, bitrate=100, musicbrainz_id='id1') track2 = Track( uri='uri2', name='name2', artists=[Artist(name='name2')], album=Album(name='name2'), track_no=2, date='1977-01-02', length=200, bitrate=200, musicbrainz_id='id2') self.assertNotEqual(track1, track2) self.assertNotEqual(hash(track1), hash(track2)) class TlTrackTest(unittest.TestCase): def test_tlid(self): tlid = 123 tl_track = TlTrack(tlid=tlid) self.assertEqual(tl_track.tlid, tlid) self.assertRaises(AttributeError, setattr, tl_track, 'tlid', None) def test_track(self): track = Track() tl_track = TlTrack(track=track) self.assertEqual(tl_track.track, track) self.assertRaises(AttributeError, setattr, tl_track, 'track', None) def test_invalid_kwarg(self): test = lambda: TlTrack(foo='baz') self.assertRaises(TypeError, test) def test_positional_args(self): tlid = 123 track = Track() tl_track = TlTrack(tlid, track) self.assertEqual(tl_track.tlid, tlid) self.assertEqual(tl_track.track, track) def test_iteration(self): tlid = 123 track = Track() tl_track = TlTrack(tlid, track) (tlid2, track2) = tl_track self.assertEqual(tlid2, tlid) self.assertEqual(track2, track) def test_repr(self): self.assertEquals( "TlTrack(tlid=123, track=Track(artists=[], composers=[], " "performers=[], uri=u'uri'))", repr(TlTrack(tlid=123, track=Track(uri='uri')))) def test_serialize(self): track = Track(uri='uri', name='name') self.assertDictEqual( {'__model__': 'TlTrack', 'tlid': 123, 'track': track.serialize()}, TlTrack(tlid=123, track=track).serialize()) def test_to_json_and_back(self): tl_track1 = TlTrack(tlid=123, track=Track(uri='uri', name='name')) serialized = json.dumps(tl_track1, cls=ModelJSONEncoder) tl_track2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(tl_track1, tl_track2) def test_eq(self): tlid = 123 track = Track() tl_track1 = TlTrack(tlid=tlid, track=track) tl_track2 = TlTrack(tlid=tlid, track=track) self.assertEqual(tl_track1, tl_track2) self.assertEqual(hash(tl_track1), hash(tl_track2)) def test_eq_none(self): self.assertNotEqual(TlTrack(), None) def test_eq_other(self): self.assertNotEqual(TlTrack(), 'other') def test_ne_tlid(self): tl_track1 = TlTrack(tlid=123) tl_track2 = TlTrack(tlid=321) self.assertNotEqual(tl_track1, tl_track2) self.assertNotEqual(hash(tl_track1), hash(tl_track2)) def test_ne_track(self): tl_track1 = TlTrack(track=Track(uri='a')) tl_track2 = TlTrack(track=Track(uri='b')) self.assertNotEqual(tl_track1, tl_track2) self.assertNotEqual(hash(tl_track1), hash(tl_track2)) class PlaylistTest(unittest.TestCase): def test_uri(self): uri = 'an_uri' playlist = Playlist(uri=uri) self.assertEqual(playlist.uri, uri) self.assertRaises(AttributeError, setattr, playlist, 'uri', None) def test_name(self): name = 'a name' playlist = Playlist(name=name) self.assertEqual(playlist.name, name) self.assertRaises(AttributeError, setattr, playlist, 'name', None) def test_tracks(self): tracks = [Track(), Track(), Track()] playlist = Playlist(tracks=tracks) self.assertEqual(list(playlist.tracks), tracks) self.assertRaises(AttributeError, setattr, playlist, 'tracks', None) def test_length(self): tracks = [Track(), Track(), Track()] playlist = Playlist(tracks=tracks) self.assertEqual(playlist.length, 3) def test_last_modified(self): last_modified = datetime.datetime.utcnow() playlist = Playlist(last_modified=last_modified) self.assertEqual(playlist.last_modified, last_modified) self.assertRaises( AttributeError, setattr, playlist, 'last_modified', None) def test_with_new_uri(self): tracks = [Track()] last_modified = datetime.datetime.utcnow() playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(uri='another uri') self.assertEqual(new_playlist.uri, 'another uri') self.assertEqual(new_playlist.name, 'a name') self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_name(self): tracks = [Track()] last_modified = datetime.datetime.utcnow() playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(name='another name') self.assertEqual(new_playlist.uri, 'an uri') self.assertEqual(new_playlist.name, 'another name') self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_tracks(self): tracks = [Track()] last_modified = datetime.datetime.utcnow() playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) new_tracks = [Track(), Track()] new_playlist = playlist.copy(tracks=new_tracks) self.assertEqual(new_playlist.uri, 'an uri') self.assertEqual(new_playlist.name, 'a name') self.assertEqual(list(new_playlist.tracks), new_tracks) self.assertEqual(new_playlist.last_modified, last_modified) def test_with_new_last_modified(self): tracks = [Track()] last_modified = datetime.datetime.utcnow() new_last_modified = last_modified + datetime.timedelta(1) playlist = Playlist( uri='an uri', name='a name', tracks=tracks, last_modified=last_modified) new_playlist = playlist.copy(last_modified=new_last_modified) self.assertEqual(new_playlist.uri, 'an uri') self.assertEqual(new_playlist.name, 'a name') self.assertEqual(list(new_playlist.tracks), tracks) self.assertEqual(new_playlist.last_modified, new_last_modified) def test_invalid_kwarg(self): test = lambda: Playlist(foo='baz') self.assertRaises(TypeError, test) def test_repr_without_tracks(self): self.assertEquals( "Playlist(name=u'name', tracks=[], uri=u'uri')", repr(Playlist(uri='uri', name='name'))) def test_repr_with_tracks(self): self.assertEquals( "Playlist(name=u'name', tracks=[Track(artists=[], composers=[], " "name=u'foo', performers=[])], uri=u'uri')", repr(Playlist(uri='uri', name='name', tracks=[Track(name='foo')]))) def test_serialize_without_tracks(self): self.assertDictEqual( {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name'}, Playlist(uri='uri', name='name').serialize()) def test_serialize_with_tracks(self): track = Track(name='foo') self.assertDictEqual( {'__model__': 'Playlist', 'uri': 'uri', 'name': 'name', 'tracks': [track.serialize()]}, Playlist(uri='uri', name='name', tracks=[track]).serialize()) def test_to_json_and_back(self): playlist1 = Playlist(uri='uri', name='name') serialized = json.dumps(playlist1, cls=ModelJSONEncoder) playlist2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(playlist1, playlist2) def test_eq_name(self): playlist1 = Playlist(name='name') playlist2 = Playlist(name='name') self.assertEqual(playlist1, playlist2) self.assertEqual(hash(playlist1), hash(playlist2)) def test_eq_uri(self): playlist1 = Playlist(uri='uri') playlist2 = Playlist(uri='uri') self.assertEqual(playlist1, playlist2) self.assertEqual(hash(playlist1), hash(playlist2)) def test_eq_tracks(self): tracks = [Track()] playlist1 = Playlist(tracks=tracks) playlist2 = Playlist(tracks=tracks) self.assertEqual(playlist1, playlist2) self.assertEqual(hash(playlist1), hash(playlist2)) def test_eq_last_modified(self): playlist1 = Playlist(last_modified=1) playlist2 = Playlist(last_modified=1) self.assertEqual(playlist1, playlist2) self.assertEqual(hash(playlist1), hash(playlist2)) def test_eq(self): tracks = [Track()] playlist1 = Playlist( uri='uri', name='name', tracks=tracks, last_modified=1) playlist2 = Playlist( uri='uri', name='name', tracks=tracks, last_modified=1) self.assertEqual(playlist1, playlist2) self.assertEqual(hash(playlist1), hash(playlist2)) def test_eq_none(self): self.assertNotEqual(Playlist(), None) def test_eq_other(self): self.assertNotEqual(Playlist(), 'other') def test_ne_name(self): playlist1 = Playlist(name='name1') playlist2 = Playlist(name='name2') self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(hash(playlist1), hash(playlist2)) def test_ne_uri(self): playlist1 = Playlist(uri='uri1') playlist2 = Playlist(uri='uri2') self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(hash(playlist1), hash(playlist2)) def test_ne_tracks(self): playlist1 = Playlist(tracks=[Track(uri='uri1')]) playlist2 = Playlist(tracks=[Track(uri='uri2')]) self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(hash(playlist1), hash(playlist2)) def test_ne_last_modified(self): playlist1 = Playlist(last_modified=1) playlist2 = Playlist(last_modified=2) self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(hash(playlist1), hash(playlist2)) def test_ne(self): playlist1 = Playlist( uri='uri1', name='name1', tracks=[Track(uri='uri1')], last_modified=1) playlist2 = Playlist( uri='uri2', name='name2', tracks=[Track(uri='uri2')], last_modified=2) self.assertNotEqual(playlist1, playlist2) self.assertNotEqual(hash(playlist1), hash(playlist2)) class SearchResultTest(unittest.TestCase): def test_uri(self): uri = 'an_uri' result = SearchResult(uri=uri) self.assertEqual(result.uri, uri) self.assertRaises(AttributeError, setattr, result, 'uri', None) def test_tracks(self): tracks = [Track(), Track(), Track()] result = SearchResult(tracks=tracks) self.assertEqual(list(result.tracks), tracks) self.assertRaises(AttributeError, setattr, result, 'tracks', None) def test_artists(self): artists = [Artist(), Artist(), Artist()] result = SearchResult(artists=artists) self.assertEqual(list(result.artists), artists) self.assertRaises(AttributeError, setattr, result, 'artists', None) def test_albums(self): albums = [Album(), Album(), Album()] result = SearchResult(albums=albums) self.assertEqual(list(result.albums), albums) self.assertRaises(AttributeError, setattr, result, 'albums', None) def test_invalid_kwarg(self): test = lambda: SearchResult(foo='baz') self.assertRaises(TypeError, test) def test_repr_without_results(self): self.assertEquals( "SearchResult(albums=[], artists=[], tracks=[], uri=u'uri')", repr(SearchResult(uri='uri'))) def test_serialize_without_results(self): self.assertDictEqual( {'__model__': 'SearchResult', 'uri': 'uri'}, SearchResult(uri='uri').serialize()) def test_to_json_and_back(self): result1 = SearchResult(uri='uri') serialized = json.dumps(result1, cls=ModelJSONEncoder) result2 = json.loads(serialized, object_hook=model_json_decoder) self.assertEqual(result1, result2) mopidy-0.17.0/tests/outputs/000077500000000000000000000000001224420023200157545ustar00rootroot00000000000000mopidy-0.17.0/tests/outputs/__init__.py000066400000000000000000000000501224420023200200600ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/tests/utils/000077500000000000000000000000001224420023200153715ustar00rootroot00000000000000mopidy-0.17.0/tests/utils/__init__.py000066400000000000000000000000501224420023200174750ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/tests/utils/deps_test.py000066400000000000000000000110611224420023200177340ustar00rootroot00000000000000from __future__ import unicode_literals import platform import mock import unittest import pygst pygst.require('0.10') import gst import pkg_resources from mopidy.utils import deps class DepsTest(unittest.TestCase): def test_format_dependency_list(self): adapters = [ lambda: dict(name='Python', version='FooPython 2.7.3'), lambda: dict(name='Platform', version='Loonix 4.0.1'), lambda: dict( name='Pykka', version='1.1', path='/foo/bar', other='Quux'), lambda: dict(name='Foo'), lambda: dict(name='Mopidy', version='0.13', dependencies=[ dict(name='pylast', version='0.5', dependencies=[ dict(name='setuptools', version='0.6') ]) ]) ] result = deps.format_dependency_list(adapters) self.assertIn('Python: FooPython 2.7.3', result) self.assertIn('Platform: Loonix 4.0.1', result) self.assertIn('Pykka: 1.1 from /foo/bar', result) self.assertNotIn('/baz.py', result) self.assertIn('Detailed information: Quux', result) self.assertIn('Foo: not found', result) self.assertIn('Mopidy: 0.13', result) self.assertIn(' pylast: 0.5', result) self.assertIn(' setuptools: 0.6', result) def test_platform_info(self): result = deps.platform_info() self.assertEquals('Platform', result['name']) self.assertIn(platform.platform(), result['version']) def test_python_info(self): result = deps.python_info() self.assertEquals('Python', result['name']) self.assertIn(platform.python_implementation(), result['version']) self.assertIn(platform.python_version(), result['version']) self.assertIn('python', result['path']) self.assertNotIn('platform.py', result['path']) def test_gstreamer_info(self): result = deps.gstreamer_info() self.assertEquals('GStreamer', result['name']) self.assertEquals( '.'.join(map(str, gst.get_gst_version())), result['version']) self.assertIn('gst', result['path']) self.assertNotIn('__init__.py', result['path']) self.assertIn('Python wrapper: gst-python', result['other']) self.assertIn( '.'.join(map(str, gst.get_pygst_version())), result['other']) self.assertIn('Relevant elements:', result['other']) @mock.patch('pkg_resources.get_distribution') def test_pkg_info(self, get_distribution_mock): dist_mopidy = mock.Mock() dist_mopidy.project_name = 'Mopidy' dist_mopidy.version = '0.13' dist_mopidy.location = '/tmp/example/mopidy' dist_mopidy.requires.return_value = ['Pykka'] dist_pykka = mock.Mock() dist_pykka.project_name = 'Pykka' dist_pykka.version = '1.1' dist_pykka.location = '/tmp/example/pykka' dist_pykka.requires.return_value = ['setuptools'] dist_setuptools = mock.Mock() dist_setuptools.project_name = 'setuptools' dist_setuptools.version = '0.6' dist_setuptools.location = '/tmp/example/setuptools' dist_setuptools.requires.return_value = [] get_distribution_mock.side_effect = [ dist_mopidy, dist_pykka, dist_setuptools] result = deps.pkg_info() self.assertEquals('Mopidy', result['name']) self.assertEquals('0.13', result['version']) self.assertIn('mopidy', result['path']) dep_info_pykka = result['dependencies'][0] self.assertEquals('Pykka', dep_info_pykka['name']) self.assertEquals('1.1', dep_info_pykka['version']) dep_info_setuptools = dep_info_pykka['dependencies'][0] self.assertEquals('setuptools', dep_info_setuptools['name']) self.assertEquals('0.6', dep_info_setuptools['version']) @mock.patch('pkg_resources.get_distribution') def test_pkg_info_for_missing_dist(self, get_distribution_mock): get_distribution_mock.side_effect = pkg_resources.DistributionNotFound result = deps.pkg_info() self.assertEquals('Mopidy', result['name']) self.assertNotIn('version', result) self.assertNotIn('path', result) @mock.patch('pkg_resources.get_distribution') def test_pkg_info_for_wrong_dist_version(self, get_distribution_mock): get_distribution_mock.side_effect = pkg_resources.VersionConflict result = deps.pkg_info() self.assertEquals('Mopidy', result['name']) self.assertNotIn('version', result) self.assertNotIn('path', result) mopidy-0.17.0/tests/utils/encoding_test.py000066400000000000000000000024451224420023200205750ustar00rootroot00000000000000from __future__ import unicode_literals import mock import unittest from mopidy.utils.encoding import locale_decode @mock.patch('mopidy.utils.encoding.locale.getpreferredencoding') class LocaleDecodeTest(unittest.TestCase): def test_can_decode_utf8_strings_with_french_content(self, mock): mock.return_value = 'UTF-8' result = locale_decode( b'[Errno 98] Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') self.assertEqual('[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e', result) def test_can_decode_an_ioerror_with_french_content(self, mock): mock.return_value = 'UTF-8' error = IOError(98, b'Adresse d\xc3\xa9j\xc3\xa0 utilis\xc3\xa9e') result = locale_decode(error) expected = '[Errno 98] Adresse d\xe9j\xe0 utilis\xe9e' self.assertEqual( expected, result, '%r decoded to %r does not match expected %r' % ( error, result, expected)) def test_does_not_use_locale_to_decode_unicode_strings(self, mock): mock.return_value = 'UTF-8' locale_decode('abc') self.assertFalse(mock.called) def test_does_not_use_locale_to_decode_ascii_bytestrings(self, mock): mock.return_value = 'UTF-8' locale_decode('abc') self.assertFalse(mock.called) mopidy-0.17.0/tests/utils/jsonrpc_test.py000066400000000000000000000533151224420023200204670ustar00rootroot00000000000000from __future__ import unicode_literals import json import mock import unittest import pykka from mopidy import core, models from mopidy.backends import dummy from mopidy.utils import jsonrpc class Calculator(object): def model(self): return 'TI83' def add(self, a, b): """Returns the sum of the given numbers""" return a + b def sub(self, a, b): return a - b def describe(self): return { 'add': 'Returns the sum of the terms', 'sub': 'Returns the diff of the terms', } def take_it_all(self, a, b, c=True, *args, **kwargs): pass def _secret(self): return 'Grand Unified Theory' def fail(self): raise ValueError('What did you expect?') class JsonRpcTestBase(unittest.TestCase): def setUp(self): self.backend = dummy.create_dummy_backend_proxy() self.core = core.Core.start(backends=[self.backend]).proxy() self.jrw = jsonrpc.JsonRpcWrapper( objects={ 'hello': lambda: 'Hello, world!', 'calc': Calculator(), 'core': self.core, 'core.playback': self.core.playback, 'core.tracklist': self.core.tracklist, 'get_uri_schemes': self.core.get_uri_schemes, }, encoders=[models.ModelJSONEncoder], decoders=[models.model_json_decoder]) def tearDown(self): pykka.ActorRegistry.stop_all() class JsonRpcSetupTest(JsonRpcTestBase): def test_empty_object_mounts_is_not_allowed(self): test = lambda: jsonrpc.JsonRpcWrapper(objects={'': Calculator()}) self.assertRaises(AttributeError, test) class JsonRpcSerializationTest(JsonRpcTestBase): def test_handle_json_converts_from_and_to_json(self): self.jrw.handle_data = mock.Mock() self.jrw.handle_data.return_value = {'foo': 'response'} request = '{"foo": "request"}' response = self.jrw.handle_json(request) self.jrw.handle_data.assert_called_once_with({'foo': 'request'}) self.assertEqual(response, '{"foo": "response"}') def test_handle_json_decodes_mopidy_models(self): self.jrw.handle_data = mock.Mock() self.jrw.handle_data.return_value = [] request = '{"foo": {"__model__": "Artist", "name": "bar"}}' self.jrw.handle_json(request) self.jrw.handle_data.assert_called_once_with( {'foo': models.Artist(name='bar')}) def test_handle_json_encodes_mopidy_models(self): self.jrw.handle_data = mock.Mock() self.jrw.handle_data.return_value = {'foo': models.Artist(name='bar')} request = '[]' response = self.jrw.handle_json(request) self.assertEqual( response, '{"foo": {"__model__": "Artist", "name": "bar"}}') def test_handle_json_returns_nothing_for_notices(self): request = '{"jsonrpc": "2.0", "method": "core.get_uri_schemes"}' response = self.jrw.handle_json(request) self.assertEqual(response, None) def test_invalid_json_command_causes_parse_error(self): request = ( '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]') response = self.jrw.handle_json(request) response = json.loads(response) self.assertEqual(response['jsonrpc'], '2.0') error = response['error'] self.assertEqual(error['code'], -32700) self.assertEqual(error['message'], 'Parse error') def test_invalid_json_batch_causes_parse_error(self): request = """[ {"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"}, {"jsonrpc": "2.0", "method" ]""" response = self.jrw.handle_json(request) response = json.loads(response) self.assertEqual(response['jsonrpc'], '2.0') error = response['error'] self.assertEqual(error['code'], -32700) self.assertEqual(error['message'], 'Parse error') class JsonRpcSingleCommandTest(JsonRpcTestBase): def test_call_method_on_root(self): request = { 'jsonrpc': '2.0', 'method': 'hello', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['jsonrpc'], '2.0') self.assertEqual(response['id'], 1) self.assertNotIn('error', response) self.assertEqual(response['result'], 'Hello, world!') def test_call_method_on_plain_object(self): request = { 'jsonrpc': '2.0', 'method': 'calc.model', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['jsonrpc'], '2.0') self.assertEqual(response['id'], 1) self.assertNotIn('error', response) self.assertEqual(response['result'], 'TI83') def test_call_method_which_returns_dict_from_plain_object(self): request = { 'jsonrpc': '2.0', 'method': 'calc.describe', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['jsonrpc'], '2.0') self.assertEqual(response['id'], 1) self.assertNotIn('error', response) self.assertIn('add', response['result']) self.assertIn('sub', response['result']) def test_call_method_on_actor_root(self): request = { 'jsonrpc': '2.0', 'method': 'core.get_uri_schemes', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['jsonrpc'], '2.0') self.assertEqual(response['id'], 1) self.assertNotIn('error', response) self.assertEqual(response['result'], ['dummy']) def test_call_method_on_actor_member(self): request = { 'jsonrpc': '2.0', 'method': 'core.playback.get_volume', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['result'], None) def test_call_method_which_is_a_directly_mounted_actor_member(self): # 'get_uri_schemes' isn't a regular callable, but a Pykka # CallableProxy. This test checks that CallableProxy objects are # threated by JsonRpcWrapper like any other callable. request = { 'jsonrpc': '2.0', 'method': 'get_uri_schemes', 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['jsonrpc'], '2.0') self.assertEqual(response['id'], 1) self.assertNotIn('error', response) self.assertEqual(response['result'], ['dummy']) def test_call_method_with_positional_params(self): request = { 'jsonrpc': '2.0', 'method': 'core.playback.set_volume', 'params': [37], 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['result'], None) self.assertEqual(self.core.playback.get_volume().get(), 37) def test_call_methods_with_named_params(self): request = { 'jsonrpc': '2.0', 'method': 'core.playback.set_volume', 'params': {'volume': 37}, 'id': 1, } response = self.jrw.handle_data(request) self.assertEqual(response['result'], None) self.assertEqual(self.core.playback.get_volume().get(), 37) class JsonRpcSingleNotificationTest(JsonRpcTestBase): def test_notification_does_not_return_a_result(self): request = { 'jsonrpc': '2.0', 'method': 'core.get_uri_schemes', } response = self.jrw.handle_data(request) self.assertIsNone(response) def test_notification_makes_an_observable_change(self): self.assertEqual(self.core.playback.get_volume().get(), None) request = { 'jsonrpc': '2.0', 'method': 'core.playback.set_volume', 'params': [37], } response = self.jrw.handle_data(request) self.assertIsNone(response) self.assertEqual(self.core.playback.get_volume().get(), 37) def test_notification_unknown_method_returns_nothing(self): request = { 'jsonrpc': '2.0', 'method': 'bogus', 'params': ['bogus'], } response = self.jrw.handle_data(request) self.assertIsNone(response) class JsonRpcBatchTest(JsonRpcTestBase): def test_batch_of_only_commands_returns_all(self): self.core.tracklist.set_random(True).get() request = [ {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat', 'id': 1}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3}, ] response = self.jrw.handle_data(request) self.assertEqual(len(response), 3) response = dict((row['id'], row) for row in response) self.assertEqual(response[1]['result'], False) self.assertEqual(response[2]['result'], True) self.assertEqual(response[3]['result'], False) def test_batch_of_commands_and_notifications_returns_some(self): self.core.tracklist.set_random(True).get() request = [ {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': 2}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single', 'id': 3}, ] response = self.jrw.handle_data(request) self.assertEqual(len(response), 2) response = dict((row['id'], row) for row in response) self.assertNotIn(1, response) self.assertEqual(response[2]['result'], True) self.assertEqual(response[3]['result'], False) def test_batch_of_only_notifications_returns_nothing(self): self.core.tracklist.set_random(True).get() request = [ {'jsonrpc': '2.0', 'method': 'core.tracklist.get_repeat'}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random'}, {'jsonrpc': '2.0', 'method': 'core.tracklist.get_single'}, ] response = self.jrw.handle_data(request) self.assertIsNone(response) class JsonRpcSingleCommandErrorTest(JsonRpcTestBase): def test_application_error_response(self): request = { 'jsonrpc': '2.0', 'method': 'calc.fail', 'params': [], 'id': 1, } response = self.jrw.handle_data(request) self.assertNotIn('result', response) error = response['error'] self.assertEqual(error['code'], 0) self.assertEqual(error['message'], 'Application error') data = error['data'] self.assertEqual(data['type'], 'ValueError') self.assertIn('What did you expect?', data['message']) self.assertIn('traceback', data) self.assertIn('Traceback (most recent call last):', data['traceback']) def test_missing_jsonrpc_member_causes_invalid_request_error(self): request = { 'method': 'core.get_uri_schemes', 'id': 1, } response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], '"jsonrpc" member must be included') def test_wrong_jsonrpc_version_causes_invalid_request_error(self): request = { 'jsonrpc': '3.0', 'method': 'core.get_uri_schemes', 'id': 1, } response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], '"jsonrpc" value must be "2.0"') def test_missing_method_member_causes_invalid_request_error(self): request = { 'jsonrpc': '2.0', 'id': 1, } response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], '"method" member must be included') def test_invalid_method_value_causes_invalid_request_error(self): request = { 'jsonrpc': '2.0', 'method': 1, 'id': 1, } response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], '"method" must be a string') def test_invalid_params_value_causes_invalid_request_error(self): request = { 'jsonrpc': '2.0', 'method': 'core.get_uri_schemes', 'params': 'foobar', 'id': 1, } response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual( error['data'], '"params", if given, must be an array or an object') def test_method_on_without_object_causes_unknown_method_error(self): request = { 'jsonrpc': '2.0', 'method': 'bogus', 'id': 1, } response = self.jrw.handle_data(request) error = response['error'] self.assertEqual(error['code'], -32601) self.assertEqual(error['message'], 'Method not found') self.assertEqual( error['data'], 'Could not find object mount in method name "bogus"') def test_method_on_unknown_object_causes_unknown_method_error(self): request = { 'jsonrpc': '2.0', 'method': 'bogus.bogus', 'id': 1, } response = self.jrw.handle_data(request) error = response['error'] self.assertEqual(error['code'], -32601) self.assertEqual(error['message'], 'Method not found') self.assertEqual(error['data'], 'No object found at "bogus"') def test_unknown_method_on_known_object_causes_unknown_method_error(self): request = { 'jsonrpc': '2.0', 'method': 'core.bogus', 'id': 1, } response = self.jrw.handle_data(request) error = response['error'] self.assertEqual(error['code'], -32601) self.assertEqual(error['message'], 'Method not found') self.assertEqual( error['data'], 'Object mounted at "core" has no member "bogus"') def test_private_method_causes_unknown_method_error(self): request = { 'jsonrpc': '2.0', 'method': 'core._secret', 'id': 1, } response = self.jrw.handle_data(request) error = response['error'] self.assertEqual(error['code'], -32601) self.assertEqual(error['message'], 'Method not found') self.assertEqual(error['data'], 'Private methods are not exported') def test_invalid_params_causes_invalid_params_error(self): request = { 'jsonrpc': '2.0', 'method': 'core.get_uri_schemes', 'params': ['bogus'], 'id': 1, } response = self.jrw.handle_data(request) error = response['error'] self.assertEqual(error['code'], -32602) self.assertEqual(error['message'], 'Invalid params') data = error['data'] self.assertEqual(data['type'], 'TypeError') self.assertEqual( data['message'], 'get_uri_schemes() takes exactly 1 argument (2 given)') self.assertIn('traceback', data) self.assertIn('Traceback (most recent call last):', data['traceback']) class JsonRpcBatchErrorTest(JsonRpcTestBase): def test_empty_batch_list_causes_invalid_request_error(self): request = [] response = self.jrw.handle_data(request) self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], 'Batch list cannot be empty') def test_batch_with_invalid_command_causes_invalid_request_error(self): request = [1] response = self.jrw.handle_data(request) self.assertEqual(len(response), 1) response = response[0] self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], 'Request must be an object') def test_batch_with_invalid_commands_causes_invalid_request_error(self): request = [1, 2, 3] response = self.jrw.handle_data(request) self.assertEqual(len(response), 3) response = response[2] self.assertIsNone(response['id']) error = response['error'] self.assertEqual(error['code'], -32600) self.assertEqual(error['message'], 'Invalid Request') self.assertEqual(error['data'], 'Request must be an object') def test_batch_of_both_successfull_and_failing_requests(self): request = [ # Call with positional params {'jsonrpc': '2.0', 'method': 'core.playback.set_volume', 'params': [47], 'id': '1'}, # Notification {'jsonrpc': '2.0', 'method': 'core.tracklist.set_consume', 'params': [True]}, # Call with positional params {'jsonrpc': '2.0', 'method': 'core.tracklist.set_repeat', 'params': [False], 'id': '2'}, # Invalid request {'foo': 'boo'}, # Unknown method {'jsonrpc': '2.0', 'method': 'foo.get', 'params': {'name': 'myself'}, 'id': '5'}, # Call without params {'jsonrpc': '2.0', 'method': 'core.tracklist.get_random', 'id': '9'}, ] response = self.jrw.handle_data(request) self.assertEqual(len(response), 5) response = dict((row['id'], row) for row in response) self.assertEqual(response['1']['result'], None) self.assertEqual(response['2']['result'], None) self.assertEqual(response[None]['error']['code'], -32600) self.assertEqual(response['5']['error']['code'], -32601) self.assertEqual(response['9']['result'], False) class JsonRpcInspectorTest(JsonRpcTestBase): def test_empty_object_mounts_is_not_allowed(self): test = lambda: jsonrpc.JsonRpcInspector(objects={'': Calculator}) self.assertRaises(AttributeError, test) def test_can_describe_method_on_root(self): inspector = jsonrpc.JsonRpcInspector({ 'hello': lambda: 'Hello, world!', }) methods = inspector.describe() self.assertIn('hello', methods) self.assertEqual(len(methods['hello']['params']), 0) def test_inspector_can_describe_an_object_with_methods(self): inspector = jsonrpc.JsonRpcInspector({ 'calc': Calculator, }) methods = inspector.describe() self.assertIn('calc.add', methods) self.assertEqual( methods['calc.add']['description'], 'Returns the sum of the given numbers') self.assertIn('calc.sub', methods) self.assertIn('calc.take_it_all', methods) self.assertNotIn('calc._secret', methods) self.assertNotIn('calc.__init__', methods) method = methods['calc.take_it_all'] self.assertIn('params', method) params = method['params'] self.assertEqual(params[0]['name'], 'a') self.assertNotIn('default', params[0]) self.assertEqual(params[1]['name'], 'b') self.assertNotIn('default', params[1]) self.assertEqual(params[2]['name'], 'c') self.assertEqual(params[2]['default'], True) self.assertEqual(params[3]['name'], 'args') self.assertNotIn('default', params[3]) self.assertEqual(params[3]['varargs'], True) self.assertEqual(params[4]['name'], 'kwargs') self.assertNotIn('default', params[4]) self.assertEqual(params[4]['kwargs'], True) def test_inspector_can_describe_a_bunch_of_large_classes(self): inspector = jsonrpc.JsonRpcInspector({ 'core.get_uri_schemes': core.Core.get_uri_schemes, 'core.library': core.LibraryController, 'core.playback': core.PlaybackController, 'core.playlists': core.PlaylistsController, 'core.tracklist': core.TracklistController, }) methods = inspector.describe() self.assertIn('core.get_uri_schemes', methods) self.assertEquals(len(methods['core.get_uri_schemes']['params']), 0) self.assertIn('core.library.lookup', methods.keys()) self.assertEquals( methods['core.library.lookup']['params'][0]['name'], 'uri') self.assertIn('core.playback.next', methods) self.assertEquals(len(methods['core.playback.next']['params']), 0) self.assertIn('core.playlists.get_playlists', methods) self.assertEquals( len(methods['core.playlists.get_playlists']['params']), 1) self.assertIn('core.tracklist.filter', methods.keys()) self.assertEquals( methods['core.tracklist.filter']['params'][0]['name'], 'criteria') self.assertEquals( methods['core.tracklist.filter']['params'][1]['name'], 'kwargs') self.assertEquals( methods['core.tracklist.filter']['params'][1]['kwargs'], True) mopidy-0.17.0/tests/utils/network/000077500000000000000000000000001224420023200170625ustar00rootroot00000000000000mopidy-0.17.0/tests/utils/network/__init__.py000066400000000000000000000000501224420023200211660ustar00rootroot00000000000000from __future__ import unicode_literals mopidy-0.17.0/tests/utils/network/connection_test.py000066400000000000000000000527011224420023200226370ustar00rootroot00000000000000from __future__ import unicode_literals import errno import logging from mock import patch, sentinel, Mock import socket import unittest import gobject import pykka from mopidy.utils import network from tests import any_int, any_unicode class ConnectionTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.Connection) def test_init_ensure_nonblocking_io(self): sock = Mock(spec=socket.SocketType) network.Connection.__init__( self.mock, Mock(), {}, sock, (sentinel.host, sentinel.port), sentinel.timeout) sock.setblocking.assert_called_once_with(False) def test_init_starts_actor(self): protocol = Mock(spec=network.LineProtocol) network.Connection.__init__( self.mock, protocol, {}, Mock(), (sentinel.host, sentinel.port), sentinel.timeout) protocol.start.assert_called_once_with(self.mock) def test_init_enables_recv_and_timeout(self): network.Connection.__init__( self.mock, Mock(), {}, Mock(), (sentinel.host, sentinel.port), sentinel.timeout) self.mock.enable_recv.assert_called_once_with() self.mock.enable_timeout.assert_called_once_with() def test_init_stores_values_in_attributes(self): addr = (sentinel.host, sentinel.port) protocol = Mock(spec=network.LineProtocol) protocol_kwargs = {} sock = Mock(spec=socket.SocketType) network.Connection.__init__( self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout) self.assertEqual(sock, self.mock.sock) self.assertEqual(protocol, self.mock.protocol) self.assertEqual(protocol_kwargs, self.mock.protocol_kwargs) self.assertEqual(sentinel.timeout, self.mock.timeout) self.assertEqual(sentinel.host, self.mock.host) self.assertEqual(sentinel.port, self.mock.port) def test_init_handles_ipv6_addr(self): addr = ( sentinel.host, sentinel.port, sentinel.flowinfo, sentinel.scopeid) protocol = Mock(spec=network.LineProtocol) protocol_kwargs = {} sock = Mock(spec=socket.SocketType) network.Connection.__init__( self.mock, protocol, protocol_kwargs, sock, addr, sentinel.timeout) self.assertEqual(sentinel.host, self.mock.host) self.assertEqual(sentinel.port, self.mock.port) def test_stop_disables_recv_send_and_timeout(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.mock.disable_timeout.assert_called_once_with() self.mock.disable_recv.assert_called_once_with() self.mock.disable_send.assert_called_once_with() def test_stop_closes_socket(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.mock.sock.close.assert_called_once_with() def test_stop_closes_socket_error(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.close.side_effect = socket.error network.Connection.stop(self.mock, sentinel.reason) self.mock.sock.close.assert_called_once_with() def test_stop_stops_actor(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.mock.actor_ref.stop.assert_called_once_with(block=False) def test_stop_handles_actor_already_being_stopped(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.actor_ref.stop.side_effect = pykka.ActorDeadError() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.mock.actor_ref.stop.assert_called_once_with(block=False) def test_stop_sets_stopping_to_true(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.assertEqual(True, self.mock.stopping) def test_stop_does_not_proceed_when_already_stopping(self): self.mock.stopping = True self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) self.assertEqual(0, self.mock.actor_ref.stop.call_count) self.assertEqual(0, self.mock.sock.close.call_count) @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) network.logger.log.assert_called_once_with( logging.DEBUG, sentinel.reason) @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_reason_with_level(self): self.mock.stopping = False self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop( self.mock, sentinel.reason, level=sentinel.level) network.logger.log.assert_called_once_with( sentinel.level, sentinel.reason) @patch.object(network.logger, 'log', new=Mock()) def test_stop_logs_that_it_is_calling_itself(self): self.mock.stopping = True self.mock.actor_ref = Mock() self.mock.sock = Mock(spec=socket.SocketType) network.Connection.stop(self.mock, sentinel.reason) network.logger.log(any_int, any_unicode) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_registers_with_gobject(self): self.mock.recv_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno gobject.io_add_watch.return_value = sentinel.tag network.Connection.enable_recv(self.mock) gobject.io_add_watch.assert_called_once_with( sentinel.fileno, gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP, self.mock.recv_callback) self.assertEqual(sentinel.tag, self.mock.recv_id) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_recv_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.recv_id = sentinel.tag network.Connection.enable_recv(self.mock) self.assertEqual(0, gobject.io_add_watch.call_count) def test_enable_recv_does_not_change_tag(self): self.mock.recv_id = sentinel.tag self.mock.sock = Mock(spec=socket.SocketType) network.Connection.enable_recv(self.mock) self.assertEqual(sentinel.tag, self.mock.recv_id) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_recv_deregisters(self): self.mock.recv_id = sentinel.tag network.Connection.disable_recv(self.mock) gobject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.recv_id) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_recv_already_deregistered(self): self.mock.recv_id = None network.Connection.disable_recv(self.mock) self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.recv_id) def test_enable_recv_on_closed_socket(self): self.mock.recv_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '') network.Connection.enable_recv(self.mock) self.mock.stop.assert_called_once_with(any_unicode) self.assertEqual(None, self.mock.recv_id) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_send_registers_with_gobject(self): self.mock.send_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.return_value = sentinel.fileno gobject.io_add_watch.return_value = sentinel.tag network.Connection.enable_send(self.mock) gobject.io_add_watch.assert_called_once_with( sentinel.fileno, gobject.IO_OUT | gobject.IO_ERR | gobject.IO_HUP, self.mock.send_callback) self.assertEqual(sentinel.tag, self.mock.send_id) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_enable_send_already_registered(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.send_id = sentinel.tag network.Connection.enable_send(self.mock) self.assertEqual(0, gobject.io_add_watch.call_count) def test_enable_send_does_not_change_tag(self): self.mock.send_id = sentinel.tag self.mock.sock = Mock(spec=socket.SocketType) network.Connection.enable_send(self.mock) self.assertEqual(sentinel.tag, self.mock.send_id) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_send_deregisters(self): self.mock.send_id = sentinel.tag network.Connection.disable_send(self.mock) gobject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.send_id) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_send_already_deregistered(self): self.mock.send_id = None network.Connection.disable_send(self.mock) self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.send_id) def test_enable_send_on_closed_socket(self): self.mock.send_id = None self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.fileno.side_effect = socket.error(errno.EBADF, '') network.Connection.enable_send(self.mock) self.assertEqual(None, self.mock.send_id) @patch.object(gobject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_clears_existing_timeouts(self): self.mock.timeout = 10 network.Connection.enable_timeout(self.mock) self.mock.disable_timeout.assert_called_once_with() @patch.object(gobject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_add_gobject_timeout(self): self.mock.timeout = 10 gobject.timeout_add_seconds.return_value = sentinel.tag network.Connection.enable_timeout(self.mock) gobject.timeout_add_seconds.assert_called_once_with( 10, self.mock.timeout_callback) self.assertEqual(sentinel.tag, self.mock.timeout_id) @patch.object(gobject, 'timeout_add_seconds', new=Mock()) def test_enable_timeout_does_not_add_timeout(self): self.mock.timeout = 0 network.Connection.enable_timeout(self.mock) self.assertEqual(0, gobject.timeout_add_seconds.call_count) self.mock.timeout = -1 network.Connection.enable_timeout(self.mock) self.assertEqual(0, gobject.timeout_add_seconds.call_count) self.mock.timeout = None network.Connection.enable_timeout(self.mock) self.assertEqual(0, gobject.timeout_add_seconds.call_count) def test_enable_timeout_does_not_call_disable_for_invalid_timeout(self): self.mock.timeout = 0 network.Connection.enable_timeout(self.mock) self.assertEqual(0, self.mock.disable_timeout.call_count) self.mock.timeout = -1 network.Connection.enable_timeout(self.mock) self.assertEqual(0, self.mock.disable_timeout.call_count) self.mock.timeout = None network.Connection.enable_timeout(self.mock) self.assertEqual(0, self.mock.disable_timeout.call_count) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_timeout_deregisters(self): self.mock.timeout_id = sentinel.tag network.Connection.disable_timeout(self.mock) gobject.source_remove.assert_called_once_with(sentinel.tag) self.assertEqual(None, self.mock.timeout_id) @patch.object(gobject, 'source_remove', new=Mock()) def test_disable_timeout_already_deregistered(self): self.mock.timeout_id = None network.Connection.disable_timeout(self.mock) self.assertEqual(0, gobject.source_remove.call_count) self.assertEqual(None, self.mock.timeout_id) def test_queue_send_acquires_and_releases_lock(self): self.mock.send_lock = Mock() self.mock.send_buffer = '' network.Connection.queue_send(self.mock, 'data') self.mock.send_lock.acquire.assert_called_once_with(True) self.mock.send_lock.release.assert_called_once_with() def test_queue_send_calls_send(self): self.mock.send_buffer = '' self.mock.send_lock = Mock() self.mock.send.return_value = '' network.Connection.queue_send(self.mock, 'data') self.mock.send.assert_called_once_with('data') self.assertEqual(0, self.mock.enable_send.call_count) self.assertEqual('', self.mock.send_buffer) def test_queue_send_calls_enable_send_for_partial_send(self): self.mock.send_buffer = '' self.mock.send_lock = Mock() self.mock.send.return_value = 'ta' network.Connection.queue_send(self.mock, 'data') self.mock.send.assert_called_once_with('data') self.mock.enable_send.assert_called_once_with() self.assertEqual('ta', self.mock.send_buffer) def test_queue_send_calls_send_with_existing_buffer(self): self.mock.send_buffer = 'foo' self.mock.send_lock = Mock() self.mock.send.return_value = '' network.Connection.queue_send(self.mock, 'bar') self.mock.send.assert_called_once_with('foobar') self.assertEqual(0, self.mock.enable_send.call_count) self.assertEqual('', self.mock.send_buffer) def test_recv_callback_respects_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_respects_io_hup_and_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_sends_data_to_actor(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = 'data' self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.actor_ref.tell.assert_called_once_with( {'received': 'data'}) def test_recv_callback_handles_dead_actors(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = 'data' self.mock.actor_ref = Mock() self.mock.actor_ref.tell.side_effect = pykka.ActorDeadError() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_gets_no_data(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.return_value = '' self.mock.actor_ref = Mock() self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_recv_callback_recoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.recv.side_effect = socket.error(error, '') self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.assertEqual(0, self.mock.stop.call_count) def test_recv_callback_unrecoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.recv.side_effect = socket.error self.assertTrue(network.Connection.recv_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 1 self.mock.send_lock = Mock() self.mock.actor_ref = Mock() self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 1 self.mock.send_lock = Mock() self.mock.actor_ref = Mock() self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_respects_io_hup_and_io_err(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 1 self.mock.send_lock = Mock() self.mock.actor_ref = Mock() self.mock.send_buffer = '' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN | gobject.IO_HUP | gobject.IO_ERR)) self.mock.stop.assert_called_once_with(any_unicode) def test_send_callback_acquires_and_releases_lock(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = '' self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.mock.send_lock.release.assert_called_once_with() def test_send_callback_fails_to_acquire_lock(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = False self.mock.send_buffer = '' self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 0 self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.send_lock.acquire.assert_called_once_with(False) self.assertEqual(0, self.mock.sock.send.call_count) def test_send_callback_sends_all_data(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' self.mock.send.return_value = '' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.disable_send.assert_called_once_with() self.mock.send.assert_called_once_with('data') self.assertEqual('', self.mock.send_buffer) def test_send_callback_sends_partial_data(self): self.mock.send_lock = Mock() self.mock.send_lock.acquire.return_value = True self.mock.send_buffer = 'data' self.mock.send.return_value = 'ta' self.assertTrue(network.Connection.send_callback( self.mock, sentinel.fd, gobject.IO_IN)) self.mock.send.assert_called_once_with('data') self.assertEqual('ta', self.mock.send_buffer) def test_send_recoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) for error in (errno.EWOULDBLOCK, errno.EINTR): self.mock.sock.send.side_effect = socket.error(error, '') network.Connection.send(self.mock, 'data') self.assertEqual(0, self.mock.stop.call_count) def test_send_calls_socket_send(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 4 self.assertEqual('', network.Connection.send(self.mock, 'data')) self.mock.sock.send.assert_called_once_with('data') def test_send_calls_socket_send_partial_send(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.return_value = 2 self.assertEqual('ta', network.Connection.send(self.mock, 'data')) self.mock.sock.send.assert_called_once_with('data') def test_send_unrecoverable_error(self): self.mock.sock = Mock(spec=socket.SocketType) self.mock.sock.send.side_effect = socket.error self.assertEqual('', network.Connection.send(self.mock, 'data')) self.mock.stop.assert_called_once_with(any_unicode) def test_timeout_callback(self): self.mock.timeout = 10 self.assertFalse(network.Connection.timeout_callback(self.mock)) self.mock.stop.assert_called_once_with(any_unicode) mopidy-0.17.0/tests/utils/network/lineprotocol_test.py000066400000000000000000000265331224420023200232150ustar00rootroot00000000000000# encoding: utf-8 from __future__ import unicode_literals from mock import sentinel, Mock import re import unittest from mopidy.utils import network class LineProtocolTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.LineProtocol) self.mock.terminator = network.LineProtocol.terminator self.mock.encoding = network.LineProtocol.encoding self.mock.delimiter = network.LineProtocol.delimiter self.mock.prevent_timeout = False def test_init_stores_values_in_attributes(self): delimiter = re.compile(network.LineProtocol.terminator) network.LineProtocol.__init__(self.mock, sentinel.connection) self.assertEqual(sentinel.connection, self.mock.connection) self.assertEqual('', self.mock.recv_buffer) self.assertEqual(delimiter, self.mock.delimiter) self.assertFalse(self.mock.prevent_timeout) def test_init_compiles_delimiter(self): self.mock.delimiter = '\r?\n' delimiter = re.compile('\r?\n') network.LineProtocol.__init__(self.mock, sentinel.connection) self.assertEqual(delimiter, self.mock.delimiter) def test_on_receive_no_new_lines_adds_to_recv_buffer(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [] network.LineProtocol.on_receive(self.mock, {'received': 'data'}) self.assertEqual('data', self.mock.recv_buffer) self.mock.parse_lines.assert_called_once_with() self.assertEqual(0, self.mock.on_line_received.call_count) def test_on_receive_toggles_timeout(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [] network.LineProtocol.on_receive(self.mock, {'received': 'data'}) self.mock.connection.disable_timeout.assert_called_once_with() self.mock.connection.enable_timeout.assert_called_once_with() def test_on_receive_toggles_unless_prevent_timeout_is_set(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [] self.mock.prevent_timeout = True network.LineProtocol.on_receive(self.mock, {'received': 'data'}) self.mock.connection.disable_timeout.assert_called_once_with() self.assertEqual(0, self.mock.connection.enable_timeout.call_count) def test_on_receive_no_new_lines_calls_parse_lines(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [] network.LineProtocol.on_receive(self.mock, {'received': 'data'}) self.mock.parse_lines.assert_called_once_with() self.assertEqual(0, self.mock.on_line_received.call_count) def test_on_receive_with_new_line_calls_decode(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [sentinel.line] network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) self.mock.parse_lines.assert_called_once_with() self.mock.decode.assert_called_once_with(sentinel.line) def test_on_receive_with_new_line_calls_on_recieve(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [sentinel.line] self.mock.decode.return_value = sentinel.decoded network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) self.mock.on_line_received.assert_called_once_with(sentinel.decoded) def test_on_receive_with_new_line_with_failed_decode(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = [sentinel.line] self.mock.decode.return_value = None network.LineProtocol.on_receive(self.mock, {'received': 'data\n'}) self.assertEqual(0, self.mock.on_line_received.call_count) def test_on_receive_with_new_lines_calls_on_recieve(self): self.mock.connection = Mock(spec=network.Connection) self.mock.recv_buffer = '' self.mock.parse_lines.return_value = ['line1', 'line2'] self.mock.decode.return_value = sentinel.decoded network.LineProtocol.on_receive( self.mock, {'received': 'line1\nline2\n'}) self.assertEqual(2, self.mock.on_line_received.call_count) def test_parse_lines_emtpy_buffer(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = '' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) def test_parse_lines_no_terminator(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'data' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) def test_parse_lines_termintor(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'data\n' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('data', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_termintor_with_carriage_return(self): self.mock.delimiter = re.compile(r'\r?\n') self.mock.recv_buffer = 'data\r\n' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('data', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_no_data_before_terminator(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = '\n' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_extra_data_after_terminator(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'data1\ndata2' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('data1', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('data2', self.mock.recv_buffer) def test_parse_lines_unicode(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'æøå\n'.encode('utf-8') lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('æøå'.encode('utf-8'), lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('', self.mock.recv_buffer) def test_parse_lines_multiple_lines(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'abc\ndef\nghi\njkl' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('abc', lines.next()) self.assertEqual('def', lines.next()) self.assertEqual('ghi', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('jkl', self.mock.recv_buffer) def test_parse_lines_multiple_calls(self): self.mock.delimiter = re.compile(r'\n') self.mock.recv_buffer = 'data1' lines = network.LineProtocol.parse_lines(self.mock) self.assertRaises(StopIteration, lines.next) self.assertEqual('data1', self.mock.recv_buffer) self.mock.recv_buffer += '\ndata2' lines = network.LineProtocol.parse_lines(self.mock) self.assertEqual('data1', lines.next()) self.assertRaises(StopIteration, lines.next) self.assertEqual('data2', self.mock.recv_buffer) def test_send_lines_called_with_no_lines(self): self.mock.connection = Mock(spec=network.Connection) network.LineProtocol.send_lines(self.mock, []) self.assertEqual(0, self.mock.encode.call_count) self.assertEqual(0, self.mock.connection.queue_send.call_count) def test_send_lines_calls_join_lines(self): self.mock.connection = Mock(spec=network.Connection) self.mock.join_lines.return_value = 'lines' network.LineProtocol.send_lines(self.mock, sentinel.lines) self.mock.join_lines.assert_called_once_with(sentinel.lines) def test_send_line_encodes_joined_lines_with_final_terminator(self): self.mock.connection = Mock(spec=network.Connection) self.mock.join_lines.return_value = 'lines\n' network.LineProtocol.send_lines(self.mock, sentinel.lines) self.mock.encode.assert_called_once_with('lines\n') def test_send_lines_sends_encoded_string(self): self.mock.connection = Mock(spec=network.Connection) self.mock.join_lines.return_value = 'lines' self.mock.encode.return_value = sentinel.data network.LineProtocol.send_lines(self.mock, sentinel.lines) self.mock.connection.queue_send.assert_called_once_with(sentinel.data) def test_join_lines_returns_empty_string_for_no_lines(self): self.assertEqual('', network.LineProtocol.join_lines(self.mock, [])) def test_join_lines_returns_joined_lines(self): self.assertEqual('1\n2\n', network.LineProtocol.join_lines( self.mock, ['1', '2'])) def test_decode_calls_decode_on_string(self): string = Mock() network.LineProtocol.decode(self.mock, string) string.decode.assert_called_once_with(self.mock.encoding) def test_decode_plain_ascii(self): result = network.LineProtocol.decode(self.mock, 'abc') self.assertEqual('abc', result) self.assertEqual(unicode, type(result)) def test_decode_utf8(self): result = network.LineProtocol.decode( self.mock, 'æøå'.encode('utf-8')) self.assertEqual('æøå', result) self.assertEqual(unicode, type(result)) def test_decode_invalid_data(self): string = Mock() string.decode.side_effect = UnicodeError network.LineProtocol.decode(self.mock, string) self.mock.stop.assert_called_once_with() def test_encode_calls_encode_on_string(self): string = Mock() network.LineProtocol.encode(self.mock, string) string.encode.assert_called_once_with(self.mock.encoding) def test_encode_plain_ascii(self): result = network.LineProtocol.encode(self.mock, 'abc') self.assertEqual('abc', result) self.assertEqual(str, type(result)) def test_encode_utf8(self): result = network.LineProtocol.encode(self.mock, 'æøå') self.assertEqual('æøå'.encode('utf-8'), result) self.assertEqual(str, type(result)) def test_encode_invalid_data(self): string = Mock() string.encode.side_effect = UnicodeError network.LineProtocol.encode(self.mock, string) self.mock.stop.assert_called_once_with() def test_host_property(self): mock = Mock(spec=network.Connection) mock.host = sentinel.host lineprotocol = network.LineProtocol(mock) self.assertEqual(sentinel.host, lineprotocol.host) def test_port_property(self): mock = Mock(spec=network.Connection) mock.port = sentinel.port lineprotocol = network.LineProtocol(mock) self.assertEqual(sentinel.port, lineprotocol.port) mopidy-0.17.0/tests/utils/network/server_test.py000066400000000000000000000175061224420023200220120ustar00rootroot00000000000000from __future__ import unicode_literals import errno from mock import patch, sentinel, Mock import socket import unittest import gobject from mopidy.utils import network from tests import any_int class ServerTest(unittest.TestCase): def setUp(self): self.mock = Mock(spec=network.Server) def test_init_calls_create_server_socket(self): network.Server.__init__( self.mock, sentinel.host, sentinel.port, sentinel.protocol) self.mock.create_server_socket.assert_called_once_with( sentinel.host, sentinel.port) def test_init_calls_register_server(self): sock = Mock(spec=socket.SocketType) sock.fileno.return_value = sentinel.fileno self.mock.create_server_socket.return_value = sock network.Server.__init__( self.mock, sentinel.host, sentinel.port, sentinel.protocol) self.mock.register_server_socket.assert_called_once_with( sentinel.fileno) def test_init_fails_on_fileno_call(self): sock = Mock(spec=socket.SocketType) sock.fileno.side_effect = socket.error self.mock.create_server_socket.return_value = sock self.assertRaises( socket.error, network.Server.__init__, self.mock, sentinel.host, sentinel.port, sentinel.protocol) def test_init_stores_values_in_attributes(self): # This need to be a mock and no a sentinel as fileno() is called on it sock = Mock(spec=socket.SocketType) self.mock.create_server_socket.return_value = sock network.Server.__init__( self.mock, sentinel.host, sentinel.port, sentinel.protocol, max_connections=sentinel.max_connections, timeout=sentinel.timeout) self.assertEqual(sentinel.protocol, self.mock.protocol) self.assertEqual(sentinel.max_connections, self.mock.max_connections) self.assertEqual(sentinel.timeout, self.mock.timeout) self.assertEqual(sock, self.mock.server_socket) @patch.object(network, 'create_socket', spec=socket.SocketType) def test_create_server_socket_sets_up_listener(self, create_socket): sock = create_socket.return_value network.Server.create_server_socket( self.mock, sentinel.host, sentinel.port) sock.setblocking.assert_called_once_with(False) sock.bind.assert_called_once_with((sentinel.host, sentinel.port)) sock.listen.assert_called_once_with(any_int) @patch.object(network, 'create_socket', new=Mock()) def test_create_server_socket_fails(self): network.create_socket.side_effect = socket.error self.assertRaises( socket.error, network.Server.create_server_socket, self.mock, sentinel.host, sentinel.port) @patch.object(network, 'create_socket', new=Mock()) def test_create_server_bind_fails(self): sock = network.create_socket.return_value sock.bind.side_effect = socket.error self.assertRaises( socket.error, network.Server.create_server_socket, self.mock, sentinel.host, sentinel.port) @patch.object(network, 'create_socket', new=Mock()) def test_create_server_listen_fails(self): sock = network.create_socket.return_value sock.listen.side_effect = socket.error self.assertRaises( socket.error, network.Server.create_server_socket, self.mock, sentinel.host, sentinel.port) @patch.object(gobject, 'io_add_watch', new=Mock()) def test_register_server_socket_sets_up_io_watch(self): network.Server.register_server_socket(self.mock, sentinel.fileno) gobject.io_add_watch.assert_called_once_with( sentinel.fileno, gobject.IO_IN, self.mock.handle_connection) def test_handle_connection(self): self.mock.accept_connection.return_value = ( sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = False self.assertTrue(network.Server.handle_connection( self.mock, sentinel.fileno, gobject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.init_connection.assert_called_once_with( sentinel.sock, sentinel.addr) self.assertEqual(0, self.mock.reject_connection.call_count) def test_handle_connection_exceeded_connections(self): self.mock.accept_connection.return_value = ( sentinel.sock, sentinel.addr) self.mock.maximum_connections_exceeded.return_value = True self.assertTrue(network.Server.handle_connection( self.mock, sentinel.fileno, gobject.IO_IN)) self.mock.accept_connection.assert_called_once_with() self.mock.maximum_connections_exceeded.assert_called_once_with() self.mock.reject_connection.assert_called_once_with( sentinel.sock, sentinel.addr) self.assertEqual(0, self.mock.init_connection.call_count) def test_accept_connection(self): sock = Mock(spec=socket.SocketType) sock.accept.return_value = (sentinel.sock, sentinel.addr) self.mock.server_socket = sock sock, addr = network.Server.accept_connection(self.mock) self.assertEqual(sentinel.sock, sock) self.assertEqual(sentinel.addr, addr) def test_accept_connection_recoverable_error(self): sock = Mock(spec=socket.SocketType) self.mock.server_socket = sock for error in (errno.EAGAIN, errno.EINTR): sock.accept.side_effect = socket.error(error, '') self.assertRaises( network.ShouldRetrySocketCall, network.Server.accept_connection, self.mock) # FIXME decide if this should be allowed to propegate def test_accept_connection_unrecoverable_error(self): sock = Mock(spec=socket.SocketType) self.mock.server_socket = sock sock.accept.side_effect = socket.error self.assertRaises( socket.error, network.Server.accept_connection, self.mock) def test_maximum_connections_exceeded(self): self.mock.max_connections = 10 self.mock.number_of_connections.return_value = 11 self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) self.mock.number_of_connections.return_value = 10 self.assertTrue(network.Server.maximum_connections_exceeded(self.mock)) self.mock.number_of_connections.return_value = 9 self.assertFalse( network.Server.maximum_connections_exceeded(self.mock)) @patch('pykka.registry.ActorRegistry.get_by_class') def test_number_of_connections(self, get_by_class): self.mock.protocol = sentinel.protocol get_by_class.return_value = [1, 2, 3] self.assertEqual(3, network.Server.number_of_connections(self.mock)) get_by_class.return_value = [] self.assertEqual(0, network.Server.number_of_connections(self.mock)) @patch.object(network, 'Connection', new=Mock()) def test_init_connection(self): self.mock.protocol = sentinel.protocol self.mock.protocol_kwargs = {} self.mock.timeout = sentinel.timeout network.Server.init_connection(self.mock, sentinel.sock, sentinel.addr) network.Connection.assert_called_once_with( sentinel.protocol, {}, sentinel.sock, sentinel.addr, sentinel.timeout) def test_reject_connection(self): sock = Mock(spec=socket.SocketType) network.Server.reject_connection( self.mock, sock, (sentinel.host, sentinel.port)) sock.close.assert_called_once_with() def test_reject_connection_error(self): sock = Mock(spec=socket.SocketType) sock.close.side_effect = socket.error network.Server.reject_connection( self.mock, sock, (sentinel.host, sentinel.port)) sock.close.assert_called_once_with() mopidy-0.17.0/tests/utils/network/utils_test.py000066400000000000000000000037011224420023200216340ustar00rootroot00000000000000from __future__ import unicode_literals from mock import patch, Mock import socket import unittest from mopidy.utils import network class FormatHostnameTest(unittest.TestCase): @patch('mopidy.utils.network.has_ipv6', True) def test_format_hostname_prefixes_ipv4_addresses_when_ipv6_available(self): network.has_ipv6 = True self.assertEqual(network.format_hostname('0.0.0.0'), '::ffff:0.0.0.0') self.assertEqual(network.format_hostname('1.0.0.1'), '::ffff:1.0.0.1') @patch('mopidy.utils.network.has_ipv6', False) def test_format_hostname_does_nothing_when_only_ipv4_available(self): network.has_ipv6 = False self.assertEqual(network.format_hostname('0.0.0.0'), '0.0.0.0') class TryIPv6SocketTest(unittest.TestCase): @patch('socket.has_ipv6', False) def test_system_that_claims_no_ipv6_support(self): self.assertFalse(network.try_ipv6_socket()) @patch('socket.has_ipv6', True) @patch('socket.socket') def test_system_with_broken_ipv6(self, socket_mock): socket_mock.side_effect = IOError() self.assertFalse(network.try_ipv6_socket()) @patch('socket.has_ipv6', True) @patch('socket.socket') def test_with_working_ipv6(self, socket_mock): socket_mock.return_value = Mock() self.assertTrue(network.try_ipv6_socket()) class CreateSocketTest(unittest.TestCase): @patch('mopidy.utils.network.has_ipv6', False) @patch('socket.socket') def test_ipv4_socket(self, socket_mock): network.create_socket() self.assertEqual( socket_mock.call_args[0], (socket.AF_INET, socket.SOCK_STREAM)) @patch('mopidy.utils.network.has_ipv6', True) @patch('socket.socket') def test_ipv6_socket(self, socket_mock): network.create_socket() self.assertEqual( socket_mock.call_args[0], (socket.AF_INET6, socket.SOCK_STREAM)) @unittest.SkipTest def test_ipv6_only_is_set(self): pass mopidy-0.17.0/tests/utils/path_test.py000066400000000000000000000224251224420023200177430ustar00rootroot00000000000000# encoding: utf-8 from __future__ import unicode_literals import os import shutil import tempfile import unittest import glib from mopidy.utils import path from tests import path_to_data_dir class GetOrCreateDirTest(unittest.TestCase): def setUp(self): self.parent = tempfile.mkdtemp() def tearDown(self): if os.path.isdir(self.parent): shutil.rmtree(self.parent) def test_creating_dir(self): dir_path = os.path.join(self.parent, b'test') self.assert_(not os.path.exists(dir_path)) created = path.get_or_create_dir(dir_path) self.assert_(os.path.exists(dir_path)) self.assert_(os.path.isdir(dir_path)) self.assertEqual(created, dir_path) def test_creating_nested_dirs(self): level2_dir = os.path.join(self.parent, b'test') level3_dir = os.path.join(self.parent, b'test', b'test') self.assert_(not os.path.exists(level2_dir)) self.assert_(not os.path.exists(level3_dir)) created = path.get_or_create_dir(level3_dir) self.assert_(os.path.exists(level2_dir)) self.assert_(os.path.isdir(level2_dir)) self.assert_(os.path.exists(level3_dir)) self.assert_(os.path.isdir(level3_dir)) self.assertEqual(created, level3_dir) def test_creating_existing_dir(self): created = path.get_or_create_dir(self.parent) self.assert_(os.path.exists(self.parent)) self.assert_(os.path.isdir(self.parent)) self.assertEqual(created, self.parent) def test_create_dir_with_name_of_existing_file_throws_oserror(self): conflicting_file = os.path.join(self.parent, b'test') open(conflicting_file, 'w').close() dir_path = os.path.join(self.parent, b'test') self.assertRaises(OSError, path.get_or_create_dir, dir_path) def test_create_dir_with_unicode(self): with self.assertRaises(ValueError): dir_path = unicode(os.path.join(self.parent, b'test')) path.get_or_create_dir(dir_path) def test_create_dir_with_none(self): with self.assertRaises(ValueError): path.get_or_create_dir(None) class GetOrCreateFileTest(unittest.TestCase): def setUp(self): self.parent = tempfile.mkdtemp() def tearDown(self): if os.path.isdir(self.parent): shutil.rmtree(self.parent) def test_creating_file(self): file_path = os.path.join(self.parent, b'test') self.assert_(not os.path.exists(file_path)) created = path.get_or_create_file(file_path) self.assert_(os.path.exists(file_path)) self.assert_(os.path.isfile(file_path)) self.assertEqual(created, file_path) def test_creating_nested_file(self): level2_dir = os.path.join(self.parent, b'test') file_path = os.path.join(self.parent, b'test', b'test') self.assert_(not os.path.exists(level2_dir)) self.assert_(not os.path.exists(file_path)) created = path.get_or_create_file(file_path) self.assert_(os.path.exists(level2_dir)) self.assert_(os.path.isdir(level2_dir)) self.assert_(os.path.exists(file_path)) self.assert_(os.path.isfile(file_path)) self.assertEqual(created, file_path) def test_creating_existing_file(self): file_path = os.path.join(self.parent, b'test') path.get_or_create_file(file_path) created = path.get_or_create_file(file_path) self.assert_(os.path.exists(file_path)) self.assert_(os.path.isfile(file_path)) self.assertEqual(created, file_path) def test_create_file_with_name_of_existing_dir_throws_ioerror(self): conflicting_dir = os.path.join(self.parent) with self.assertRaises(IOError): path.get_or_create_file(conflicting_dir) def test_create_dir_with_unicode(self): with self.assertRaises(ValueError): file_path = unicode(os.path.join(self.parent, b'test')) path.get_or_create_file(file_path) def test_create_file_with_none(self): with self.assertRaises(ValueError): path.get_or_create_file(None) def test_create_dir_without_mkdir(self): file_path = os.path.join(self.parent, b'foo', b'bar') with self.assertRaises(IOError): path.get_or_create_file(file_path, mkdir=False) def test_create_dir_with_default_content(self): file_path = os.path.join(self.parent, b'test') created = path.get_or_create_file(file_path, content=b'foobar') with open(created) as fh: self.assertEqual(fh.read(), b'foobar') class PathToFileURITest(unittest.TestCase): def test_simple_path(self): result = path.path_to_uri('/etc/fstab') self.assertEqual(result, 'file:///etc/fstab') def test_space_in_path(self): result = path.path_to_uri('/tmp/test this') self.assertEqual(result, 'file:///tmp/test%20this') def test_unicode_in_path(self): result = path.path_to_uri('/tmp/æøå') self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') def test_utf8_in_path(self): result = path.path_to_uri('/tmp/æøå'.encode('utf-8')) self.assertEqual(result, 'file:///tmp/%C3%A6%C3%B8%C3%A5') def test_latin1_in_path(self): result = path.path_to_uri('/tmp/æøå'.encode('latin-1')) self.assertEqual(result, 'file:///tmp/%E6%F8%E5') class UriToPathTest(unittest.TestCase): def test_simple_uri(self): result = path.uri_to_path('file:///etc/fstab') self.assertEqual(result, '/etc/fstab'.encode('utf-8')) def test_space_in_uri(self): result = path.uri_to_path('file:///tmp/test%20this') self.assertEqual(result, '/tmp/test this'.encode('utf-8')) def test_unicode_in_uri(self): result = path.uri_to_path('file:///tmp/%C3%A6%C3%B8%C3%A5') self.assertEqual(result, '/tmp/æøå'.encode('utf-8')) def test_latin1_in_uri(self): result = path.uri_to_path('file:///tmp/%E6%F8%E5') self.assertEqual(result, '/tmp/æøå'.encode('latin-1')) class SplitPathTest(unittest.TestCase): def test_empty_path(self): self.assertEqual([], path.split_path('')) def test_single_dir(self): self.assertEqual(['foo'], path.split_path('foo')) def test_dirs(self): self.assertEqual(['foo', 'bar', 'baz'], path.split_path('foo/bar/baz')) def test_initial_slash_is_ignored(self): self.assertEqual( ['foo', 'bar', 'baz'], path.split_path('/foo/bar/baz')) def test_only_slash(self): self.assertEqual([], path.split_path('/')) class ExpandPathTest(unittest.TestCase): # TODO: test via mocks? def test_empty_path(self): self.assertEqual(os.path.abspath(b'.'), path.expand_path(b'')) def test_absolute_path(self): self.assertEqual(b'/tmp/foo', path.expand_path(b'/tmp/foo')) def test_home_dir_expansion(self): self.assertEqual( os.path.expanduser(b'~/foo'), path.expand_path(b'~/foo')) def test_abspath(self): self.assertEqual(os.path.abspath(b'./foo'), path.expand_path(b'./foo')) def test_xdg_subsititution(self): self.assertEqual( glib.get_user_data_dir() + b'/foo', path.expand_path(b'$XDG_DATA_DIR/foo')) def test_xdg_subsititution_unknown(self): self.assertIsNone( path.expand_path(b'/tmp/$XDG_INVALID_DIR/foo')) class FindFilesTest(unittest.TestCase): def find(self, value): return list(path.find_files(path_to_data_dir(value))) def test_basic_dir(self): self.assert_(self.find('')) def test_nonexistant_dir(self): self.assertEqual(self.find('does-not-exist'), []) def test_file(self): files = self.find('blank.mp3') self.assertEqual(len(files), 1) self.assertEqual(files[0], path_to_data_dir('blank.mp3')) def test_names_are_bytestrings(self): is_bytes = lambda f: isinstance(f, bytes) for name in self.find(''): self.assert_( is_bytes(name), '%s is not bytes object' % repr(name)) def test_ignores_hidden_dirs(self): self.assertEqual(self.find('.hidden'), []) def test_ignores_hidden_files(self): self.assertEqual(self.find('.blank.mp3'), []) class FindUrisTest(unittest.TestCase): def find(self, value): return list(path.find_uris(path_to_data_dir(value))) def test_basic_dir(self): self.assert_(self.find('')) def test_nonexistant_dir(self): self.assertEqual(self.find('does-not-exist'), []) def test_file(self): uris = self.find('blank.mp3') expected = path.path_to_uri(path_to_data_dir('blank.mp3')) self.assertEqual(len(uris), 1) self.assertEqual(uris[0], expected) def test_ignores_hidden_dirs(self): self.assertEqual(self.find('.hidden'), []) def test_ignores_hidden_files(self): self.assertEqual(self.find('.blank.mp3'), []) # TODO: kill this in favour of just os.path.getmtime + mocks class MtimeTest(unittest.TestCase): def tearDown(self): path.mtime.undo_fake() def test_mtime_of_current_dir(self): mtime_dir = int(os.stat('.').st_mtime) self.assertEqual(mtime_dir, path.mtime('.')) def test_fake_time_is_returned(self): path.mtime.set_fake_time(123456) self.assertEqual(path.mtime('.'), 123456) mopidy-0.17.0/tests/version_test.py000066400000000000000000000036671224420023200173430ustar00rootroot00000000000000from __future__ import unicode_literals from distutils.version import StrictVersion as SV import unittest from mopidy import __version__ class VersionTest(unittest.TestCase): def test_current_version_is_parsable_as_a_strict_version_number(self): SV(__version__) def test_versions_can_be_strictly_ordered(self): self.assertLess(SV('0.1.0a0'), SV('0.1.0a1')) self.assertLess(SV('0.1.0a1'), SV('0.1.0a2')) self.assertLess(SV('0.1.0a2'), SV('0.1.0a3')) self.assertLess(SV('0.1.0a3'), SV('0.1.0')) self.assertLess(SV('0.1.0'), SV('0.2.0')) self.assertLess(SV('0.1.0'), SV('1.0.0')) self.assertLess(SV('0.2.0'), SV('0.3.0')) self.assertLess(SV('0.3.0'), SV('0.3.1')) self.assertLess(SV('0.3.1'), SV('0.4.0')) self.assertLess(SV('0.4.0'), SV('0.4.1')) self.assertLess(SV('0.4.1'), SV('0.5.0')) self.assertLess(SV('0.5.0'), SV('0.6.0')) self.assertLess(SV('0.6.0'), SV('0.6.1')) self.assertLess(SV('0.6.1'), SV('0.7.0')) self.assertLess(SV('0.7.0'), SV('0.7.1')) self.assertLess(SV('0.7.1'), SV('0.7.2')) self.assertLess(SV('0.7.2'), SV('0.7.3')) self.assertLess(SV('0.7.3'), SV('0.8.0')) self.assertLess(SV('0.8.0'), SV('0.8.1')) self.assertLess(SV('0.8.1'), SV('0.9.0')) self.assertLess(SV('0.9.0'), SV('0.10.0')) self.assertLess(SV('0.10.0'), SV('0.11.0')) self.assertLess(SV('0.11.0'), SV('0.11.1')) self.assertLess(SV('0.11.1'), SV('0.12.0')) self.assertLess(SV('0.12.0'), SV('0.13.0')) self.assertLess(SV('0.13.0'), SV('0.14.0')) self.assertLess(SV('0.14.0'), SV('0.14.1')) self.assertLess(SV('0.14.1'), SV('0.14.2')) self.assertLess(SV('0.14.2'), SV('0.15.0')) self.assertLess(SV('0.15.0'), SV('0.16.0')) self.assertLess(SV('0.16.0'), SV(__version__)) self.assertLess(SV(__version__), SV('0.17.1')) mopidy-0.17.0/tools/000077500000000000000000000000001224420023200142275ustar00rootroot00000000000000mopidy-0.17.0/tools/debug-proxy.py000077500000000000000000000133541224420023200170570ustar00rootroot00000000000000#! /usr/bin/env python from __future__ import unicode_literals import argparse import difflib import sys from gevent import select, server, socket COLORS = ['\033[1;%dm' % (30 + i) for i in range(8)] BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = COLORS RESET = "\033[0m" BOLD = "\033[1m" def proxy(client, address, reference_address, actual_address): """Main handler code that gets called for each connection.""" client.setblocking(False) reference = connect(reference_address) actual = connect(actual_address) if reference and actual: loop(client, address, reference, actual) else: print 'Could not connect to one of the backends.' for sock in (client, reference, actual): close(sock) def connect(address): """Connect to given address and set socket non blocking.""" try: sock = socket.socket() sock.connect(address) sock.setblocking(False) except socket.error: return None return sock def close(sock): """Shutdown and close our sockets.""" try: sock.shutdown(socket.SHUT_WR) sock.close() except socket.error: pass def loop(client, address, reference, actual): """Loop that handles one MPD reqeust/response pair per iteration.""" # Consume banners from backends responses = dict() disconnected = read( [reference, actual], responses, find_response_end_token) diff(address, '', responses[reference], responses[actual]) # We lost a backend, might as well give up. if disconnected: return client.sendall(responses[reference]) while True: responses = dict() # Get the command from the client. Not sure how an if this will handle # client sending multiple commands currently :/ disconnected = read([client], responses, find_request_end_token) # We lost the client, might as well give up. if disconnected: return # Send the entire command to both backends. reference.sendall(responses[client]) actual.sendall(responses[client]) # Get the entire resonse from both backends. disconnected = read( [reference, actual], responses, find_response_end_token) # Send the client the complete reference response client.sendall(responses[reference]) # Compare our responses diff(address, responses[client], responses[reference], responses[actual]) # Give up if we lost a backend. if disconnected: return def read(sockets, responses, find_end_token): """Keep reading from sockets until they disconnet or we find our token.""" # This function doesn't go to well with idle when backends are out of sync. disconnected = False for sock in sockets: responses.setdefault(sock, '') while sockets: for sock in select.select(sockets, [], [])[0]: data = sock.recv(4096) responses[sock] += data if find_end_token(responses[sock]): sockets.remove(sock) if not data: sockets.remove(sock) disconnected = True return disconnected def find_response_end_token(data): """Find token that indicates the response is over.""" for line in data.splitlines(True): if line.startswith(('OK', 'ACK')) and line.endswith('\n'): return True return False def find_request_end_token(data): """Find token that indicates that request is over.""" lines = data.splitlines(True) if not lines: return False elif 'command_list_ok_begin' == lines[0].strip(): return 'command_list_end' == lines[-1].strip() else: return lines[0].endswith('\n') def diff(address, command, reference_response, actual_response): """Print command from client and a unified diff of the responses.""" sys.stdout.write('[%s]:%s\n%s' % (address[0], address[1], command)) for line in difflib.unified_diff(reference_response.splitlines(True), actual_response.splitlines(True), fromfile='Reference response', tofile='Actual response'): if line.startswith('+') and not line.startswith('+++'): sys.stdout.write(GREEN) elif line.startswith('-') and not line.startswith('---'): sys.stdout.write(RED) elif line.startswith('@@'): sys.stdout.write(CYAN) sys.stdout.write(line) sys.stdout.write(RESET) sys.stdout.flush() def parse_args(): """Handle flag parsing.""" parser = argparse.ArgumentParser( description='Proxy and compare MPD protocol interactions.') parser.add_argument('--listen', default=':6600', type=parse_address, help='address:port to listen on.') parser.add_argument('--reference', default=':6601', type=parse_address, help='address:port for the reference backend.') parser.add_argument('--actual', default=':6602', type=parse_address, help='address:port for the actual backend.') return parser.parse_args() def parse_address(address): """Convert host:port or port to address to pass to connect.""" if ':' not in address: return ('', int(address)) host, port = address.rsplit(':', 1) return (host, int(port)) if __name__ == '__main__': args = parse_args() def handle(client, address): """Wrapper that adds reference and actual backends to proxy calls.""" return proxy(client, address, args.reference, args.actual) try: server.StreamServer(args.listen, handle).serve_forever() except (KeyboardInterrupt, SystemExit): pass mopidy-0.17.0/tools/idle.py000066400000000000000000000110621224420023200155160ustar00rootroot00000000000000#! /usr/bin/env python # This script is helper to systematicly test the behaviour of MPD's idle # command. It is simply provided as a quick hack, expect nothing more. from __future__ import unicode_literals import logging import pprint import socket host = '' port = 6601 url = "13 - a-ha - White Canvas.mp3" artist = "a-ha" data = {'id': None, 'id2': None, 'url': url, 'artist': artist} # Commands to run before test requests to coerce MPD into right state setup_requests = [ 'clear', 'add "%(url)s"', 'add "%(url)s"', 'add "%(url)s"', 'play', #'pause', # Uncomment to test paused idle behaviour #'stop', # Uncomment to test stopped idle behaviour ] # List of commands to test for idle behaviour. Ordering of list is important in # order to keep MPD state as intended. Commands that are obviously # informational only or "harmfull" have been excluded. test_requests = [ 'add "%(url)s"', 'addid "%(url)s" "1"', 'clear', #'clearerror', #'close', #'commands', 'consume "1"', 'consume "0"', # 'count', 'crossfade "1"', 'crossfade "0"', #'currentsong', #'delete "1:2"', 'delete "0"', 'deleteid "%(id)s"', 'disableoutput "0"', 'enableoutput "0"', #'find', #'findadd "artist" "%(artist)s"', #'idle', #'kill', #'list', #'listall', #'listallinfo', #'listplaylist', #'listplaylistinfo', #'listplaylists', #'lsinfo', 'move "0:1" "2"', 'move "0" "1"', 'moveid "%(id)s" "1"', 'next', #'notcommands', #'outputs', #'password', 'pause', #'ping', 'play', 'playid "%(id)s"', #'playlist', 'playlistadd "foo" "%(url)s"', 'playlistclear "foo"', 'playlistadd "foo" "%(url)s"', 'playlistdelete "foo" "0"', #'playlistfind', #'playlistid', #'playlistinfo', 'playlistadd "foo" "%(url)s"', 'playlistadd "foo" "%(url)s"', 'playlistmove "foo" "0" "1"', #'playlistsearch', #'plchanges', #'plchangesposid', 'previous', 'random "1"', 'random "0"', 'rm "bar"', 'rename "foo" "bar"', 'repeat "0"', 'rm "bar"', 'save "bar"', 'load "bar"', #'search', 'seek "1" "10"', 'seekid "%(id)s" "10"', #'setvol "10"', 'shuffle', 'shuffle "0:1"', 'single "1"', 'single "0"', #'stats', #'status', 'stop', 'swap "1" "2"', 'swapid "%(id)s" "%(id2)s"', #'tagtypes', #'update', #'urlhandlers', #'volume', ] def create_socketfile(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) sock.settimeout(0.5) fd = sock.makefile('rw', 1) # 1 = line buffered fd.readline() # Read banner return fd def wait(fd, prefix=None, collect=None): while True: line = fd.readline().rstrip() if prefix: logging.debug('%s: %s', prefix, repr(line)) if line.split()[0] in ('OK', 'ACK'): break def collect_ids(fd): fd.write('playlistinfo\n') ids = [] while True: line = fd.readline() if line.split()[0] == 'OK': break if line.split()[0] == 'Id:': ids.append(line.split()[1]) return ids def main(): subsystems = {} command = create_socketfile() for test in test_requests: # Remove any old ids del data['id'] del data['id2'] # Run setup code to force MPD into known state for setup in setup_requests: command.write(setup % data + '\n') wait(command) data['id'], data['id2'] = collect_ids(command)[:2] # This connection needs to be make after setup commands are done or # else they will cause idle events. idle = create_socketfile() # Wait for new idle events idle.write('idle\n') test = test % data logging.debug('idle: %s', repr('idle')) logging.debug('command: %s', repr(test)) command.write(test + '\n') wait(command, prefix='command') while True: try: line = idle.readline().rstrip() except socket.timeout: # Abort try if we time out. idle.write('noidle\n') break logging.debug('idle: %s', repr(line)) if line == 'OK': break request_type = test.split()[0] subsystem = line.split()[1] subsystems.setdefault(request_type, set()).add(subsystem) logging.debug('---') pprint.pprint(subsystems) if __name__ == '__main__': main()