pax_global_header00006660000000000000000000000064123347137570014526gustar00rootroot0000000000000052 comment=7704698187e2de97ea697fde9ccfea583b36bda0 transmission-remote-cli-1.7.0/000077500000000000000000000000001233471375700163225ustar00rootroot00000000000000transmission-remote-cli-1.7.0/COPYING000066400000000000000000001045131233471375700173610ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . transmission-remote-cli-1.7.0/NEWS000066400000000000000000000115271233471375700170270ustar00rootroot000000000000001.7.0 2014-05-14 BUGFIXES: - Check if 'transmission-remote' exists before executing it - File list: Visual mode for operating on multiple files - Add torrent by hash with shift-a 1.6.3 2013-11-07 BUGFIXES: - Properly handle East-Asian wide characters in progress bar/torrent title - New keybinding 'N': Start queued torrent - Tab completion in torrent search dialog - Faster (un)pausing of all torrents - Display Transmission version in top bar 1.6.2 2013-08-09 - Support for Transmission version 2.82 1.6.1 2013-07-26 BUGFIXES: - Tab-complete files with regex characters ([,],?,*,...) - Support for Transmission version 2.81 1.6.0 2013-06-28 BUGFIXES: - Fix timestamps after year 2038 on 32-bit systems - Fix host placement in peer list - Fallback to UTF-8 on LookupError from getpreferredencoding() (as seen when ssh'ing into Mac OS) - Jump to and select directories in file list - Sort torrents by tracker - Make blank line between torrents optional - Add preview for incomplete files - Move queue position by 10 with S-Left/Right - Show IP:PORT in peer list 1.5.0 2013-03-21 BUGFIXES: - Torrents should no longer become 'isolated' for seemingly no reason - Support for Transmission version 2.80 - Open files with 'xdg-open' (or something else) - Limited support for queues (increase/decrease, sort) - Better performance when scrolling through many torrents - Submit torrents to daemon via RPC request, not file path - Tab completion for files and directories - Ctrl+u deletes from cursor to beginning in dialogs - Peer download rate estimates become invalid after 60 seconds without update 1.4.7 2013-01-10 - Support for Transmission version 2.76 1.4.6 2012-12-13 - Support for Transmission version 2.75 1.4.5 2012-11-07 BUGFIXES: - Fix forwarding CLI args to transmission-remote 1.4.4 2012-10-30 BUGFIXES: - Fix crash when changing priority of all files of a torrent 1.4.3 2012-10-18 - Support for Transmission version 2.73 1.4.2 2012-09-26 - Support for Transmission version 2.71 1.4.1 2012-09-05 BUGFIXES: - Indices of sorted file lists are now pointing to the correct files - Fix crash in terminals without colors 1.4 2012-07-30 BUGFIXES: - Fix crash upon opening help window for small terminals - Consider wide characters in dialogs - Don't draw dialogs that are bigger than the terminal - Fix file list indentation for multiple same-level directories - Support for Transmission 2.61 - Highlight torrents that can't discover new peers - Use terminal's default background/foreground colors, making pseudo-transparency and black-on-white color schemes possible - Use locale to find out preferred output encoding instead of forcing UTF-8 - Peer and tracker list are a little more compressed to fit into small terminals 1.3.1 2012-06-12 BUGFIXES: - Fix wrong progress bar placement calculation for two-column characters (kanji) - Updated bash-completion script, README.md and screenshots 1.3 2012-05-23 - Added Bash completion - Estimate time until next ratio milestone is reached - Don't connect to Transmission daemon when imported - Show which versions of Transmission are supported in --version output 1.2.2 2012-05-02 BUGFIXES: - Handle escape key correctly in Turtle Mode speed limit dialogs 1.2.1 2012-04-29 BUGFIXES: - Fix crash when enabling compact mode while focusing torrent at bottom 1.2 2012-04-25 BUGFIXES: - Rewrite of manpage to eliminate warnings and conform to the man macro package - Allow filtering of private torrents in main list - Don't reset focus/unfocus after deleting a torrent 1.1.1 2012-04-02 BUGFIXES: - ACS characters in pieces view look better but are extremely slow in some terminals (rxvt) - Fix UnicodeEncodeError with t['lastAnnounceResult'] 1.1 2012-02-29 BUGFIXES: - Crash when pressing up or down in empty torrent list - Append missing '/' to downloadDir to fix sorting by location - Set individual torrent's seed limit as float, not integer - Encode tracker errors as UTF-8 - New keybindings: - g/G Move to top/bottom - C-f/b Move one page forward/backward - C-n/p Move to next/previous item - Space View torrent details - New options in config dialog ('o'): - Turtle Mode Upload/Download Limit - Torrent Title is Progress Bar - Replace upload rate with seed ratio in compact mode. - Show sort order in status line - Use ACS characters in pieces view - More compact status line transmission-remote-cli-1.7.0/README.md000066400000000000000000000067511233471375700176120ustar00rootroot00000000000000## About A console client for the BitTorrent client [Transmission](http://www.transmissionbt.com/ "Transmission Homepage"). ## Distributions - [Arch Linux](https://www.archlinux.org/packages/community/any/transmission-remote-cli/) - [Debian](http://packages.debian.org/search?keywords=transmission-remote-cli) - [Fedora](https://admin.fedoraproject.org/pkgdb/acls/list/?searchwords=transmission-remote-cli) - [OpenSUSE](http://software.opensuse.org/package/transmission-remote-cli?search_term=transmission-remote-cli) - [Ubuntu](http://packages.ubuntu.com/search?keywords=transmission-remote-cli) ## Requirements For Python 2.5 or older, you need [simplejson](http://pypi.python.org/pypi/simplejson/) which should be packaged in any Linux distribution. The Debian/Ubuntu package is called `python-simplejson`. ### Optional Modules (you don't need them but they add features): - GeoIP: Guess which country peers come from. - adns: Resolve IPs to host names. Debian/Ubuntu package names are `python-adns` and `python-geoip`. ## Usage ### Connection information Authentication and host/port can be set via command line with one of these patterns: `$ transmission-remote-cli -c homeserver` `$ transmission-remote-cli -c homeserver:1234` `$ transmission-remote-cli -c johndoe:secretbirthday@homeserver` `$ transmission-remote-cli -c johndoe:secretbirthday@homeserver:1234` You can write this (and other) stuff into a configuration file: `$ transmission-remote-cli -c johndoe:secretbirthday@homeserver:1234 --create-config` No configuration file is created automatically, you have to do this somehow. However, if the file exists, it is re-written when trcli exits to remember some settings. This means you shouldn't have trcli running when editing your configuration file. If you don't like the default configuration file path ~/.config/transmission-remote-cli/settings.cfg, change it: `$ transmission-remote-cli -f ~/.trclirc --create-config` ### Calling transmission-remote transmission-remote-cli forwards all arguments after '--' to transmission-remote. This is useful if your daemon requires authentication and/or doesn't listen on the default localhost:9091 for instructions. transmission-remote-cli reads HOST:PORT and authentication from the config file and forwards them on to transmission-remote, along with your arguments. Some examples: `$ transmission-remote-cli -- -l` `$ transmission-remote-cli -- -t 2 -i` `$ transmission-remote-cli -- -as` ### Add torrents If you provide only one command line argument and it doesn't start with '-', it's treated like a torrent file/URL and submitted to the daemon via transmission-remote. This is useful because you can instruct Firefox to open torrent files with transmission-remote-cli. `$ transmission-remote-cli http://link/to/file.torrent` `$ transmission-remote-cli path/to/some/torrent-file` ## Screenshots ![Main window - full, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-mainfull-v1.3.png) ![Main window - compact, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-maincompact-v1.3.png) ![Info window, v1.3](https://github.com/fagga/transmission-remote-cli/raw/master/screenshots/screenshot-details-v1.3.png) ## Copyright Released under the GPLv3 license, see [COPYING](transmission-remote-cli/blob/master/COPYING) for details. ## Contact Feel free to request new features or provide bug reports. You can find my email address [here](http://github.com/fagga). transmission-remote-cli-1.7.0/completion/000077500000000000000000000000001233471375700204735ustar00rootroot00000000000000transmission-remote-cli-1.7.0/completion/bash/000077500000000000000000000000001233471375700214105ustar00rootroot00000000000000transmission-remote-cli-1.7.0/completion/bash/transmission-remote-cli-bash-completion.sh000066400000000000000000000013121233471375700316120ustar00rootroot00000000000000# bash completion for transmission-remote-cli(1) -*- shell-script -*- _transmission-remote-cli () { local cur prev opts _get_comp_words_by_ref cur prev opts="-h --help -v --version -c --connect -s --ssl -f --config --create-config -n --netrc --debug" if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) else case "${prev}" in -c|--connect) # no completion, wait for user input ;; -f|--config) # dirs and files _filedir ;; *) # dirs and torrents _filedir torrent ;; esac fi } complete -F _transmission-remote-cli transmission-remote-cli # ex: ts=4 sw=4 et filetype=sh transmission-remote-cli-1.7.0/screenshots/000077500000000000000000000000001233471375700206625ustar00rootroot00000000000000transmission-remote-cli-1.7.0/screenshots/screenshot-details-v1.3.png000066400000000000000000002545461233471375700256750ustar00rootroot00000000000000PNG  IHDR4%FbKGD pHYs  tIME 5,ܗiTXtCommentCreated with GIMPd.e IDATxwxEwH#M#Di(R,J "DHI8T:y>ϳb.ݝz3Ը( B!B!ld4dA!B!v!B!B!A t/ۭ X@Ƃ: ߽,)#Xa7T0|KwBrQLpdk2כWSBn6pf5v!Ѕ)~&qJatn2ш1;0g2ZyK0I>Q[?40;={R1q >¹3>3_̧b3p|*/6qI6~T^bDG^ a%eL!zN䈵,'Ts^-L-H{WH|,:o^ jnjaק>b$OGNIJA]hS^ng9k N@|}zz8 gtnESyIY BF7X$saJʣafptH@=zB]9'qrC:h#k¶0s܀_Vh#|$^iQ??kzW콞8Y ^o] ?ӝ&?)—04MHyBj|PF_5}sa1^y-OyIƞҢz~~~Tok_<,?}Ҹ~3n`ƻt}^2ޛrž^OxTWD&U(_g2(YGA?pyxJs{*|$}Rnr1t[xI 8Efepo/g6oבD&Zx5\O"N%fkjZr}zK ~G߅Ow^o\߼2=t_G4]ӸsW ܾMM!tw\7Vq,뀈x%S^#ONEƉ2Hx .p-}+]ΐFhro;pu+O!nءf7D̲Xu/Urtl'9[qyOoqh9/}2te,jм v>":l7+4'݂%lYҾeAwقv~MAw _-ORWt[_oJZF>=mSK"VE/y=9w'u$R~nߌ΅aiފkꤻZmŖ_ =5+۾8}cfjԅM2~(G72寜B)=k~.^.#1zNxC QY]s8PÛ܂hRݗbxIR:z%WW ¨\ ,yfʏteJ5Z=0eMG/\aTt t7?|i\0),#'@vXޔH-da  7A6JgKO< K6I *to>b*5J 7n\˴wWV&;JFR\=pN%IYxfɘu3+'J@)_KT;8A1nsrԞD[H`Wyj4e4Z=0u[;v}zëb\_1̨'tW";֛'8B|8K U>Zb̭+㟲VTwKO< BK?2y{|QwyujL=CFtN&.1 Ufpi쉋 !َ:x -X/.C.hKQGͽ-?Ϯ/S#سS )q[|)5H[ ^ '~=QWn[,h_ٴ=dIXg?EcL* 7nS6+bMx,/9KH9mby].MwGOӧ>3!W ܸ% b4݊G,h[oA{ _G<s,tDi5ңǼġ0 o9U qv\&lZs ^?HVW+x.Rg[)Pg,bhHEY@ξ+tW2%_9ōBuxB!D [w+ojkO8kʧ϶|̡`z(Uэ!M[H=:br?sm/3Ao͉֞Bж\ԘS_8<\KSEvS/;X~9-_K`MgsIG\HLVݘRdަS\F/-y\]͒bѐF=j+lf+/FpG"ëhI&y 1> !v^u!]{z,,EdӰfCZ~l!@Kvf^xy_#L3*X&;~gGoF!xDwF%\3ciK zCV Dھyw!p:T O+=<_oz < {ߣ[Y٤GxNcpv15ea^o~֛.ϳ[;~ne],Rprd^uFocpWIbJ Wضj#fp=ϥ{b1,9|x.^ﬧtΙGCmȓ!ؼRy| ݖ9Q4?t~ZFtj>O;l0s~ :9q'Glng_y˶*?"z'^u'. {n TM_@̔D'Fy2@3O}a n;Sާ<:򿏖ՠ-mKmҿپrH @Jr4\pɚţ ]#0}~oҘb}rsvZt:=g_cYzDKCyRnb$nG秏qz$gk{OguWtpM}ҼR垠4 u=Uz5Jp@c6̚*,ݘ.$|='~7/u)P{%oO vl(L1~{nB#y\ȟg.Wr)ӟvbux, =C@*|m3Kۣ фs6||\cow/զ|@05Z>,\1ކ,I9^S*W{7y>Zu_c* !xYr|gӷhv;kO4OgmT'JկMQ@!BQ:֓ʸ>?릳}XNyitB@ࣴ}o;Cٹ}_@һ}&W2_9RKBg=v._1b~ڔ (}jXy(Db+enj$uugk<˯s:|3;WԸ( YMhRe6 oj%r2H!Ba 1{x\ t3WfbA[ܷJICFĝK촵|?ܐej$jB!ȗN.ĆB]TECa;JX5OB!B!,m׮]"ZV-%k:!B!>dA!B!vQ1Sڿx-FǾW)`:P[ s{`[vfmO/z&Mh?3 G0 x6Jp˔^v!BiπfZ{vW)ӷz@Y_/`yz?' 56jW E,Eqmy_ :eX`,`S)C`|&NBU3sO/=ח| D}"Q-y/^`"=pT*)B!# $jm?0ݤܤ)ͷ@+ì5Y+31a$ΠC~:1۴ǔ|i ֑|%}O`^czjC.ֻ^z/AhF[JO8Jj}.U1fB!ĽZ}_g2d <9@#eoy`9Iy#Ye pPeRo9M-yqc4qX~ӣY_j+}- 8 4Ȳ_Ca\wbVyi-o[z}V^{:Kl:wIe@kx-Ŗj_2OӬ ee !B۵z`@um"/ 9;b!AW_gC ܤ˜\q -w-Qi :0MSَiZ 2Jo#0=@4ek |uhX keC>}JM^Smy^L2#ڿm5d9Pp7+|u'0*S=P9>w99G*Ҵ'i:w;]6s%kPkOo_~ӵ]Nnm׵kcudxS1.Zy+6!C6}>+bL%˾:yu sS 9t܀r8r"*1u̲vPiyKojRd&Y>Nϛo&`ZXa#'}0 QW^>`\4Sm^5iO Mnz}VigT4 2{֤B!MMfm1nĽM;5ӍsϊMt33L3}?0`z*L7G7Íy_2oɗ~c62Rw+&9?;`*򻓘t;̭/ZƟupxN@s}F) jK=t.eA- ]nWy:Yeש|UY~~0Lp4DL= !B=^3鱇[>G! fΡQì -ʴb!unqܧ>1q4$EmA[A\ƒyE-β: Ŵ LSuYyn,twK-ga4>.sazO#._s-|Fow czük;[Qުb SqiFM(aMr7y@mKwӨ0LM7!B~E }~EnO&aXxG:#}Nn؊uo^HiZ>LӒƴ^\>;C`ZnLv4zaLzvnaz]LwGaSʎ7ք@4`>`c51,FhIS:Zz3 3bv.2Nv+xɘلQX+K{';iYŒrn|0ӂ0^ Yz:݄B!~/jihMMD.AZ{9}SZ;![!1u>:`3h5?:q>t;k46YU79urP|-eg &bZcZud-ia@~-Pzh61&K8ѻ^zAІbzig!X7]O8<χ`ZG;7_[0MzS*iȮVOj`z5P_{4-bZv}[Q>,_*恜mhvTLohIg;B!Ч:#6000~)\r&wt#צ$mŐOB!B] `MLJ=\vhYd,&B!BA!B!B؅ :!B!.2|\"C!B!D? B!BA!B!B3݊e' ! S.,3bhen2[>59q)Ea49{ vWAuV2Dz䏳FYVs|]2W'L~؞L}Ŝs'kLZ㑦NYԗǓ|ur ٱ}_e|\7`l#ٳ:nmjuǡ={6Mqz<3E+>Ea3{lfJ yAqq,AOR~\8Я~~[6D[w^gM/1q%B4q#{7%=|d!7C?.F?Mri1 gPeW!{W}F m@ƉhPID/Y^2 viFT nϙD>K5G.dJ|ܾ:!i4p9o}è<p(ֈG# 3GU_̫-*Þ2YF<߭r!$ O\:!B=@g>5yo$ IDAT/^9'p^l-'.ǒp;f u\NU'@AL|9i6cxI q]1|x;'.&1%gș$vGu3zӨ(:n_ܹ/5<,̈́[m%аe̳'OC|6FRA :8nƠ?d6{V2eP<;;N;Y/ӹ7zMf_1D^U̚*z=_?y e, T?Z:arG@K8ke 츞vɏ{i@"s0>_w ~݊ea2pkE`49j&j'G61/o~>Eg pn߅~r^`Kz7oJRSHst`~{Sax\? QG؈NnqwqxcՓ/u-o盗*fxQ{.‚Z(MLx|9xgaqqK'Jw>缮eyULU}Yԯ.S"_ϕo'|=w]*Uny$z{/|< ^œϙz];zξ+Rrf oFr:+{?siU|7XU<,inQtV|P]﷡jP|ȑyw{C~zOg82[:STyptX :/H\n;{h>\~>ؼ>ǥ 2t[-c[ڸK~q%,~ E'k||i0#=ٳ1Ֆ{Xq@Ht'E=MYOx9ZqqQ*5.Jݼy'!Uܕ!Հas3վ5U' s.mޯtX2;kmwոn>w,S=j~}j?Ռ'3׻+leT&Sԁ{gP#5qֻKUIU5;TKf/aVغ>7wU}vul(ղ|䮊W&nS+{[l]OF:rz};jNkx?RP#6]j]jѨƭ,OR{9KC7`֋3OAqa ,l|Wl6.n@󄯑g2s9x+Ò lF_!ޥPh{uK\dɘLSZbNBŽh=| ״ <1ao$.gOl"5( o%NU?l*fӻT+%]xM0rj1jqevl;8Q >N[z];m_ЅkĦa(ځ{W[pC*L_gQ }+RY$\Ymq\MwGt7gq*R8.XA%xnTΘ,,zu'PL~v~*[Iz;BtFĝIE/V\͈(}O\b"T\ ]2,g')s0Ƈ{CN3v]M?-nE{Hvf|2L:~NsL::Hkz򩭿+vS|X%nz˛%zYg830d6xP)O:1u-%ew`~Kӗrk,VO.ͣaQ=izc;a^ BQjIQʩd0Ce&ЬS|5d*\̀[٪n<+~esճ9 wJ<ձDh|D|yzLyv5JKoSNEbI~!]ɝ5{.vcM`c42޵Ko|0#EBحI Mvoh̤>(킓Kac|q׃LLކ*=pr.DȓM(bŵǟbٙjpr=0.X.>G;SzW>Ԝss!H])OήI:zGPTO"Onͥs.}q넋}>ENTnsmջz=%^xzB_f'ƚCɧ޷uD~$CSwJ(6 2u9={YQl.:Ñ. gciT9}5fʂIcvɓU0lPmYȑ  ĐEQѝaA~{><ɧ_Mx8R8>='.jo<}-lUO:^f=FK;*ws?>UBnPE3Jڅњv@. *G|5eCV}mm!I*d?5afu8´Ejekaq,;O Rh<T+sVzMQuAFyxj]XX#. s@2*Yj͉%/"lHVW*vm>"Fe4F[]*)/u.LFuDvO}cs*d:]ߡ߾VR+.$^ }+Tz$R_֪i}j(o7\-;"NѠj(S|l;*eq>׀Φ_߆96=3_zl6؜U6ҽh4KԘ~ڲ;{_O]aQDo[]8.x7ڧS,YXVoDo v̀.jsXnJ9fpU~ K~WaZ܁wZ)\̴mP/9Zv`/|jٟZvhBOUC>aS_ٺW~q,2j6d6e?vR/z)-M?U}lnjuU?_l[>j㱲nNRmG2jlNvQ0OYIal?b"S!9RMyί,3ڵk@qՒYv|lp=gߎG 8YǾŜ!B!R;42`K11gELȉ wdۃ/Ep-ZW7븟Y\4=o!#T! :!Ľ6?I8E*ċ#:P|n =lwWI"IO[_jxHIi>}ʯBtB)BМiP C*Ĝtze=Cky;^wzyĀ@qzIG3֏N|7쾘R'X3=ws.՜wrF鼩9[F>]|G(n񧈳н&*.W%{80xSl 5>_3ˑb `0u>VZUV(uUGTo/z>+BHRqQ M6d{6uW+iJ)Ү,R AQm41jg_RoUq*N}o*&}7..xVyJ}|-eUn~\Үõ7Tj'ߣ-=ov}J:/ fԿ󭺘l&:J)l7yzQJ9T`*M)u'G~.ՈJ)X^rqRnE~Z)fsNCa՛vzEgz\6d!R㢔 :&lժ 7 (V:~|@Qm:wVMy>o2vJ:>jڹdSGw(vf78FNP9u^/}`::T{? s! (ǹy\)!N PJ)תAzgɧZR7պMUɫ_ؠTV/̽R*"4zts__AŋUy pdyNmSJ UNSzO~lQHjT24XЛOxћz9M6d{ !ă/WTR*%W/\j@Ǻ 8X5c;Ň9#`t?\=.4\sD]KԨ3Q;?8PQ]>Bv~=F!ՙv.@O ;q@!Z ߳C!n̹~U7H~-/xjnb, 2cbUb\ݙWiW$ގnhQ _FyN_j8\8E27G>uԛM9BI.Q x`9R  @j$KTYcP:B!\p۝zEJk6i_ᗺٮ9SΗz>>Žͬ9SSZ6m;РV۠g݋;d~kcsVx\dA<;KVƕTO=[sti7J;:3c|UO"3BbJUb\R+2e GoB źmWJ98˟Q_wߤG^Rwz/\!dA!n<]S8!b?@-6˓\_Z- +7Wwx]}*@.ԝa{]_7T~*$Wݲ:&gW8@0 s?T9M}NOV[QYejzp8Â{9^,(`;Nƥͺ_i6Hjzf jKz.?M ! :!ć#ޤ7YN];&4>uTTRoҵyI2r)KǛ z! úƦ\<+(hGӇWF^јpg`wH:WM)3޸/3elN>m5\1wH&M. %wpOw1E JVxIOu~%(Jb@x8{PZ[FΝFIgfȗ*cZyaK7=r."YHR6d{P7DuaP,M-Jn=ÄjL!cLQ'Sm<AU[~JŤ}˄5{F9t]m[/'gAyu٨_AA,낓9-< ~>>OUdUR*^]ܷDT!c? l1H}栺}UNV/VpyV=d?=.xLLoAZpJSJվTd|n=nh/y}f ]Myr8c)SW;=`a7^ǽ^eM6$ ڀeeF@Q90dQ5!BaԸ(${Y',_!xpȠoC:^!@!>B!B!Bf:!XrL!B%3B!Ba2 B!BA!B!B؅ :!B!.dA!B!v!B!B! tB!B!]ȠB!B!BB!Ba2 B!BA!B!B؅ :!B!.dA!B!v!B!B! tB!B!]ȠB!B!BB!Ba2 B!BA!B!B؅ :!B!.dV[2Zy bv1v.T0d fo/vΔi=%h4rvbF*CZë7݌q2mY'NS.,3?ק7tgfwƤ%;8i:֩=+rq>= դDŽv,.%dze丟435g.bi^vZ_,9DMT}je~wy6B!Bs% 6U jwC: IDAT>K&?MQUs`|HC?.F?Mri1 gPeWEy- ^Я~~[6D9tK8c1wl^2 viFT nϙD>KÚq ڼ'v5 @W~"*x@͜y.w|j\Ȕz'}uBhrЛܫƻOr*sʜ=[7=G'Bt"WxL9m7IJI|4i"{im(sGr:7O |كos~$m'[@tגt##>Q˃ia vfb7ma/Iܗ3'X7HH#8`$;xqmNn߽ ۦXv8=1l㉗cZ\XŖ ؆B! : ܍G41^8YScnKf 3OFjS^OgU(ZiFWrhhL&jb7cq FeK#7h8ir=%}}z'UzOga ~aJvsZɬz sf>GvKEޜ?zad%P C?C`﷨xa \΄^3gkqG~7o}ܟ>8А/q8>/|npJ2Tb𺳜Y;ۧQ=H6YA9v߆AATn!G*vQYXɬޘIQ-8]{.^xgaqK'Jw>Uw^r⧾uT~\'ҝf5gG ޡUD,8CQHC*i=^Og>Ȗ0=P!Y F'2oӔv&}m/Yυ2 Kmy 2tɴ[o]dFjwU:r)jvSn_"㚽[eThCL`ڿy*}\N ڼ_Mi?XDNqKz>*d:Tu/qm}}zOo>csR~w|MzûZaT6^YBy8[S[GUSn%}VCY箪ޮZ/\Uj] aaV>IΫ\ϊȭ]M77 }n"l&l=4[j\݂\v{P x-t{¢[--[؃Y]0^ 12&E_!xf(S)l(<ڤSo M)lyU2x:@I &u(996k@ꕌb[{ K]`f#3cöqt,<꽾ywDwaX` `Y5X`DI%Mb(XbE},+X@(Qi@wwaQy.bby3yyW |ڧҢ֢γO-W-p6ki 6D'D +CYp)zNWWlJP+ן(;x>l`&x$3 ڪw7o4``83A[Sv ˾SZ,spx'ܚRuԻo%OCB?q~{NTbzaaLM!w@OD aIw&νT^2q>$ Xx6>'}GC TRHtxnN**г^b? &L*f'#,: ߶))8+XYG+bW6QYѵFl&HNE ѹa;v _Y@|g^e'Vb$pڀK h]xP~y1׌Z}e Lÿm<4ŨpwR.5Gq *oebO#-?qMOL=V\O0 0t`jp1ƌB Núan\Y=ѪRmH/K9ը6^y֘9ۛAOS;7|kXG!VӃ$䳕 }=S` x9?AvVB!AaU9!? Anb4 tnL =S% ~а\[?]8BR=3umU5Eb TVww$݃Y˾ǰVKtE~0m=?k:xMNe Cx }ýR}i.gkR$:n_-)@.XfTfXe{1wK/KhԿ";.k1!֏?MMlbX0 0 zIj ɲ'+㺾r-s$$)}dMA{UI Ⱥ-vڕN{?N*R^V~eEp"AXtXb-I-:",lYvO&A(.׊d%˖~зt<  qGt+RίA)E!$7(2x%MeKrnIhJ)?C|wG2]B}5q9~%A[өt{uK."A(|W{B ] ڬX =B[j' hwDRa>#hAdkPն^vQ})/%Fd| Xx`SЃ\LuJۿbhrP(?Dq=kqiPX'^/,,,,,,,xMk؈#0 0 0 0 h4^0 0 0 0LAaaaaj:0 0 0 0 S#pЁaaaa 0 0 0 t`aaaFCmFAMGQ^t\qGKE wDe4-bƵC=im ]߅faayCm[Y #h++wyT"x5} t3c}\SHjnn:Cñdxgw@{UH}g vMwӄpVpP6Aσ`4yf6>faaS! !nǝ#0J,+!.΃ga[MŪ?#o8G-0b@6O]q~DƼi'0caayeë=Nh;-%jhBF.wK]'2/bUK8-:C{,aaa^Y8#D~}X=ׇzC1gv"&RHͬ RX-ɸaaa^UW'9gJEgEh۔?L]ΎÎ` v,+_éaڼ?FѐIaaa^Uxëڱ =B=d=vCoL;"9% E5rp_1 0 0 üJW}+gԿ qa;0 0 0 AW #ScWx.܎c++6^c DXv ~-,`(Շ+<`^ݒ\vaaaW^QQA`&5A/Gwy@_8 ?<:`ִa`c,f뇭Bq )KBD?"caaaWIAVa#0 0 0 0Z +W0 0 0 0 S3pЁaaaa 0 0 0 t`aaaF0 0 0 05aaa8Mt뢝7&pz2|2+ 0>}/a[}(ʒ[CM2X%A$E> !_cꂑ wdl A;1o UC_ <0%3$9qBvMTTm BKK(]Bv~eq?J!^$rw#V {7K˨W/.eSd74y/|v鬔vݤc [a:+GGne+5i~R,A˩sAk^ :nŔ%F˱d8jU(bΞ2.pmw 'hco!?jet<F7`Vk# cx{\]P\TZ[49%שww[0vJ;;l.wP:#i!^]1cjGd[^逎gƪ| f(+ W2e ˱UG/DFT+ϗ${zMF?u"k>H}ڪ&noӲS4QOAt`u2)oRW{*d).߸+m ֺjk=BlUJ۪i;*Eh \:02҄dQ/B)jB:R^}nc,蹶vۋ7 &4B#= POzDfW䱢Fst|qDZz :rHV#9Oj6pstr󟠠OO rOKyA"COmH3À!f83 ۦ!b3簤\ec%8wzZU6TBr86Ol S24 kBUqg,a\M\%RXMƚ(uE |`~2~C4P͸mO:=G$~h 5oNU+P`@7?noBebܙENSF̦IJ;A[fC e+В'izɕN\&f\Coo֯ȶzG-n9K4!=]=2QvO6EҩmXRCo4i{))T߱7}*EEuuI3x:5|GgK&ç 椇*E ' o96hM#C @J֞t%=yVn; tfV r5x&PކL$hEGRnwjaO,okhT[+2֓G"\ !]~6^[.Q@$)A"ӧuJ~ۗPJzf)zJ4K,}EpK23=hUdzyC^d;v?%Do5 #=#jHZy.y)IZiM؋W:tࠃ6EߚzL= .tAdDA.oCÜԖ>9@F٨Mn((>Kft0E,kr܋v] ~ 7GFhя^mWtūg{;%KIߤ I.A'Nz_FXT6Ks҉YNd4Z=Ew:/;->@BJ=]?2KvkiKߢS 6Erx2di/l$|!u6)]/Cj}odlҧA)_=Ǚt*r5,fؖE3MޤyH}o3lG>zU[|sV66CmaNL~R{EZE(Pt\A)ڄ.YySؒ6$H}y; !@N`Or $Ci_zr3^*ӳX;5)/rWcWt^j( F/z4EZQQO~MdրpЁWrx#}o*_t㌍Viv-L8 ;9~fUr=!3OC} '~}0,/#02#a/ eX4t!;+ ۿ +?\ ϴ2-RFkoc_"s\OjHgbg S!g&v痟 n}_x {= =iawI^p;K1Ň4[>OwO ne!vQ<4ɝ1Ƞgh0~s`v7 {`!eDprnAv<FGy])r |ac0|sfD*T\X}ݣ3-+?OD{tπd$FrqR;T !vv Ri}8; ݧwgt\zJ5/^mEmsggyƵSUKMWEE!tbFvuh5a*HY5DC܊؉?݁het g%z}2k~-k;OR?PR Յeߩw-Ge9. ;ey3ylL mah\ѠZyq-\NSމ=ji8<{QHϩ!- fY,P5!^E'9a_3N>$bt_{#8Vg1R. Kt;0NF'J#,X*K_XMƁY"c4+=[M$ 3+:.l p C1J=R= O0 `s IDAT74LǣYi]si;^AG8Z6bL0ѷ#3 $ucogZiW/@>Pm{y}F0 pbSG`Ӛ: I$UJ1$q.ƭGeMc bf8=Rz3 a 7Q0;VPo` %^IkF`m>{ ֳli789!א֯荿>T>Q&хLҖFxk/%Tuf4q ?B+5#Pك㫿!wNQlHzΒ} wѽ1y-qӮW!DݡQzxez(!c!=}4q4=TDaϽgִq~%tVKQ默3_TUK@0~/nIF^uԣ%*#1st7Tvnlװ DI;D؋ ۼ1k(!e?||*nGLst~ o^A^8XUbȻM)p[br9"))aX.)Z 7_ojFR; !Sܓq,axc|0o$g<[84Ƙ1ݠTH!yiXb`AJezk)س6ݗ}a, a2a[ ƇXr>t.lGѧ/nTzUbO#9By6a!8a>|D8mӏWU?U _rI}2 cFEqXI3u:þj+wFOcH^t 8} 8O!8n#"&Rn^Aqٚ^(?b_a‘CL\,r71 +ޯB\()3`OR?Awp,IEfMY⣯Bp'n{8qx%\MFJJ"·MCT7${K+7ͻC3﫾7󪙟h _pNf@y1~ I)I}o%׋q{Lo˾ _lj"0 0 0$RVA =5?~ PA`;0 0 0 S^MԖkm>bLmz>7ϻ\!Mc1Hzխ"1ݠyTXSn=NZc1))1Gv+ԚQ8$BDSM;|9EwR0|9@FِTmEE|Ӗe 9I^*ko-P܌mUjG^=*7=Gv"POO~Md*JO՗{~z$ޘ-C.%;gq9EHӚʏJAVbxvY]v-r93$n7[-Hu ;sx%7o!)OEk|ւԼ#}3| WNn TE \7{O̸v uWܮtn ^ISzْ\RFb@]ht%0puVRjXfS  .Ho|+т;GI@BG~7I:"oixJ⎒V)y9uǦoqZ?՗c道!4u?I%A(%-\\mکJZ?OV~,,,,,,,,,,,IJ5lB9Ȱ ,R?>`Taaa^; L9!sPk?80 0 0 üpЁy>]_aaa^0 0 0 0)JW0 0 0 0 S3pЁaaaa 0 0 0 oAAEn\ktCi> 66=&a͉ыDVnaD*sBc*}r1uȅ;p26 7gWR9=>ӸZZkzUK}&\Q3a EmHѴŦhX=Sz)n&DXuŷeaaPFEI^CGQ=3E?A'g:yEѕV`B9zQt#[M3>J-Ԁ6A_D߶!cIkIC6йӑ2(7踯'uC2#l5f(j]U.]뇖g';27 3K,2z]?1ԥ^)fdfoFzR=2s [)ju/a:mQ(NETR3S׵IM +8Pb2-wY@K &LPȥQtɢ/si9uS-#_9L:J7 ݗXƮ4s;d)-/ OE-׸+m ֺjtGQȕXt/2 oE-)Y0|p K5|C \_6_2i}(Ŷic8(JjYbA۩8 cD'kSVGx|jQ!b?OMA"CDt‚E>w!NIk)N"˪ -IF.wK OteT/.%'xR|0]mGtP7Xd_='cwJsXb~ӱX R­(uu臤p5!Q) |`aa*%0P7Rǣ#p"}o߃Cf.,M?C~#iXaV_tHXQ]ES~ru؎vƵL_MGRmr]tnݱ{(oG~ =x eFX}(H'Nzxc4\/բrbNJD c pEtJK:LMGHf~M|\\8-&4ǹEM%\>X=4` c!*1 0 0L9sd&E'9HRc-}r,!u*<8mKh˴I*iKI^lE %|!u6QICf]}rna--?_E/ڙ"еCK2Sf}iX:18.:ճɽo҈\Ϥ]Qi KT)1'Xj*Ky$o)L( zK!R:TUыAL&GEA?(G$Ci_zr3)P`@}LC:T3,,,,,,,,,|ktP}u~ڃސUAѝ.]]^GU짡G ijMHM>DL^M#Gi';2tXXꇰ*}QT~OC^pk{ƸyKNOOj w!LDdtg2,mE?qMcQ?۷p9M}I{83:Ԣ3qlWsTч0}#cEc*) q"Pw~'~YKz>//x%&5B?*a#2@Rw0{j1gv0 0 0H$0m08;W ٷpFd=@I8ѩd }T %@"<{*:7C EFzPN ^3BkCe LS?a` %yC8Yy cy bE+хLITtEYz)Ary?# /rtHmW]$0q*!aa&|OCЪ; B!BT6&o5!?AvVB!Aaո )?pfyDFfЗJ!7wD!P/R#X쇯OE4Ç8̇{+ ȥ]L~uƾㄆ =H` >BXWCoC?Љ#YQ{Ŗy֘9ۛAOS;7|kXG!6Kt/JϢ=aU#[ߣX9%/ڿtS"O>YuCw47./M' hwDRa#hAdkPv>oxP pGzˠJbLޟG[P Пh#KJʶ[a{5(WGDK(" Z:Z*tNSکXS/r4okL r-sT#hr?[bPyu3XXXXXXXXX I¯(!A%AAn K: F怡3~e bSsSosIW 0 0뉤ht 6aaa Yi|$0 0 0 05aaa80 0 0 0LAaaaaj'P| @q-wl-:n}Eƿk]^zp*qh$-w~] "vbnзsxB -'m8~ z vP[lR[C_ڪcꂑ wdl A;1o %ƯDoXX]?PR#@bMQSzoc[Pffp~'t߻iyi巹v>\=ÈU{B29싯Kyh]"b|עoz%Tb5k_k=c=4oŠVLit;4>4a±ǰS`~~ ǓE֥q1CSώ%tǑGUm1gx`@86E;?1.Ne!?jet<F7`Vk# cx{\] m[1SA}&#d:u.|ZNi}g7VN_ Qk/Ʈ1#-Ȏat@GUH3vicUwd EecLCmrlUX{;>؏k y+L[κz>y/(TIL҄ds=]]劄le?_?&;ظO9NDAѓ!$$ߨgE:'GЯ}._~c>Dɍe_KF BtiY)騧vݠ\:XS%P̥Mٷt]icLU;]Qre#u5){ӴK"m4B.LTu~zOiB2Ȩm!)ZrEZiw\ 5a?0_E >Ӂo'a4X ЭIp,phJQ芺őX&9 A0 "8"'cMHn :wi]Y1}X,VBVO@ upA𥛅] Q-`$ YݥEL{따 &TI),D+fY&qRXCt %T'bl q'zۦo7 MGӋyH;Vno7Ba^h"fvajyK:ׇZkq$w#{`ƶ:i IDAT㸚*@HmaC./쓸X=vHI `7h~;4Ap0V|T:؁^ >sK_Ysg^}0+y_M BA8>T ֆWi1l]bS G܅8ϓ2e?<)E>ՎvTj/ޫ zj30{o0`yi]QxM?g6+6ė7~a՟oHWފ};|9Em 4_>/A9B4w3Gh;z6QԮ)ڏڂ\4JYxξu(;nq.ł?vJ4~k(+ƳgNis>@&Jع|ݱn{*J c;]Sҏq +_^DWm 22NrE A+ Lfvv6莚 Y e\9ɿv7s')x 6 @ l|!҆X+}q&}'a80c;ݬ!X&,_Gs;48 B BWSpC{pwu xm{4F7}fC𦡺H;M¾AK<+-uGHմ8.22){ cW&1)nohQvX-HqïCǝj"5Z4+.mhzRN]tnݱ{xRV~Y1|P?E' t`성?LMU !^J P߱7gS8 bz;E~WtE~\O}A9^!?j74{MUjΘ>Zhj@67z3{֤&xW@_si'/Ҋ tNtLV3–!9@irj,{Znq;ge =PY^+[$~|(ZOnӔhqbSjpXh^14Ԟ>?vVvQ-cUt\Q==цN9ER?S#t# rGWwR/F4g ߏ뒴іс lz޷(7e->@F٨Gᶊ""%R;p2aVrai`[z3T2jkXtͰ--ӶBgH| <1v% ErJmkpd?!A+Lt]:~-p{J{~zb0vol^;]K(dds5>^?U'x8ê>;ߨڽG ^3mJ9`gjҗVΨ7c og:FH#Np :T=Tzm6bL0ѷ#3 $ucoʶы K+&c<ՇJE345<,3o:p70ϥ0cH(p{4} O_gOD)*3,>cmϓ,,INXx> K|P!7daۇN0Ӱ @rnU}Rtaw*]QUL mah\ѠZyq-\NSwwpx޹rpx'/Ry.nSCj[?%ZX͂HYkB 9a_3NPw~'Pxk/%)q3 щRȺ8 .V WwEOxV 2~av_# 2rtgʹ+XKϥ5@(g%"4tC[o82=UC[b'w@OD aIw&νT5jȿs¡6D7ŶWC:Ыm[c͍qӮsFr6L={X啡Q0 Rs uwRDvl_GΣpnf$uG{%s8m(o0Lۦg`e= G=I7Jiq ]?B+Vk&#J5NAj~U| lZ$o5#Vg3oEu}\w<>!ɧiUJt!)t!^*֦oe~WDk/+yVyMG Ws҇8k3-tFû%_/h ceØs>t\*Gѧ/nw'Yй88^Ǟ`0H^U>NE&1fb84Ƙ1ݠTH!yiXb)س6ݗ}a tE~0+oִq~%tVKQ默3_lˊ.XfTfXe{3g#Ȱ27pה\3%K\+s24,SSTL35sAdv-JAPP; uzg9oh? GD ~4V*ط%nkh%_Ӻ~mG`>HYľ̓cd.uJm+J)\&H"iu`jf*v)VOH9:? lJn5̌\͢ ^8f|kn9mN-ׅh6{9O+3CgS> O*}V'ӠAE-%QvHuKR_<N?L!#⾪$]"5>?q\Oꆄg >"9' sXä>oh?n*ؾHm e~AT>NӦ/ zGKxT$,,zHt`ai"8E Rxv)q[#r086 pPuÂ5sSێPFlAb-v;'ngoGcúE&׳ʯ'>غCoǛҺ o!DJ₎cL_WbWa,:?N3Elev1|Xs눷^cU{jOAgL7J0 0=2#fj{LC9aHV3Ke5 4ްuCzU[A> Mm.X ?yjZGu,:Յɩy`$ 2qYV=}s>rSTnv&Yћ5Mj{UO"(:Y_zXzT}QFDoPU>DFHd(8T^<O?2ߓn?jR`$ǻ`_\T3ȿV_K6הU8P_zƆ4`2DbZ,WN'4P=`v\_|8n SY5mєo}ם5s.S}FTb=(TpڞY͗ $CɕPhj;#twodFBvp(/ءsU`{8w~y q ~ݺFImo}կwXcxU^} m=k!ߎj0ȨQ3|#{V&ASImΧ!د?CiyeUEu9ڬ;^6O5CfD6ϽDҡcnתq9?,5XVW2.S0+2$6=wU>z}a\q9?+LՋ'w[+_F]rSɚLaMe%KUSomJW+Gz`2P)fh~$Ilr%ZTJ՟}Aj|ZI՟LeT^Zy?\E~+q|rR~P+5p4b~ ˍ[zcbND/"□9˦;/<ߥ\=ĸ--Ea[5DfsfXoʦÙx⭉ E5|}pho÷_<abdKbgF L /A>=H{7 [n=s> ~hs_rORz*~ #-jy1|2iBc߽Bܻ] 0__Mj{_\/yof_t*{W\<6B~=n[g1?vS׳8g^rxt:CqmSIJ:I}U IqeTZ/Ew#WQ.]N,Vm#L;S`Y61C:;<:ŗ O\n> P$@zF.Ϧ!,R0&'0zѸGR'\M+I/?iu'UJ5:YgpR6? dWWR_~0NxJnLE,d2-~ j؎ڡ`> ʕK ɭq ekbm;Fv?] CXA9aX)e!}]\K܇E qJ]#Z>@ngnz+i8Y"j"n9;3ުdJnsX|Mn"n̔+qTImoC+]%OD7U:ȸ]/Wlć/Ơ7|Zx:cҡ6n IDATn`FwW.n2ޢgCp?G"?49âqdQo4yȌDe)oYxJ&w=Q`tzQt?jRհ~OQU=V3׭K/_1 SS f+p1"nnU6GW3l`i~..B~2<}ݻѭ)x0lڲXtA߀7~o-ˏ^f ->[(/P 4 XQ ! mѵF-1z+~| f^~?XA)9 ?>GFySt78º5djh^z uc&cvMj{_Z/Y5.~{~ʿ j%Jԋ]KEx鄮iS !\y.5H/g?ųm\у<@!V勱ԸR75Arc1ѧHK r/xq<慥G/.$; Ok?MCU{je߹kֲZR_~0Nxz.`3js F0w9}RkC .~P JX9 eU⛗-0jTⓟMЗxê$?բ1 pjsjAiVcvP+9Ѷ$?kO?|UmJ+4rsDh=/^P ;uy>& *l(3k[>ꎠ vP[ALaf_2 MmBBzSol2G >y)<=5gklR03-_Ec!ARCU7C hm^ow`Kϴ~X;lU  8uE+C\j6T/Kk>V&pu|'+ѹu#z?ɱ5?b,;͢6:CyD] :\Gj3Tä6w5qWoDp*|mYHtCϡ}$$TԔ0a//h:{B%0WAxbP=&#_Zd~oCFS]0a+Ρ{B` S#¡xF0Ɨ+atS$@23 l#M*.wK䬔=R8ML$dln AuB\N)\&H"iu`jЦ?F!NP((S\*:vtV7sW׮Pј2c8 G)K_w@ =|NZ}qKN"60ii_A=p&6M*I<ܶ7BTRFi3fF Ƈc0ղI(KAn1}M$!^º`neh=z)Ǘ-=U\xFf_`aaDL*IX*&e YM }_jlP>mZ˕,MRX4gGRf_1>_MPQrѨlTaJ6ѲɴsEEW&f]iU~  m@(RVoVE~i􁛢ucYt"i-4T/RŐv7GjPP#7;̴гʇBDc;2UH >K? !Ck@M}K"nȘZKZ=OJKMMH0 uzi -޴zh#l}7P`iM& SiOcDkuд~RmЋоt;Y*Ħ|C!ϪG^h,,,,,,,,,,,Ew3"IRVeigjnAM?|z*\Wu Ӈ'Wҗt>zӚL#jڟf=G}N穤3йSϐ$+w_LbӠDo8K+{[tRsܥb:3%sp29 Cp7&ӞTݗSd_OQ+9Ѷ֞~z/, гUЄJ +#l+<"R\0p2 W. \~.վ0 0 ϙy-G> 7E;AHNqhQ )JS,kjU20 0 0 S{]cؙٰ^&cpr8;mpk^.L;u7V q~m^:\ 8o / 0 0 tT!COwpfu0b_4zLl8T2;1+&xBH$3P=`v Q[T/+L_+c=q 8O( +N떚(BL^ F^X+eI|k%Y%~[֡~L-28& C!"|ذb^}) 6z܇yjX (WJ;T>MډS}x,."DBfAa"Bc;0 0 0 xn6?Äİ*4;vǥ/#V!:,oC &23Z8D >mQ6}놆.XeP7<78 W# &JkIzj7 bS,qW| 5<_A5_Z2l ¶:Գ]sq"*j¬g'5?7^²eCKg}ŃB\9\ΰaa:_{0' g.d>/0/8`}rO2av<+hN߷9F|^(-j Zϻ# qHϾ#qpq\Q5e]__fzj7>ĚBV Z*BKH+y\dn"ZB,EpU>axW"nZ:0 0 0Lt{Y8;{u+7_X] zlj ]MCXXKoNa\Rr*T%\j(K*EAq E 䏋HճTii鍉;y$e`i^1TFp;X-*\*s =>Y86)T/1 0 0LD o#-z LR?5=Ku w=IiVRGo#]R+cq a7v9Ő5~ ;Pcv)wC ?=x``FwǻH2 0 0Lt0i hW\d/Lp<8LUAr \E~nfn=R)E/2\v媡 u5$UR& hQfyp+_ K!pAƎ`TXא_~V\ͫtPBv<%xx k`aa xC?MÀ6V0q>}<{l+-MW4=l PXi1z4M+9 JA kO?|d0/1[`7 % #5{aRUuqzT=Kd;d;'*OǪa^ݹ₩ 1 0wIl嵰&z4 y%:w|Pv<-x Sg(ϟȃ 0 0 Ӏ1s0EBE ?+DϡO1\z7q9i?-+wtk!`] RSqj /B|vm^R'?CxW$gT^8@]i~y7G2/ 2uqc,nR퐂U>َy92b^Ec;b{{iH_Eed_9lpW6i;S<E_ko|`aaZ#+I`hҌ0 UݱrY!I98|=G 0 0 Sf aj0h&> | p`aa3aaa9<Ӂaaaa 0 0 0 t`aaaF/0 0 0 0zjBPQXN.ُ=a' 0PJa3l= Q3}l!g=k[ݫKMC`rޏB?eaxСιqnvC3635E`0fS^?6NZ5o3[Tǝڕ;q*ʶI7Mn'{;"SsgaxЁ'nbZ+Y'Sԋ^E@,L۸B ~Ìd,j/BS2qU#){Sa\Oo43hRɴH #Zd rk"gRMr\ QIU#zg09A)"O]Lki,l Qo tꦠKln#`r!@K:t1IUB<.{ p`;Q!C?|]xw+3ȿ d`ו?Naan&$,ʇBBGU O+ˮP~Rb5627FVdMӕ+/<>N֦ 2iG>A^f (\_ciԢ r2tN/MśSUmEG%] jJ]OowsF 5R{ӸJu?@wQr2 *L9?Zv"vSPN"]:;9P#@Jjp %o%C IֳvTؾn4ӚLd IDATΟLj$f.5O=YOZ|  7zg*]HҮ5L_'UY^̩321)GF Lg%sTvSś\D Eszqv$m._W]%א_ZC'L$+hiT-^%SMUZ-mE=Q`MV= GU* IYNlTaB֞#)x,4̦YXXXXz)IMKU\;/=+3}fl'\ys J?Ci9FhZ%Y ?P_U=_zC M?OJ _mg7P+c;J:r8]Mj5|I4iZMU5t.jwMe$k2:5Pڑz~ɠC9 RQfK(druYXXXXA^^!rҒpW!lW& , ]MCXr?I>+p40]yr7&*[N,Vm#L;S`Y61C:;TBW(&jO£qdW}{|{eTZ^ř8w#Wac$Cb;$Ebho) 9R=@)>p@^SkY^pL4J0t7 M~g?RYv,11x'3 ,˕/dI[vQxI O< &F#+/'w\Ni 0L}hXFRd⎹'30?Lt)9EZfƎֱ5.p9fǧ[CY(PTtYWT.Herڽ單xV*ԿORGh\G) E1poD?2Ao .&r !kvD@}gJ]2؏@nQLgx0 4hxȽqA{ʋaJnRwX6Lz=T_MÉ{]A?֢bEL[v?շ=$&@!Զ \Eׇb.:29/uWf}zߑqD=Kt'eplܽF`\PgB]m+Z"e}\ROb24臖J-sZUof-{O2Wjp+_ KU?y9zVOH9:R,Q/v)VEة:/1aa-bǗqRorjb˭naJej_;;moJaaL4?2cؙٰ^NĊ189~ pyVaaa4F2#*d(~xpάFL6e;̏S\*\`|Si`d"ĴXx>/_zcH!Bu;ܠc=q 8O( ͡BIr\ QI#zg09A)+WxwK(|f7#KN8&<22 ep,U-u?__e0i 5{Vk'Nᱸ, ~6axMY %KO8PHnz\MYJE^#z +3uܸ}z+oRԸQ2ӐYx$dY۟`OHF,QRQ{k kCIUo| _B;#jAaaPt7fibИ^XG)҄ڐa%̩321άKeT=hoh|/W4Haќ^Iq}߫|(/<>N֦ 2iG>A^fOKȲ"ɪB:y|Az[•56OI-`('SKZi1PWHbrogC Ica6dx?Oj[A_ vFFҨXZ=Jtn4ӚLdΟLj$f6|(TIb`@^L)C) 4boJґɁ)RZWc(y[y 8~%G/Qِ”le'i85 K^?Kno9[$Ug "i/IMR{ 5{{JxFUSnuRoMb7$!Y \InHFjo7wVIyTITIm8)Pio$tzG09:NF}/'23v5u7!42 * ME/7˴.TF&CiGzM/ZxmrQpUfK(drj̓ɴgCFUw4kNޜ#O/ 媺ӹt *T>dJNi}/vr;89*ECN~ohOP2*hӴ[H;N~\O^$?KmԗZ~4 3M:xuSUG*i}7qHbܐ*},mQ}~RE$W%Cj[4$s7a0m?FOV Ndj,,,,,,,, :hwzŽ, vzM݈/,Ǯ=`."\?S~P-%Q:˳iQX>]a\Rr*}.qiW, YM{}oB;^NUas:rkd Y8H;{XIՋr5gI핊zԏ/RګTu'eʬ씣ڗ[Mj$nHA68-EJT=Kld+NΥ;gگ0? B)ϐda9FZ̙D=~hkzgGh\G) E1poD_Z5\y> ĥDNa1d_Ž͜jEri_fƎֱ5.p9fǧ[CY\C2Yv;G*}&2MQiꘜp9j$^]ĮPE^BQq]WPz^H"u:^MbEܨ>"OXj^⤀F. aC6ǛCp<aa;` F`[>ؿCxqdt7 ;jCH[N>.)oFRYӠB]Q[Q؛uTهpNY[߸s?i~e s/xq<fDr \E~M3pQQ%ٸtdـyjt4#GNw"/vl]?D/ݻv!Ӱ~R"\1E/2\TWj4\GVf-5kږ[M8$1nH߉{#l?T,]#g(?APu'gw1՗ѣ1:[Fݽ'qGaaK8O0Lhܢ,zۊse/_"aA /`"2kdo[^N]mc{B (-ѪtZ:.'CWc>hFz&'|| ?Ok(%ڎ}* ie![~~A1*śJtؾ2 m׋}aQթ:>8'["phh?]_e2?aB/GI;<^W0\tH^+՟5%/1[`7 kHɸ]:VC1,GCGGܐ B`U \}GϞ/3=#g(:R1&)W+[w}![@, 0 0A$eJRDA()K$Q<.gaX`=PPY.5 T%2%-/>(RFP,3%5Eyh',0Qr=?EKe<<< 2"خ/cm  ۊ;aq96Q0_`[gugͅ>gW}V&Oߤ_Q-w'_֑BW aǭgȞ>>ؑ}l*voT[$f|v9qI'XGO/·ܔ'ϲr#B!tИ8YmڣPن:ÀpxYTg䃘8o^!B!T^Q^2c'@oXy TqϚFF!B!yCZ'Wh:mɁKFh#M]KXٶ5=}(0z2.#6OEW]} `r: Yql |0'2i dž]qjz4e| xu {#~O8d\9Urƙnܰ=umLH,~Հw=彝>C̍cKf_WJ<z!q⇁(^B!BM:4TNhUK0p appA8|>{ږby= >(>zrwƌ]>j#-߾OO8ua6x#/Gm/Gpa(4g}J_<{ju13+lڠ?م#͡8|V)gXXT]i!G<1]wx|0{KOo]v:h>p=l@{p˨-( F 3(wwƲvZ.\ls$M?K!By-d,a' <%/5bRw|;W}f: atwՠ紈u6|agiId2' mW}0Ai>?&IO_<ի|)Ϫ~.dk,Da;c b,f/R,xoD>?='6%]1AҩOHzZvBk6d2;mńz|.fB]d_(|_|;2V<(P@ (PZ,0W:-θSߍp/F' oH?(S3ⴃ8R\"7rޅGp(Mq@Lr71doBg0GBln-p#:-4~< FCЫ.~.qfG)gvD>k9\Lq&E1y7O#jڶ۠{ Ep_| wa-L\(R}B=zJ!B!6T˻ O~ < œPq?m՘?t23A~i9&P\.Ǐ G'@sJ<!2b'^V|i1Cg+p14Zg#:pի5nW(,d rrNj)B!t +I;9=3aga0WVaPbV^b %L{TJm:0sHDB5⭡;$ZJ/!B!&Hm'<_ X1x <OqjD$ K9|1.(_ȯsq@q ;2=#bExx0Ҋ_+h˹wk8d˪@^"(*KŅq(2 Ͻ $?9 9]n3OEӰ[[YՕRw1쾌TY*. sJV x&XA!>CR IDATH|??ozL4^B!BH5lM3vÆSkqw|QLA!B!ޔdn(t|wSiB!BKpJYH<yfia0RK(K!B!?B!B!WVE+!B!4@!B!IB!B!hū3 DPxJP74V@#Q|!_{z@!BHMhXU 0zaW_TTS!By݇˔(7^m)$$fc"1&6jso+B!Ԧatm[p2Q K<DlY_N'!]?!+C^'q2plTtߡ.y;a{2q\ܶ+'Bo!38s8C0e sJOAWWt-7w;;X 8*B|WSjL=xP u"p=XG3D}1ۃn"@f XBϼUbzV!B! XYAr' <%um+d֓Ml̛-kX)62tِX\65kIݙ%͸jܽXulRGfj(dzMK#X/RWk[x,ubLlց*qx(C[nJ_F"׌^⹫, f˟ZœKIzCL*TJe' mW}0 #0meZt' MȾr1PN|;2Ry6e'obG/2d'/Ƹ3&b&rgk2wc%'o){L=vA6 g%`};Vb=$52,dU?I#N\{//xyn2 (P@ ^IL}sd㷔o) ()/AA1Cʄ=c-_xGbAOa˯ {O&aX\JHBJzROL'H|x9zѳ!B^Q~چ؏C;Aty:PV̋϶,EwqV,ysfl^tT*#oA5{ O~+KX, a# }LG|UwDr B!й$p0.Z7Pzf,RWWB!ܫ+'/@)҃}+^e/#M' <;G(`|8k]7 OSs4[sZCR_ ՜$^ÏNch=9O7=M*xN/_=G'TB!R,t B!Bd+!B!4@!B!IB!B!hM:B!B!D+hҁB!B!ZAB!B t B!BVФ!B!B&!B!4@!B!IB!B!hM:B!B!D+hҁB!B!ZAB!B t B!BVФ!B!B&!B!4@!B!IB!B!hM:B!B!D+hҁB!B!ZAB!B t B!BV4:|7N]8Qma,xMJBh3,}}81{iQFZQ8˽`XWa C ?6UtpٵQ_!_ :"[tuxbH~}gp=b7c#з 骖/w;MRA qjqeT/9f?'wCsa_®dNtk<'I}w07Z .rl+?bZ$S47IZNn1.4 sGK32,. a#L?tWeou5S3*/jB<&4Ű݁K~Ŭ~omw*ݘb~vxhk$=`'fkuLڠq [&F/!Ts㮘7WV".JE- +;i?᳎Fuh;`q4|muܷ=-p;JlrS"aC̍AI I a;xLD&ǁˈñSDW1>nܰ=%G<Ցyz K8`$ m}+x8lqVD$:xrReUQ^VR* , Ƶ e%"߱Jƛ*JQ+@|/|ozխWA CtU]g}5x_(]]C+(,C|Y4yo(lm0tx\.y #eߎǃnl9_{债6c N&V,% ߜV0~L|,? EvW3:@d=Ou10k Ī\`NKOx&WI {zu$2|ˍO=P/R_4دVfw+wX7p[+a+1j `r: Yql8gj^+SnխL)<.Ҧh^Y/ucঋ Seo!|߫8S\fU;ɘ5hQۉFChy~Oހ~k.g#n sз ڸΰ"IǁHζvp>dc3ƚ"vx6#|ZVYj^UJ~ a>b3v}"BȤnhc〮wC8a>:hLa_|P9a喝utlsn0 G$bKAOW10> նtFPl.TJEf~3k8,5t54!LEbR_:jҫ*]4wف \tLgjx/W'FyW Ъ VŇ]xWuzX ԻKC n@=1ěcvp֮Yc#ػ+l{c*vZ.\ls$Mܟ#f @ 5\cN-X{DU^>tM=:{"+y_E{bQ zJ˫N~ZE (,%7[fѤU] ވ`Aˑ8oPA!k}hf% o!h5|VًɋOA&ր*4@qeDJStwjAucF'wDۚݮP+n4@iAG`zSĩ9[gc_Wg*ZaĂ^1_NEگXY\Ә7v!y JQ_*¿یpPv֒o"kxMuŸ{|>7Vg%HT@]]Dd5u?s?}+C1g%Ӯ*(LžCxmB_(Z? !)X2 L|/*_ U^UbgFBD0YxAMzV=>}m$!5[ƣ$UPD|LF{"=384IS}EQ:N'>W Kxp- w$0 5?Þ/ݸ4`'?,>/3S\] Qٟ9\By]Ҳ?żpZvO!b ;/r+.X22D_IWO=ໝ2&UyΉD<*{w`Ys?f 9hlx)>>y06` $k2}2Uu/`t[_p3n$!OihH^<}9M:hPy룑RxGbAe'AVf@OᎹ;'4u'AIٴ1 m k`NRo$ij1Y KD0@-!i'Wnø0 JmFw<&jfz]PQHškH"Ǒ=yrubzA|_|:0j7L7Fe/¯٥^ [q >_Os?MVkN QNMT&0D_ %%((f`%E(>{B9\Lq&%v-O|WrPOvWy'px~X>wYvkC,E8u#OK9+O<֓Tg|UeSnD?ivF>ΞR^Y:*ktkWݗ+ż2)Hl.B5uuPr\9wPԬ & P.ΏsP4venTk/>&4Yp^stlmDEɯ' |٬)H(ŸaJŃ].KH| w=>s7Xt/ WGۊn"ǪƫQMxFwb,z jvxk9*UW>CaYlĔHUC>E{̎#Yr|}X-Pwd[^սt{ҝH?š^͔vF:_"~UvKmwG+L;VZ`kͅ*_w3bՏOGX/G-]Ao]uiW̽&eu̸NCeq$5G'k}~h< vMqdke; x%Z4A!zZeU鰏jTډ4DGaA''O(\F)C?| Q_Z@F.k@Ofp=(&ة촹q&^__KR"W"B$ԁ tjlC tMǷ: Txp GֶAGcx/6)IrCJ Y ˖_>Rkv^\}h=Y]3r ˇ TvWRqdOشsLy>nGa.ܳD -C/؊?V/V_ZՁoYʤcgwƔ3&Wsjd nk pni+£b1L%# N&4:뀌~>FЗHsV0-=,}1zxktD+Eٸx'T"Plcaro2ga0Q#Rp}-Ǩf h:Ф_#OA&|O ~1gg_GK>M'Cc|SX^5U$= B}4vø!XgvAxuӜ DNu5̗zxgx `?i}Fh~0>0#w#1_>塭I7֥/hS/ ]*53b % UHjwy׹ ݂CM/BljXm~11X<}MUh^&Keu}(3dY/1Cas % 8>ގo){Q_FQ&.&4;ѓx,6JFZr4v~dD'7x&XA!>CH|??o…V$[|>Ǖ.,,B#϶Q]ÏNnu4neUWqyƋR La|RqqXX-`E1at\'Ӓ*WΟ$i86CUǯ#]{g->{ ?eʡיUɿ+nqKk0g3^nl~' #^yq`\M2iܔH//yC&lB4d)5v1gz|_y?-FzRHqz|cH0; >?gF:] x.Ʈsב&!v|b;yoA_G10rK Rdqil[2%Oo{@5WTn뫾e`yi?JRȯsq@qCMRxը|C)z{gіs?6dpdU/)]3 Z U IDAT|.rcbAd3eO^q !i,_1P Qb wq9/ߪftQFI*~|'s@rf5P|}yQV  b=$L:K@aK]Ŕ <?f1kY'j7츜}<$O (PI+.yYAqeyYXXVyYAhҁB!l²žk-˹S{oQSB!7l4@!B!(+z9$I!B!&!B!4@!B!iI3,. a#L'T"*|ť@ '늰_ɧѤ˧4 a+~x t#"B맬 =yE ly9+عՀ$,F(hh;h8^ c=LgE+tgm̆/o`xIzCL*؍-Z؟*n/a.17gb7PfLԍW|xǫrwb.foܗ5P=Ȏ]ռP#$i/R#: ] kXGvيWXĬ6Lk3;Z:k3K¬VPWBКM9~l @ (P%dAq8; ֪:8_Cqd ǰѻ*n}HaיK"U'wUSsy;a{rmaQ!0z2.#6OEW*= Z}?ghrc/Jz.#aL:!i2yRqeM$VB&uCt Xt|z4lkQ[PAfUz&2}qA ٿY͹T:&d5<⫓S>$fc" VxcG0=O>2ě@f7o8d' \xU˭_d7CwXM; hy.twǬ?s|럑6'<9i]_1 " l Ъ Vȋiɻa;Yc!b/Cj&uumױa6„xsqN+돶#xc{wz@`lB߸3-Cwb##t]l4-+q|p{L5ٟR:1c)bWHa>{a˷O-ل9j8أ.z/k~wT@qB, @!thwN$Q#$;*uYk{((+LV`WLׇq+|t{z߼ D0q9 @WW::(]A~~qϪVT1Z ]S|er+g b46qIMqhZڢ{pc#vW^U˭0 { ni }f^jX64Hb2y gy3GB~oz!FQi5^/|E΃1{YjQ#H}e /Y).ED݄ێ(V4Ә7v!y JQ_*¿یpܟP3\pvB!b)%&0IB!ФC``o{(E)ŞEsɟX)w4 UNl2)\O>;6PrJ_"=ma7()0$ŕL0d__yVfc B|}Y=K싿xx c^xyJjWB$5RH dq)d!ZL\LR7phD+C+OJ Q8:T<^, ֲs-S܅[r(|\~:VzT–F1Wԣ,œ{Ix Vg(x5]nP*ywY p?(jއ?џ8paȃn\4Jֿ*Sq(+tῚgp} +Bn>^CE[-PL 6XC'#>䗖C`.~g)ތA#碨>B!zdұ?LL-1 ?ס0G ;Uuۥ!*> :YUy,,GDӴǙ8{M~}-yu0z'hսUOx6'YaAGߧXs7R^vhUzM3_'^M[utMǷ: NS  ?5wLG VZ(Ug`-sSw+lGЮK$LdCӁދۡ] ޅ/- @#n \g>m긱C/؊V/!BM:ڌ6ODN(Abx6ln2[Q u,VzCxoo16`>C"Bo ݁%Qr4 d>'D_iس"Tz g*4F9!8>bk:mgRxu0XocڊW;^ '|ۙP&n:lߎ4([$叩C:BDBZO7CHWj17Q^ѡ,ϫ03%z5Mw<ܖ`\/z&8[/T hX?w~ mUOK 5zKa"zl\g{C*B(6EAe04Ql6_G?;HhSQX?Mu!ZvXp4@!txأG8}^)w4rm&0>Rec,JwG™&0 LVB| I~#2~ބ yO k:is+bO@PJ60w0r8 FWc%4\;\/-ص(bU9qV"^[l 7΋%KC5R3ƎX*;`5a¯A&KA>;w)ƨanyWӡBx|1鐥'} }L){qo\puZ􄥰ɚƕ"={1eeRqyd3TkqiحƭlWFD Kc˵Pfb̦<m\9%".LnQ|^-*@!)+beY 7R/P !a!^ M\ [*xɂcu}ÎYl7C"@ (V L8@װ5BHуp}g1kOk8R{(09v6 koes^f| ;^0Hz!lP_v$cU"^ 9%z6azB=ƃM6`&:Zs,%lS3f 4d-\G1'WT(-DRoHzx]?w]Tw|2Fv"f[@aK]շhG_ v]ba (P@ eY'73_Ps {klŘ&4$wW3g"f?+];3V8L#EeRnlPq{;v1s|oYWf܋mK<>U7;2RNl݅lE?vxs_Hzt2!?Xo Cly9+عՠ?V"fa"Xّ,nJVg>Y &^lZ)'ϞMMR@ (&{@Fz* \{˚í'ײ^` d"29\Fm&լ~xS_Q;-;B!~uF6I I\֑yz K8`T*Z y32%O?,AVD$:xJ_rZa®8n2K>ϋu^欬"9^M狦X)KQV^e(eJ+ o\[D;T_x?lUWDęj7GVp7ñoV琇hP`&87jl .L|;0!q⇁-YflDYE{x?LwsY5D}1ۃͽA5bU NKOx&WI|T{p cZ`I!LO:$b- kq;7^pALN |,`aa /Wwƌ]>j#-߾^S7L9mgdhgL:h>p=l@{p˨-( F 3 GlƮOD mlun'GmFEo/n>(0T:: 97oc{ߨo O&WdWqWLv7{^gtHD#(6΃1>ٽH,xՐ^M[A"H0s$t6Ic3a|x;oov-V7t5i3tLgjx/W'FC,%7[fѤF2/>%«7>qyEgnp9h%;cB;D-}.6Rؿ9AŦMI!?ڎ౏6ヱ[ ZisČ薼= ` kbZztM=:{"+N(/w!`Av B}Kwf̲9_:¾ݏYinYt$;yTwFf: atY,fSN&VL([v7\2-g". n ߗtbdڼ}cw= n l^x}l6s2B;8wpY]pAs_3L暙-jVbi.ebZhej)nQ14e_@gF>p{9{Ϝ{2);]C+ܷTHSoUQKUUViI+/_Fnh4q$~^hY%I}:_S%H)Bbgu ~R=van vOD`~LX7nV}j`;R8Ƒ\B!^9enR(&K@E(`F IDATCWz:fGqSTcr} ɮC+k·ٛ?$]`ߕ*.5-zsv3бg9ڱGwr.7-N^ ?nmq&`7\af?ot/ŽVw1 bFazF/{qkޕ|y1),QNgqX* ove /j>U.r'W.5Zy0Ɣ۝&7Sr4'ƅNZxkfn >}};Ҡ$;"N4s Iwٓ\sI5⧷"_rUC*B!N{KSoRg'UKSGS;^U">91xp; Zz7jun͊nkfŜs6nAU08hѕQd:my5ErI!빣XLLAw r4#f17G0yq~nVkhÔ?a裁T1rS8;mh4~u)>˖=܀TGs%'t'c_tZ}0x[qqtzen}iͤ/ߠKmwiNO/_I\.}ܗ G;VupuFӬi'g&e"Xo:O>A{P_>ڙ?l t)6qtwB`q?&~>*Lrihٿ#QE@( )sf6 |0eȗۍi BHRWGi1ߝ?Y-~)U9 wL9<{;')s>y0m^_bDgV RQjѨƳjYjT'XΠLSkw^Fuo_S`Z`io2*rrng含<\U͞徭;e\seh5]Z=Lyk2a] s}ɦkn]=gX'HD2W!>s[tv6#=F0e_\שL֔A /sc .ҽ_ʭfol;hegɚnYv;ߘ7,e$~ٟky #-OUmU|Hh?[^NrC!%)=1ZeM&i0_U<~V>C = żlq]ic8_JK)GVFr,zaw Js}1Fuf$5*wP.ʹ57_@ahf,:L /`[-H) Jch|Bm6@-\Tz[_MVsْrضӕQ>-/U*Ee(3UڨԄ"իb/[r@OPag6䇄DDzb}S}?GO_\޷gPKr6jkvKZ(Cja[sjkg%bnRnELF7]<(7+@7I5\wQՕ+[Ն"F ^0\hRlXsHC>jಷ;d%$$$$$$$q͟vVnA Zм΅4Nv hxwf<!ek_@RY^\C 櫫[;0>FسMj,\PW9ٹ|+ww_!ˎ^ gn}F-kcQ ڋ>o<ϖs0&MW&2Ӕpŝ/Ž,ꕹj=Խ4[•bH~Ypًrx-IKesA⊔+=b1"+kneu ?9xS^aGY:Za涓'e/31ϰ{xKAhܒx?1;Nf.sb3s}t`ԜU>y!k]u&EoBuכphx #<q!ɬ:pHԡuL{~}y4MG2:y/6\~obzN!B;[\kpƝG˓=]fjz u|}CA]m !ņ1$<_5wMx{>?UƷ1JB <2p!c@o7qyj6C&vB!B`?.n<xڸ了+M;(ꕹj.ċC˲zd+{Q8fKFT՗»@گ?r")Um-ŰIQf2ǫ%sFk;hʚsl?r"*>퇮p..f͍lEz^ 6Dҕ#NٚQN| zJ2wE 5ϱ dߖD)=gٻj{׸ z&RoDዹLz vyEħ&q23F~=^"!5K0~< ?ԿOpL ܥA!e?M`˘0qyN?畯އft&|j*&w֐ >KZTvQgdOvۛIkY}ֆ4pvLÿLNCZٙ'o}QH1͡U;4/w96pTVNe1957_RH%^nŔJ#)1%CI4pzY]~їp}D1O>hϔ#$ݑ~"ǖo$T"ۖ}h08QzePi$*TZii$*TZ2ZgFѤ\`\]%IW'[]CKY٘k$IB6.ViO9) R_ʼtoeݣ֭#fJXx7E摊1l;-?Ȝϱ Abq:w|Ĕ #^XRtB!DIv:h/#7Fb48?V?>55ki'\ݖիqqDKfIVc>Үq 5;wpIٛ\M+ wsLm*)Yl?f71_{YWi8w<E羉L|\(?sr A0ço֕xM{Ku]ꕍCڻXEz_bu΁L{!kQΤ{ۯ͸ lۏ 6s3 bҊ0O@yKϘZo-Xɘ ]<G?.ǾخGΩq$^}!Bw:Qᣔ1@Ϭ y@(Gǜ:$gϥt ,ee"]١+TήYM|۾sG^YG+۾?4.᏶O$ ;3ǯTף;RםR).r݀9@LoX;:+=711_{Mc)QyʅzbUM;B_}' ove]UIGqSLf=ɃO& KɮCVҭ\M?~ìObdߎ40΃rp?t?ʹ+* @Cn?Zuwٓ\s]k/r)9|ݓsoKx^?1z\7Ns%!BbrdC ׹=+fi3y7 M?m7iWU"bȵm͐g&vk#ӊB3/+Df$HZ˪†WlıhFbtGo ;aІ)GbСՕqw9 Jե.[^|p^R際/fQ[fV27_bh?u!k)[qLL*t:sJO̿av}8XVW~SKvT1F2بf vAyTÍh4*1ϻ *Ԙ{TW+ѨR]9w W] WK˼pP]W?TFQE]{5fwGTѨ(utb5!$@jrK ])o9۞_lPzؗlK봤M$͒zef|@gO+'+5'->3BOq|/04lG[layےv҂Vy\?㎎"j]jz+ý-t%$lC5`P_/!6Y5[}=g6䧄t:Uzn:y !!!S5_۪NWU0VUuZR1rz@#$$$$$$$$lp޹^(⡦qƯ(>x{|سlq2?[GTdB!LHB!B!]zb-^!B!B2tB!B!MHB!B!lNC0F#|#`C9Y߬QfŽB!BZ04 aCd(f`B=RB!⁧'m ֆw}{BC!B!@!ף=z+x/шxKX%t`ԜU>y!kS:Q\R{bJגиdV8h$:=?uᅬ.lž<_Ϧ#Qwd#^lN9楚 6Ti$*IeP$u!z_ZV>ϊFL97'|ti!16}B!!?6CQLʸ )۟v$Y6cp-隽>MdEboM}T΁L{w|y6Ύ}B!B:8&nFfm[8| "hγ'kθ[\kOKE_uuVs'~~cɮCw9+;tfZ}B!B!ohٿ#QE@( %}(V=FiSؤ .Z'{/hKj+Mi'V{P_>ڙ?l͊97m ݂`pТ+M/2}tݟDѭ7vZ}B!B!fߓ /yƐ 0w}{2ӏTBFl6.boH+ K3.3i~+j1]9Ȳٓ98-*Ĺ0L ,ڑo85!ߟ5y2f{fF8at/!_}̜|B!Ba6M֛+w*Q>ϳn;@B!B[zbCxŃКN_ _J봸TiCa'A!B!CB:JB2ǧlĹ(NmJs3Ӄ$J!B!xHB!B!(vxB!B!lF:B!Ba B!BN!B!B؄t:!B!&A!B!6!B!B! tB!B!MHB!B!lB:B!Ba B!BN!B!B؄t:!B!&A!B!6!B!B! tB!B!MHB!B!lB:B!BaF !B<.yx<QIׄ#F<1o<ɗH!B!B؄t:!B!&nw:m |# qY&%u>M[E&"e@Qށ!?m\M0Pe|QGzZlGa+I* [B{RPHRIDATOٷ`,!-kQڴ_m@kq$" Ȫ-^(U %uaĬm\2=ml1u)BjS}.!1i^35 .md_sl-k^c_-'.to?#>O`͚3.3(9ص k0an%YÄ5kxgIu.;7 F h^9 ³J!'C1zOmY}Qgsn/fc_ \d^frsu-=|(6J5gI#"O~=&/h﨣Z%~/ӿ4p-°_\?w 'EpdlϾx1mߎs.Jg\(Bq79s<;wQdj$-Þ{6&B!JC8-M[x6\Ry.q:S~o>ڎ%Z͕#4 "w9pcvfP]}iޱUjgKE&%hY'ЖK\۪8>%:FOʸё6֔Hy~yeV#iWVG6 5*ZDk bS'uN${jwu1m vt;ߞs=0zR+rOXryڂ%(s|vc743:47su$K֟r"/ >45h\`>{>d{dVGNF`To2hL3,G,Pqy ߷J@%b*jrFLCnJ)}{{r ؝n-s()D8FՂd~]$Pʡ)N% ^Nw@t3daVS|$?9-'qf:,?g9 Wrr*" ^q_ŨQr){w{$s͞X3 9}7f g镀,^.8q)eFr!_<ϵ]I[?:U(a)FӬiE, tOaLX_v+".|7kt_oA٢gΒ4{gw&s~P隸r${8s4{iTVa?o:WON`/*Թϴ̤aoǹ^`/yǯ=$\]η슊%5=ب]|l !يve?涧6f`IbfOz?o|6|iQ.2w{9} {as5,Ǣ+>j`&lX?yL'p*ǏW8V݌KQ:PR8B> _bI[v<ѧR=2Nek}lj<>m"4΅J 1qZ01B9_yS깙=.:+5ߦ˙.r&g^4q}}cpCz#<[Niӌ-f ?ZaFOP?0q>^?j/T4I>3ޞ*GJ9Yx\đ`gV=ǭQs ~Ák7#&?>HFn˻8͂zjyy@[3{9MrfoR{/{ҮkJ5Xn3txy&+wuSxrvn* _s{XC`P~4?8FdT$Ƕ/2~+7%r Ec #箘u|w{ZRvΩq s7[ޟ5soKx\=gW-͢~b+uV'խAs&ZMEA7MVCge]>9-Bq$)+ԯ$f<΄cm-nJ\@JDFxUjk{m|:ylHƉO`W_$#*1rpcJ?3.q\el3]싼&(bhIi|Fm/C 8; 2,۾盏㾋Y?-MKW^o(qhThci9aL_ aAbk0RMZb`ݝR4{sm Gղ׼H8x}͋lu)2&tf(՜Ҏ}|tE`ʽi+݌XRtOZǛӶy@!I=_|bEsVdŻ@ "Bq5 pM[\{}4zF"s Rqek]O)9eSݍ͊Glj_pm˄dF38:T.gYMĔ ñs7e|;gmy%iֆ[{z;h^/` )cAf/J'͔Dg2ί(n'J뒸|RhOKX}kej஻79_["~*ngj™?Za&d>C(= -B|g&dK)o-JEg:>ͺH9*|%ࢃ)$ځC)2s 4 [Q&c7<8ug eʺjWe+$rxYOl냶*^e詬9|47/xlvm7Ff[uRm㕐fD d"j8~5Gᄑٳv ۉ+$Rq>z7"b@?g]HAFZ·x|?F8T}ҰbZzjjDYkG68y ?=DʚzI|6LrrTuMh9[xd%z(7l7Fe,S2Wc߂ҽoI!t4ߎ_!1-g63g4nՇf/KB0u\F Քs;4b%n{|k\,'dLiqXs1͊ԖX!f3<]Ǜw;_Z"͒C3O7v+#z^C曬wIBv^d>!Q3`kl+ɜ@27[!s"=r*x 6]̬zM#;o߾&݂@=T5sGc\Q S:ܒDLU߇!2^^7"O&!<nNaGp=N}o|kwN3hTWYrQ]o[-^5 f}6|€VuF!̻؅ W ȂgPۏ'd Oý"s%gWl˜bR˃> [hVJcE=Ry)ozGj=J U8TcЧb(^]`}5x~4֘gDO)-ânJ5j41w[\R8ʕ7?]q4(eoM}mx'dtp0WC~ґo45w<g]u' ~ZFyb"&2Hkįr: eúw}Aio5k,I38\aƗJ"/Y H- @y"N}_wK Z k} &iPq;f„zɐ|x:[&XPCĿޣl,d\MŇZqTZjYwzȊJyB܈#7oK>~]>iҳN M:%q3D},k~)F?EImesr6g5BPt|m+'^x 6xl'8?o4YrGCٳ*Wmm 4c?(Bl,>q7YěRB B9{N!B!BM^/IENDB`transmission-remote-cli-1.7.0/screenshots/screenshot-maincompact-v1.3.png000066400000000000000000001643651233471375700265420ustar00rootroot00000000000000PNG  IHDR [IsLbKGD pHYs  tIME 5RIiTXtCommentCreated with GIMPd.e IDATxw\TWg&hbhL1i&)M745h{@DQ@^fys! >'џ {ι)saDDDDDDDtsלDDDDDDDwfe` Ȫ / obp.`RpkH5*I> Z*4"u`P 1`mzb\f nC`EGj!oxV!_oEZ(\^<8WV7_t3meS}? -=}.CɆ:ҖΆXCSހbNwǖ[m-_LH]"Gw?h$ҐոNjunRw_ϝ a0橼xUSwAW'BxaqK>MDĿ,>S_<E\b6#0Jj`''1mװH7ьp-jQ{4\<>{ ?`~[}ݮ-&Mp [7 گN>"|Atf1ߝ9ۗBȾ1]a;>/÷wGq!npwBnv9chG<'D>kKVt_H9iwȭx:᏾xjKx+?:\QlN@,z7 D{sҵ='c."vO-ZWoioĢN?/Xр84BHGPf%N# o?L^^mn<,z۾‰BI_ޮ) 0$)\츰ԾxhC>!g) `! Fq1dt㋟Yu)IGf>m[w@hY*צ 03 0x},KD| 6ti*>gva}<a0qf~OM٦ Ģ|t4xEo̜$|uyxR ? BCxn TYjKSń6䳍as}6`|rHF3H-l\v 3|@Ecs!/"}V/O!߁ /"ۤ<ٶ1&`.-Zm!_IPzbkgmx?Vfkpl2.[MVgr ob/$#5<캊ϕS_x$_LB± X8= I+m6&[9_-C W#qƙEFkUl&$}8t B|> ._L'Cx%ln#/N SZ܃vCȽ:m=^326< o&?6ԅ :YrV6峍ak}9U,^S!R͘\_Dxo(F/y$"Ө2=z Y^vKkq9~4\Oǜ0|خ_ }9'NDAW ǡoװ'>U-ȋߋj cH+Qjgl?۟l~޸]cgFLY~ bS`\Y{z(N>tu\Ql}^M=\ެ+z5k wg4 =#BbPaG l ogtĤWE3qJ.%# z{ V.7nV1;Egk E^"2նٺ]5gӅ(H.f]A8gT6EO d_mW(õ2"ּ={3}+T?6䳭REZ}k//Pb*BƉ՘Jx-Fupwu,e(3[quXRE|f>&~nh_}x#MutU][￶҇axqfdنkm#T_W牨a\uWVၮc]Fb\saQ\ّϊ[g\r;i}1o㑮⎦`;pz jReEx/uTsmꕣ":dkyԗ]uݭC)08 Nrװf09"_lymԍSnFkԗr8}s8Ũ Sw$B|@{C{̽X2{ݐ) '\ѻ[ (+C ;8vҷyGHMI嗪oazp}#uUzlX#ngvuҷ'edxlTnQ/xpi=]y݂b~4ƶvWfq8~y[667ұ[p“a~4ƶžGx'w{L'&ݟ"!9z2ў7hĩ'v[S"Q!{c[qU_[jqu*ϱl?UY`WR䦦݆|%#_;7}_cNE\m :2^2O(t*#'4'-婂ơk(upUw1ۄn~88\Z@TɎW+ Ir5]?9Q!yZ0ّϿ}b]]gp,3u,2^)"UydXqCQ_vruұ;"r?|'k/{ﱾ]cwl Pر߭r8}}s[y!0 |7R^pEs&Тsr./|(6l=͍dGgQط`Huu?䗊]g 0qEUA.u^t&ukm5Ej;'-E[ՇWl> CjM)_VSҺeĶe30ܸ"Dyxkk*^jצĈ-[aZL>зF}1ïPsGްZnWuy [ᰦ;&61$"~Y!vGSX* ,98r@smY9zQQ"vDŽ;Rpv7.tGKڦqK2̀WE=(N:\1)|VU*z|u^΍`,ŵ208Qyԋd WO} =o~J/O]fXx> gO @ڇþOW]XYޞ alY ǫE{bͼH A䈿Phw)Oɶ3^sAXOSSuaAFx'G_֑1zwfG*)0"qߐzl|m,:UZz!=[{Mom ڏ |O;x~iq8yhY1a|Gtռv}xA!?QXzykjJf}~:R£Nbj4BC=m,OI{~<01Kv xΛlg[땍sh/jv5l7//M =:9\i] ]+7C+tᚏKGʹh6rz3wߘj`h̅)|3э+yx(c0C3x:k/DDD Y^ &OKGlC8/x}zϧr 1{,"S i9'6.FDDDDDt7,ǘ1cq#$ """""3)>(=L?Y7<ԩHpV/J`)N ,6I<)>/š ,?Hn))&)%1RP?i\rQ_e sy\Q)loB~ T(_4W^*Է!Q닯s{q;s~"""""BDDDDDX>~/a2)\vVd;s%>ٟn@n).jxC`jD"&L(Y{+-A)0 l0 =;U IAvP90Ym~(~9*|+B` %).Uק/B||8/ ;R'N ;5}reO-\0yzW;%E%'Iqc)<]|X%~lc9="R, K!?URbyFLcK_ݿn@DDDDDDDH@DDDDDDD4 h゘DDDDDT 'T㇪׍Y`1j}@$""""|kD6Q y{X^w\^;U[Jv)v$*gby3RpJqR/W cBx!"""$]WvHI>Qh`X{@@Ftf;8 [/cZ/t0!76-[d &ǢΘ:i2潧Z.[IbkQE#'oM`_C7 6Eɞұ᭙`S#@,̜y[Jڒ YGWaU5~Jc.L6.a)Δwy"CyQ`4lՍY`1c4XG pA J{b 󅈈0$p|!""""""5n65KcnQ,P3p D"""""""*Wp/%Mpm۶kqqqbz℉qs{d ?"{3RWb**~glJ~wt˹* b:n;vP_B!u\GlNO/>{j(+#G4x|Ub)s9~wjF#'9g_)/#z皟78ShѕB|>K1 qwBg&uFU׫$!(ЈCXyj;GO&^y,LGL2:g Q]ȋԈԍÃBa0{2\Q1/Jqc_5k[{]dUնJu,7_E`0 σxcu<-<- W42Oo|d ,y_x}ZBsFpvDDDDDTkŧ`yKwbQ\I eBں^xy]0MU+-h)8w||ax;>美=wVS3^Y:k :3)_wҺiIbIBܯg=*W-btUT7(uܷ{x q5O]'44T+W,Dx$;abjܿT!v7*\C{wqf~c3ţ}2/UxFN>P-BܿS-oFX?F#s7!2JFBrYE q2{ؽ?R\2IyIwBTU{~X{IϺ닯XW.RRox>]*t~ٹ*Bq=ЈBbch󭝷x8O^]<s%W<#sw<ԫkx?TmWźwh R%9K .hIwx|\~[ T#U Z?˞j"v!Uz:wbeLy- }?<s~Ž|Hﯢ#=*$xtt`+ZMYw`pih\ <pe&\*FZ!,"""""c&s}J#L#9GF|fE ȕҍuӮݑ7G|R_ +x;rΣȢH +܈N ef 4 ᘅvÄ \ %"PVWxOXo0`0 h:bWm(pi...aϱȩf~jOߊq]]@_9/54! q@q9iBrM89 jJ'XEqLnY%W3H[.>G= Bo!.Nb'y Eq_p8fUx%W\2{_qɪ3Top8F7"ܮ)LtC%ΑѺe uԈ޽8&q̭bx֙ r8[y1E8'-O*c!Mx?w 'η^5oVAUI[ZckSw.*Ǽ{UZc6mHyo_!>}T<_thg Yw!wQ8ʵoS_1{JלY+~#xBc@sRu@`1\W:~Y33%Bs|%Ym8#5{&~~;B=OU돸A1m:s;uXݚox&-{8'5Feh $S|[KF0TJ[ bvn J^}>qQRTW eVNi]AΈ’2-Ĝeayrh[Wb^ N>}CʻK'UctΆLCJ/>N=Xfm, cԊzԈ2q㙜ODDDDD @u$ `~|ߟ b^"7:5±ZZO C+x׫j ;Y`DDDDD@-ѿ}lM4a)["CE76ӱyw,{v1N׺sTaܱmruW B?N) s=:X,93g.@l1 y!zփ,$#$[쏁]7zB~x E4{Ta_ǀw6tF!L۵)冂 ת gǬ!߶mbytM:S멲o Ej_o٢O|3k,~}OZ-ۉsl;+ quqL_z8X 1JҠB8°y)5g7,]Rbt=")A?S"5Q\&^Dח G~B{\"|@!?|%^Gx=X]<xtNb~1&>5֗`qN+p*A~xtwK1?Ú祉sJF"""""9O@uǜ3aj̅)|Q=RtSW/fGi85> w]Dv`000f̘q$ȋDZtQn+Rw[y|Tޕ>R/R|F[JҺyR^̡TII݌#E)()6+YR ۤxK"[V<CIq#)*Cy!""" jW).Px by SάU C;Uy o~L8:&`X{4 :M϶ϱ'QCSVƄJ i0.\GC|;{i3`:Mϱ@wTz D&u\49ZW # p, q0&Eh#BVgپ]65t@ q/aO| (kP @d@6Mג @]bΊŕ$8&d)ǸKR,!o2) O^1A[c܇YWs/Rez +?KD)>(qRB~{K%Eag5/׉&"""9ъgj_ -ҿG*oϢS@5->܋5胉OȢ2)7z[+J."0c^_7k֢<HOvٓ,uzC j@`cm(U-\ 2NXf|ή&l. K5caE0FV{ (1Y_ŵqXRėj/馒*9.=۷F"""""MtiWMAECAۮaQVmŕ\Nzo+UJ`kz*+I@Q<[/)WZs[!>#q?)˭fj^썙O*?9Yf8@qr<2:aYG?ŵLOvٓqc82~Y|JAߧGImHܴУh#:C,ܔbUzoיNDDDDD`7ڣ Vh3Ny`V`}Rig* *- OV"""""9|.?\Q߫~8Q!7)%g:5FGGS WẃqN"""""n<_`r88f:h5e1ށeSfׄZyC$ڽoMNDDDDD E( %>wG ZhLCήWV^c%Xd 0q8W\@DDDDDD ;ކutwfߛem(X<"j8y. ܍L5)Aؽu7B׼>Ma̛ ąѰ0U&a'8)^Rb+-5Ku)So)((S8UlǛWs9cmޗFJ `C 4,5TP`3VIV`=mM(_3,Nُݫ>jBF""""""L['TL,RDHj* :St5@7Y nZg:7pW7 """""_|ޘiN󩭪80~n 2*,ł3+bNpo4զWYMGnF,eDDDDDD 'wPCA~L[qJW6಩>A_iѬ#FOa;R+ [h_@X/)["12R,)zyLvW9)P|I)*)T)9yODDDDTg9!RH[JA!:k(GAk $l=c&i!Sشl-~>%И S, ri$Ph$ȑb?<7q#AA /* BU6HksFMm#AFnxRҍF6= Å{!53Ao?H"""""788==s&lĒ&"""""""@Uٔ`4RE ?or{vzIqG(<JODDDDDDJ]DDDDD@2 j  8' '9%Iq?,f).G!!R+SX)""""Rx_+I ?g@0Hcѩk ?Qŧ1z7!1/v)j g11:!8_XX IDAT{C x6gV$ODDDDD E: YQ+kCAzh e0pb6[kvvVYJ_dWwg&GB-0xH$z#b`gaąX`Vتj f8,}})K֗tSI4f ۶zQNl%xc.DK!Nv6EBg;Q/m7a]&[Ү]tZEA. 7Im0 rնbB\pk KA"b>~ 1ΟK5H0J1#R[%DR%R}R,CL(RO)vR).`)Δb/Dx+OOPs%>="""">)W&Ǥx{I/0_fX?+ϒOD?ಚ{c&Ⓤ0~@ξPLy>Np‘-@h™#Wk y7 ܂ڡ4Uʛ V"""""""2~Y|JAߧGImHܴУh#:C,ܔX;dEB$lދk0`p!hʞ=Hͺa:@gd슞^:@DDDDDDTo *M^EK1ud{Oӵl]Jkʼnpy7 d!nq5\= _.Y=W˄+ 4unf= oCɞL: Rhq,̜N`ĕbo>W]>csa <9; 7Cf=ƌ cݷURl$ """""""$8 """"RDj'ܓ>R;)x^CnoM *+QC?GV}yRTe}Oy+ V_~o 秣wzzMI .RO'Q|(#"" |' p/s xbcs!?Fx1ŶvT:k7@`mq]޸:qu0^M5+hj?{EcW$ kgxޭ,*-@l1 J\ e/7`Ğd =m5V_pkY,^R_?K)\OsHDw /G Hͽ>+'ʟ8QMbKmpC舁h"9dbB8"|rŪ ӧ Kض%׋۶}GsQr_9q!QC7/~`D}di|?+ zM- 2D kIDNezZ/4dXa7\C]P8j1v1QCk]j,Q@5->܋5胉OHm7z@rbY{x7U{RRzyhB3&^8f-\(ʌǁ n3©QC7ab,MV= pu28i`1]OQ' !hꍈSW8T)AXge4m.?@h;DӣUI! 201v%R,aTKe)n!š 'Og)O?"A 'Lj^b_))|^3Bs,7>P6 oQ|5 icԻ*?yy;Pȯ2Qxow}Q?*kP|Js`KKXjG%1?Rz*|}RHE5nm mXZP^mR@j((Q`;3JLtNЖa맡`*R|Ώ`?_IIGAQ[ϒOD?ಚ{c&Ⓤ!ZHξPLy>Nd ,N6#[P;4w548Y; eDDDDDD +!̗ӟ6$/O4i'2Gԣ)GtYع)Q71~RýY7LCX\1XǠ{/ܴh@yxT?d2d' B !0ز (bPh}}_[mR "(E]=! !}ΐ''纸ȝ9y,s=ϲ5%/C$7PgTY"}e 9Ϸ()O~мS|R~!"}jOHrDIJO|׃5FѬ̬]?XM+GF!f}ttܿn㫉Ѭfn|)5׫9gj7<5wR 4[Yҫ?/9!"j-r59Qz*geKfK`3̋r"A`W_YłXPqֿky`o yG{_Jk~xѭFzC1lc,N؀ g @ϱpEKp&SU-eJ'վ%{i.DDDDDDD&[QJ6W`Pw r'Nl۱݀Z'j4v7 """""""LQ5%rpG8Qrf9ozװ<ݣE|Qr^Q9oql-b9tf: qdzf&bݼuyrlMӜwQK"N_9^Fk6xi_yڠ9?xdXs]oE\9rtS & bo&ofyDDDw=$gE|~Nڡ<4[PsQtLw;ƍD`x'*|+{o?y<% ,zz!Jkk`LgސkQKR`V/G;^OhL}V>W{oGyu Y9hfO.`s@x+8XF{N8Jq8_Wi50@{!nh yI(jdGGj" ~#D䰑JQKk }.DqsZ7D.r| IyRݢZ(oLCncx3aMxoV4MEd[÷Qmuıp"m=>Wr5`p:W MYm#N8EĭDy=KS?GdXlsj1 .>]4'_}hp9wTF$} '_}CD|1hVY^O@0Jɮ/9@[8UE|@S~k1X}Fo2HiuޓO^zKS߾rLw|y}1xi;np_sx\4"+"ο+"%T4kk>1_.hRǍ r.A6ּ1Fb,=*'6Yy(mYeT4"E3`ٮx`'_61-<#}7q-,Չ+}j+`IN(pO{0?@ںx5=g@wKV|YZ XycxK1_ O """""j\`o|`PU5 AtK (G׏|^c81#lN1M|b벣M}nC浚X0yƉJp\8R2E6߅ȷ91@DDDDD̀b9u0 L{q*f/=['A0`b?X?_O)Uq !9"~@t3 2mCZuiIODDDDDx_ {k0)$ `oQw^=goͪI,y/W,YZ7Y_L?vJqw)X5I6;!"""""jjp.Qpno#srd wVo&[QJq5M"}CYEDDͦ_PXV3&N"cgos D"""""""DDDa8n=5I>{YDDDDhiiX tHdј5kӯ ]VqJ\ēƏRbݤr߬Z^oytU-;Tr՟z'w(9$ /`#gk-fZ5|k)O5ޫ\^~ ' """"jxL]ٽ|-ߣįB7[oVz/#Jy]J|0D_Vy X8_ !9oiBg1X,[(_6o=_ 幽pPœ-2 +Yދ:~v.Wسq 2**u1vμ{߅Yӆ!:ЂR|/ץO>6~G>Ƃ?B_/{+˚!r+f[4g=ʃ Gf7,-nFQTI\T9QQ|'bżXm,m"/;T\JE1hciuıpZǮVbEDDDDDͯ4{3ZcDk3޸V&T鍼@ʞp,6»fmw! ¢`.#G?^NlQ|MA ;_`-:by/|y ַۿ @ygwwOle<0qZ⅊Cd1rKc~$ɏ $d:(O=ק[%"p F } vTl*O&;K#Qit9GyPT ' բ wW׏ U6ӿ{#Kh`y.9c[⣭Ȫ'!N:EXcdXvshxfJq/XY ~ Qq5w+W?2Y.w(v)q39_q`ǭ% 3ҮөS էNfgGr ]S4u[}fg f DDRN;<y=g;@33L%{v<'nG~h<,WBGp)茣B=FsFu"QPݢl(`q(L}1*J<]7Yyn灔UK)ӰąQ"""""!kM[.[\z& %ؿ7ڿrFE=_=Rem Ρ[~nJΎGJN9H$ bmyVp?e hHy& lKxkb,~s^ј|Y& """""5w E^USZӊ੟XY.䋘 LvP{ˀOگΪr_{7`]Yާ= F/W S,ݪ @2uS0qI%s] F˳T ,8$'`ŋx o,;s{zNN` P0I7D=[ kbܢԻ"4-r ;AH:V85yJB"˾tEyAO"TMn\NB2º@yx7x`.'& 9 _4!w A^A7'<ߝy MV;|T\ء%CmYJJι.G_zlMݫr|1DDDD&?osv/8_ęyJbيGހ[ݬ3JY%SOsJ|ԻC5w_Tx*q.{Ϊo֚뎡}\e܁ i16lL9E\\Q'_`R>ْnH[ ~ɂ&H8^ۅ@waUGo6lNXoGO5Yqdx'߷5 ݉(6&"""" Ȍo| OM]S06yًX:y~$Z ~B?E~mlDB^R&JnU/NIJ%WCSj2rj_3Z ~xH T[??nigc"C_!N\aAˋG`ø#r"ߺ ͊:C0r=;-U_w h#Q#ժݺu SnW%e*ڌ(ͩ }G}ntg%񨷼c UD'V;vsw_mZQo_yQT%>X z^ݯc#âx0DkVmB;*qjz՟(MތʃөXxjIJn@ zy놼`3(DDDDDtcċOoAaqe+-u/?CBah$A`cO܊- ڣW7 ȵ]I"""""1xOy1~' jZ< RTϢ3ҐO !h7"p QvG3PQ[6!Odb݋S1{: ϯŅ[l{qx1/8+DDDDDtS "0믿A[ 8+_,ĠSŽ$F?1(e ]>ظ(bw"p~Y2 ϰ*H܅ۣpȼU n`IU:\9(qR7@{vLDDDD7xL]ٽ|Xt a%8aĉ-b;V~6DDDDDDDc]?ODtp)شqSN:ƎQb'&dvTS*,%9F7t QiJ5\O)qXD%v>K兇GDeq!& wj1'2rKWMyΫQ:{+=o>SqQ/Hv>|#gnGW ՟G  cw"""""""$]#>U.pvø}b} ޤ [c\Ӯʼ|`HLBBB5h^Z*? &w9gx1/}DDDDDt@gO`Tksy ?ت `l,ٜG=b[k$F7Oo)jfͻ!uQ׬/lӣ:_˥L%.T 4}x<rۥ0O)S8S7uN٬mU☮YJ|)M^lSW~9J\yG%>O. YJڿZ_PJ9\#un%U?Ի=?nToyclݛĹyg%Z?"""_P}콺Dn 312<7vMD_xq%3roqH_>Ok!4yz?CZWjeBm2u|H2r V~7lLj-0lIͳ)5Z@ DDDDDtU?`[X"4|"C_ ‚IqGEXuޛ -=#g߃HYux& FB1Oohk{SZH)؛}:Xy9 xkb+(0bn<*& fP/>=a?Ɖ&*jT#>GxL AHHBB8ܺ y|urI=.Twإĝ?%jEi甸ЦZNfz̖)qtDSܱmJ[V_Ǩä^u[o߫ T8pU뻇Zޅ@3&AQ~:\}w>gO')qXHg%-R~%"ke?OXD^J|mN$ """WEsz_v|}3y<z2NA"dRwWA>eǨ{1nRJZ_.EJx:FبQ>n/\qXfO.X ($Yz٤*X\9aK"""""A`.nQxoO'Xse8Zʺ0썈]$jQ="{K=U`2@lsLэ!'{r6:Op wzb{W0Ė /MgLy![hJp\̝ %LM˂|?1Enzx8Y\޲ )e} ^(Ht&?*naXxH\Zxnv2B4Ɏ=xJ)W׾}:Bcsy%6p)oڧWn%.⭄1ѡk"={}~uP/_T7J|X8P}F!3& SV q18)Gy kYʋ-Z=>_F#"""Wf*v\T⿏]O xkWٹ&%V-G9Jo ~2QI1X}>qB-I}*nB򫸷T>0AO N`=+M4{J13E|}aquP㫭̒QxUD.LlG歚 n$lxt6iL}gt5 \z(N}Yص&oVR.]MzJ9cxD'$$'X۷HKxLy;]h ؂.h~""""""j6W[ ORV3ZRK1 """""""`j$(X"6:Q/8AA")/CGDZđ" ">+",piN5]Exf^wJs5啊x[x~E|LS=D'"q[D-E^reE_4gҜO߉8Zķhui^^O\ķxM_﷊X|NY9&B|~=+yT4x@5DM_mEkwҮ|3V]  ' 8 `MXG?ֽ¬ihAy lb)ܗf-aܒ """""jj'ɄZ4 x,υp~ILwk4%~V>P'wELVt;mQ񻑞 (F[ f+$QK7wg0UbaީOy`ќ3EcxhwלFi*iWs}vzoќ?".L$5CWƘR}OߝXxo 0mQW, _K 4Eyޗ- ZGɇ^}[v5hTϞ(hT3]J.&Tl\{#hhy _& ZG-d'ފ`$9T7Al v : /DDDDDD-M>nVOCuA&Qеwp`^f Zu&>}<˲c`yr"jwլep1<ݲA'bmD,(٧X }e /f{eڦ:3˓ydigWAYr^њoVS^S >$S?N\?qKOotG1Us|đ/nw;kD, x@Sz/C"&!fh/__yBNakEy-0d*\0`_Ĥb @x+m8$'"K>X0y ,zz!JYeK"""""*h adW h%8fd=EW dcwꌻt5}]׉Z~[4>A`W_YłXPqֿkʚ.$QK `S=1Vl@z3U {Xs% X8wnװL*0 ㎧Ѩͽ~]J719Ou>%tw<9S"~q y"+4Z-3tĉ[vm6B!""""""";& $ """"MMA>њO<\pj3Vu|Gt8]=ݧh5erfևno|5t?/Z*9FME\."?UODDDDDDDvKV@:XVX>ao cxwaִa<6_FfVt DcڰhfǶ=(dcH P}ׯ—O!<0Z3zt3׾!'F#?1'1!ܭYaSx|LW`[xPF= uFc()^ICto0))-S _.b9v?qwlSoi>O웓,Pwq}""tsGU[~{4Y?FMuqfk'GIJt7^1i53.c]:xO_ w1 @tT5 +:(ƮH/Lv;o=C9 yl.ؐa#ΏFc(P|qf"opv䃈6TCnue"R@(`2}#X1/p1."sAť$\6} q X¢Ѧ MV?X5|k-$QK#;Jx q%>5ZlM} C<` z7FT" ec?-Q%QP$QU)bOػ: %ωDdbBfUn|T>\ #GŠ/`puFy.vf; m]<ΖּjD,Kc1dqݘ{X {9D^戈:*q35WrL5/s`IV]u"QA訁m(`q(L}1*J<>kUoKq'6䦤GyqC9/ƒbѷirDDDDDD-'8{.`5D^$j]{G@<6x@:VB;C|ulx/r#O|IlO܃h Ptx^w2n@DDDDDҸ ·O E'i2w.y/BbR139P,տ5/gm` vrFVF<>OlM $ """"""rv_=Q7Qi U-یLxؾ B=ll&1e D{]L,X\3Ʌ ӟh ,ş߆rmYG^e~>7X9,b\nCJm4'q[I"lWS5Ay"?Yq>Q=4^2m9A~bx#"""H~DE\ߕc%x،5Hؕ%ߗb{/T\:-ŚXf!Ľ9'_`[N+;cpÒWY[jpK ^+I@DDDDDD-7~O$ bXZV}c$`ܹ/SU-˹r$Q8jI _lp~}4{\8U|&^P'[ l!""""j'.󇻈[56I@DDDDDt+c{Fcw"""""""$UnPr͑">&pGXQn5+➚;X+%bG b9tGkk)_Go{k__M"nr&  IDAT7W8F<y~J֪:XVX>l oCtN` PT2Mw1F2 8y`֬ڄ<0ZJԴ7W'`fVӍȤTs*2OK/M@Py *&1er,.mo,MFf:4 3#:ODDDDDD-MOOg[Wq&c&-M7:#|0(I؄3տ.>x+@z^)leqrz$0"V {& Z)P dVчș~&7+֛4Q`NY9p:(>)5S E>2!">,bǿLħE"1% c[/bjʯԬ}4+h'lnOL3]쬈hE|YW(P`K3Twc 0x0g T5h{s18+~;Ӝ(p{XyH߈)euc\!V^+I@DDDDDD>e'^0 jOR ߊ)yI>_@Yunx 8n2msRoΣjZ*II" @ LDѮʼnU}6%Oىs.‚;G P&lũؾr=aB؈5kDDDDDD-M޽AވA&Q#N>UgAO[9j^B}˩ޭ]k\lg:XVH E,cqe"M6 D.b9oi1zGz[8>M3DDDDDF'/ռ.b5-^.+Yu^NtY~aǸtUOh1}da놤:#.^lT^nuVVA$ 2ũlB<އ&o?fWx fE#qF7f%9$mV#=~7^ayG{_ٍFw 8^\t8`io~KLj w9gX>sLT6=Փ( SG7ۑYrJ'm`R>A떎zK*Gα-M*0 ㎧cJS`PO?9 2q+?{PȎI"""""""1 a{FS{%5%\>"#iQIJmHoܟEN755XZ}9~/qӝ<_orx "_%s^nJ-v4:믩M}}7<{Ο&ͼ}7@~NxM"9Onnܿϋ."> FWe}q/ć]jv7 """"""" """""j8ZaoY }N3@\@|? @iql[1ܓ*ۏ/O p^Lw1F2 8y`֬ڄ5MpkZ """""jy̰wfVgA O1]oᅧǒC3To&s̞Ek4LEپOOWOm(BͯGKTCm}Q\~"'" b}eD,XXv[TħEyr \cfo7MM_f{wBŚ+")5C~"DkM>1%di4Ys|Ϻn5YIJf _MwgW\hPO5'OqלF$K:05"⁚C3Oz\?iOKg痮ڊ,]$ ů*1`Xj-b%s[ hhs,_"b[;+tf:4[y=U&ɬ;Jd/Z!*ⓚh^}TuiC}dTk"׼Us#LIJl;4\~M~F7[?}45ח*<hoNf}en?S&>^5Yk4燎;d|px6z/jOŚ&B?0z|~Tq\+5ǫ۫;?noS^Vh ojof}h5G󠣈~ר"n?*Yk>37p0J^47KC4_(X+ajpM  ?pӷsHrSQ h#-k7ZγzYf>WϏYs*#d>i2%i+bH o9Orf^_'O.ϮǼEim#^"mO7*eMi|'TF7wR~m%gx6z>?:F;tdzOG^ou}c5{]GB?Dsм\zvDrL&ioph}&:IX1C1n6>7S|oD- J@i f ƒR܂m9ͷ %9?/1=5/VbTe,6B!"""" FVExhTO.LlGfeU#e˿-jýNʑs|x|)I"""""""m dDDDDDDD0يR|,ADDDDD|}ɱ>gL8Elfw"""""jaߞ݀0I@DDDDDDDjHWHy=E,My4kWrb9Ek弽~"(InKrgݼr :="g{8Dĩ\y%ͬ)cqi'xci22K<ԤM /YF.m0OcjD9/^ٚ`St[^z{y% X8w\^Zv7 """""jij{g3d56mnr#wr Vv'GSX, a&eg2Kk+RºS;‡CM8S$O(`28"g.ެhXMMSȁ@H)5X@9l?P Cnwt?aofy&(q/ؾ$j^S@ {d"}Z)"N;/LD랚?ft|@c +e!'gt)˗c@zk_S"-֚)ox!""" ;9Woh;ʛ-^.+Yu^Nt_Q.^΂lȯ]p^y PT ǎLu3tŘ>uCk[z"""""*hDA&ֽ8 Z\0XG {n +\0xxHۺe+OcF= ۶!M c @ؖx]׉?{_U}qu`$٫"8*WNhkZ֢Һօd 3 I@q|rr#{='|w3zxZb/Gsw3=3@e>yC_`f5<ˍ|wyd4P~m _KࠚOs- TI """""^˼4QQ]]HUZw5;%ެJ,M|4-A<|' lsԔe|w*T <܀g^ڣϿW#"""r)i8&4NԩSq|jI B0p%g 1w+iDDDDDJpIݏyD%ANTRCDDDDDڇ4%7̅-+y)"""""""`ռyϷ,0|E/+-qrri¼ܿAI}ZZĔ%7YPo7wa]e_PKwg?S.vO>͛-q)8}K\Nkז< Ν%WX_pdKUŖZdٶOd?\ܚ~ĎMoꤋZu~聽%-LΊ\Kyg%jK&0v'ףVKTk~L\i-),qRmU,{׫GYˢy!zC'Z%88W^DDDK'[/Vj3=y:V@tt4 cfֹM*CFN=Kk`?u/㦽@K|V̥sxEM1r k(0v]d\gq|p.-;cK?ڢXI;Qې)_=gsGﻑ=0_lIv90O'""""VE5\ǬeG7`ry ^`o.H'x#k=2C$NJ͊ux%KgO%h,q>梃nX"Xk'$$߷6ݺe%9Ǭgev&o@ʨi+=Ǻks<뀐ե8Ol%k-8nՖa2ݗe?z}ߕ͹}|W.a/6`i"-dem5C_5ogҫ<'n#ǎYb_#Y犵K-,kspK]o7s&Kgۭ̽17Z~]5b5:wh:=Xχ[ÇY˫\jnj3ʆ_7yp|Lkk#q~wkϚ?Yz~Z3kk7eI}233{K^+ں?[ vZL1ٷ]~yI/@ۗpm5wo*~S\On/sC|֥n8yO*ƱjW~íSr 6Xqr?2c=i,D:8B(?(u^KeRy(O>Τ!p_GEBDDDDY5pI%e9+/rdşw9?mSSMXW_wnUWAp2OzN!!44Oox_} 7%sD3 ^)9J9?T籧#8*\@┧ޏ{ȳvSqny6 l8vglwLo\Ýk@*eD8c?W+ |#{ǯ~@0E'?G[""""r>rWSZ$8njjjqY]jVZ |eo|xt=q-2G?`mWA݊ O6S ^2q};ko_ZcM/،I`amiu`Bkuy߯b_;)K<;-%>>>>¥.ٌ4[ю8<,EtdgZ׀!x1 ˶Q=:e4k>ν>U(ȴn1 ]o@GޑcadW!kwg?kGuqtv1e\Wmd]nw%>zU%kcsX);jxvX󯪣uLTWm22zhcׇNUN_h>12) IDATyȨ:\1"b{⬦"w>jsR A<2J糋_ ^oȭJM_aEA[ 8:ǎ'0{aΜ{I9X N&i~oT3ulJ}xMn1i߳n06fwy:=X~K:HˬnuzYm3p `:tȺcǬ}.1}4ߺ@p'K:fÖ(Kv5ZJa`Ķ)CRZU\UfZ5Fmlɿ,-aKw@.-{rKyd,v΅G6`蠾M޿ˬcBIGXwQ&_6}w5 9O 'c15Q.4ynA3~MDDDD#Nb|w|ԋ]7qy+s&O~bNU3ˆ46y8|[\xMyɶٿ//#.""PL7rz {ψ/3F\ \aE@<ߺ=p#'TEM8t;Sd|b8~$+tLͥOdT@yn6.4m70{fcYy寈9i+0^ _%U Ld8XDF {<嚫9-5WN w^Kz>_?jMDI DDDDDDړ78|^ x.8yd{Sy׻8\\<͋DH\Kovа`^DDDDD䜵^鞻ZzKnKL߷BJ}qW5R@3^fN#~Èqw#6}\{,6Vx?#6}P9Cl۬F2w66gO]p#6;l22m6Myd;xb^Y6S.h+2FeYksQS3Zy}&1>:lQ]_&vEDD96wU pϸ}J)ϸT5U<˼'&/<Ә.*ܭ! ᒋ=Ť%},}9GѼ4V/xɱ2\DDDDD)˃`` ՠEamTQC؀xϛ^yQ jWl*}~|+n =y'$iO:633[1%Ss4 0* Z5Yw%(:-i0Z)O?bMzj᝗>N^1z-UI """""ҞtSt$j^5RAp qPo[Tz:lyU ߆lЈys니ȹsh7O~ēP6Ńrzse5k~\oQQL.GBK+=U}.6cBoEznGRj[H9|OIfjrkWz}aF_'^/6QAЖ1 Ց@_?B3ItȮ_iOBOSVxH`Elf4t`}gCלVdtC}>YoR""""""U(̋KU/\Enc-*6w6,s:$w^ܼv,7Xe3Xkf(%Yd9@r/""rfS؏&̰UN.÷-x@DDDDDDDI """""ۋ` Mkg!#q96si7y'f>8֟26bxm;ۯ'ꁴƷ}}96#N2|#4F Jvi9u7~K%]^KѷywtR]q5o- T0DDDDDD}fS_<>w<_%շzzGwaJOӺ.g`8آgxct7#`6zfıFff o /6b9Vk=3=BZ}lӌx&x^Ϯ9oz_oi6iSlʋ,ˆ/22؜wЈ7,_mf^]%OMy9ӂxo0d>W?|~E%vpA*-]G .+^ŕYkI ,z~;e}߮$iOvUyɶZ}bۮWZ: I#d4w+.Ӵ>/Jtn <{* x_tgV^K+ |PNY?ᾧi}^lW""""""퉋SiLVD]D+* | qன/D4w}߮Pnfs(#6ܘ}EFl6V s 3rlcoY07p/2?Vx>Gs٬Ȉqm?lʃ9VIMISVccՌ#m7gO5&|Oxcula畯|ُix85Ѱ(`,=݋5P>V؎#t8S\ nn}߮Z'4:8XO hdEAD"y)p@ၞ/Pt"ok >/J$-0El/J%A9iXQu}r\ޮ=.' .4n!1g{Л5 of[ _}Ylf%&닓l2a9/}Q+חmzQboe~\lxwy~ms_evgbĉ;&"""gJ#fm1zqOjRڦ̻+Ux~<}&݉.2Nr\DDDDD y{iB/Ϟࠫ5+s]y;h}[N"""""""Yv@DDDDDDDZɎݽ)S({q^n? _[3m\^zTh/Oj面Z{?tDl+ DDDDDDeo/bn """""""* DDDDDDD%x4ٜhAFl{Ɉ؜Ӯϵۈ?0kx}9] 59fR#^gĵFm%Fmجb;lKMq#12#>bqL#γ)vVqw)f#"""r)Ϝ/'oF͈!}l`Q]{\i&7,u c[݁yV[Fr&eU٘4ϢC81Sc!n]^D䱤9<m" 'yICm7ff,ݍxSvswݘ fzc\|bJ#6vL,oa3ms>cb>#@/DDDN'B?Jj"tS]JZTJbҰ݀ZWu_(l9;#|=7l iܦZj#Sxz%I;qk| 1j_ zgx#w>&Bw\V%yOkOxZw}e(䷟Q/`gH-;tPgu9߇\ (7n'fmswH> %$}5.\5[Uqt2C\.sAcl?师H;g4]^5ZNtmw;;Ո|UU f}a4hЉEWXo OOeHqa%]mRԛc}cLffy;6k"YFxs3œ~'M~}P+Ofl"g^qMy<ʓ9&]yǦ|>)o68""""VVm0#ks1b?`b}fܿ7_T42@@(M (̥ Pҥ+G,́R_DDDDDUJX )bTz>ae$GW 6_ t~b*\vfi|էZO#īH{<ϴh ijXU>ܷ|C?cT7Ԑ|wfr,'Ofcpm:&sW0n֏Q%y7%Ck~2K~@^b?³=zgLj|/cb,7ض9{N%`>S) Zc~g."""""""$@cH{ ,S2` k6k{hƈG>#6k#N6H4FՈ</M|kk]1}m@ mLMpf~Ŵ׃|-EYi0#Nr}6xC#lSw꿗,QbH; *f'\E[j(b̞tNl;QZ?/jHN¤<:*3Yt.oW}[5y,iN)eHt9I F%NsVʀx g3U%iy !( !m 3SW]=EP @ w}Ì='隶64<=>CML ʻiΈ>fFlIDMl54xۍI`5bsLFhI6mdy2[x;&>UFˈٔ"g+4X#N^f,x^M~UyyN"""7FۈqF*4>?w?{KݽϚ$4<-\_ץ/BؙHvFb22螐~_CR }P砤OorS/Fvܛ7kXxLȡ[vi/'_ hkj( dx gKW'PĨ}y*蝞䊆+kQ$}>83eĉFb37b;lo*s}lƈ72?K"dl6<ӈ쯹6?'wwɿ ٿ6ǻuM#劈H{7ܟ_y25w`< Ҍg̹J"ZĬ|\duYQ)^qA#f3Wsoᡞ?ޮd`TI.9JN""""""HTW9EIspT1y6zBy84F;_WuOy>J0J|_±h`< M6 =H{2xf8p+i %\eP {k\%]Z+ZȾ,p ;W6TI """""Ҟ|ψgb0(54ZN?w񩫹"^G]\}?1.JpYa|;4>ꖬ>6I '4n`gsoSo<üMD`>fb8gi`[?ڈ+8f}q]mflO9v#n%6iġF|ԦԈwڔ@ɏF\j@DDD;W+'ܿS^6kaąFgK/sxqMz}?4bZj)/6oƈˌ3b>C?I~^vlΏj#>fIy%ut=6Wi|"{{Fg'\-J3 (e{\Poے.`~T8N,Igp^1n |޻Ǘ>ĭҋHޚ<4'gqòM$$pH* DDDDDDy]p0x`,x)̯; &g{UӁO.Iˣ0n/@O ikjpf]P}oLJHg}p-@w'TP>!zNz{`:qӁ .CؙHvFb22螐~_sUħabBy3%zIT$DDDDDDګ`!p LDŔgS|/M'yW[FB~%p'I8\J 5&"k32'#o%p0q/#^ocЈ! 0 #6F\euYYkcjsa678heaněڸ&?\eq_g+ج63>fyg}O;8}mn7]y2cs 6ٔ.F<ވmV6l^v/_/Osμ+5bs3s .sL6'7 tv &#—1DTsJi F_2(Jaq9Ⱦ,ǎ1ag[tqH{q֗3*xZd9 9IO"FV]F}t b8|坻\_CyT /4YAPO⓵S~J|vdG3-b,p/,Vթ?u0%m-p A6s'뭡NG)kȒY\_¸X?*F""""""Nc] SҴF'qG聧^+SگAlsUY5eYnmsf(#V9J)|taJ֘ڜso?GvP%@DDDDDڛP`A%h89g{1QFψ1|ڈ8f*؜Ǵ'xy9^qwP+wmWqkiocbD#^g_1&"""rY r}Y6aFˈk-rn """""""@z%%@HEuy4%"?͈#@$oMK"ՇuZz5Nާ<m" 'yWG3iϳ~؈>6;ﰉMFf|nM39&F#4$vL6g2-LL*#elSM3hqH/_[3C98/wo""""gAFۮ÷yyc#q}-uŽJru]tUħabBy3%"G󋨈K($-|v8qɂI7ES '_WiCb~ݙ6{iw|θ#ud3dyHg6' WԵ.MbcOGDDDDDDړں>Ka~4=gz⹧6<5mQt? G^t"}RJ?m3r n,k?4{7 MxߒL0#6D}y8̚D#1b/gof$_#6;8lo*s}lƈ72?K"dlĻ5U3ˏIOo#3U)6w7N#lSZپ]Ex)\ +$E/`Džg.u擡' ]eD |/v33_qt2 w0$K%m`'cZW@'{*!ACN,ϲ1 )iU%H{L3Á[ᕚs3+Ⱦ,p ;r@DDDDD0O9@P FFs++7drkk *9Ą{/{qTO>!lГ (g;O&h8&]dϽ9O990/ڬ2>fb8gN5F\ay6dΓnsvBmߌcmۍx_-ڔ^`6)ρ6wo""""g*#.Y_ܯk>F3Y,p/lV}[׉_ &% udu#LJ»rh2_F7(>1%:q~T)* DDDDDD}ML8tFԩ6 R՗z.G\FcQu7@- DDDDDDZn'sSgev}#m^mfs^~Im)6z[a^.|`St:/On盈JKqD~MҔ^i˝wyV9sΫ$@"""""""Rd%AbGGt+Le&o<6+ؿt6wM6k^㒉&:?g/O[a;L%Օq|p.ܭn4\&~ؕ25+Lاt}#""g?weolg f޼0z磌X5sϭ>՚(sg˝Xז_3oރ|+5*k(dej<7.YRޠ|\Oxx<* .gnN999䤽Ę1VvZrJ}?|( )<ݗb`hk\u66E^ڑ}ͣ)@Q]x[d5O>!mQB٥HÖdf ^,Qr7}M?{T3I]e#nةwJOe?1[P_wb)m6ߜk|^DDDّӕФt\T@qe|f$ Af0bLzXuH6E5ozw.pvtvpIZ=+A*]4~Et.)^V'͕IXjjr\raVWplK,z :b G< yA*5vlaԛO2X[[J7{3\ҝP "^s>?\HGgUwL?:Q+bkYK@?97Ql/?ku x+uYKqF ΥP*DDZYPMIq U]Pk#+rsR:lLUH%Asّ/嘻9mKBbdJj+k*:Z"uqYI/.v&& tȠ{c;[v"bOqײJ} Itt4=OgHc;Pa~R% &Xn{f-.WI'ɞ&/6Q`='+;:D(oG{mk~t&Oflr\2sxtݧ8S5w#3-Lί4C[GNmQS7ˉkWi?'E9(o\콍_vec~Ær/tSw<άMt!\qs6-fdڰޞ\_NTDo]Jڛu۽*LIjbg;*7>'a~Hf,ᇿ?Gˮ-s8Qe98F~i/Ώf_wb|z763hƛ-N{RWΕ)5t*՚ջx!El1W \s;0-0L<ź3M0&`c#뻣ʊr{>P,9[_xi#*N+cC)6Ѣ>gO#H-],ܗkY> NTW?fXB pUxf*%ʘJIIH#s6-x/_GB ~z[PU_{/[_0\ֹm}T F솟_ MTzQ⹤QV5Mq* /tӒ~#ѿ{3I.?:;~A8`N57 wz eRip{"}+va?տlk ̍[^v 5cBXεSHr~;{pM#`w.dh+@#/EB`|tg?7nNo*?7NsꯞGtu. ʨvI>Q_J8SˮHw' "*j)4+ҩjCχ@+}iBOGySN.7Ə&K'wkbvnusKcr!jM<Ҽi2yof.|o<"P OO|m5N\̙ZłcXbO'XBv枿]#;(׏Mw\o^ϣ/2"2W}wY;~sd39x̔1D4\y:+K-oC&*2o7 E4 AU ={{.R"#:#[x_>Ư Mŕxy^S ;Y׬rKacܺ\5 +[3n|󢜺V1g0&)H1ڼzۭϺh.L i>A-̋oy&0!9RSw|˷bտBosk/>ƲDoc_>}:v| nx^^L۸f4hUZp '̽Ihv:Nx.gݿϧE> *l;|4i- `jhQJ#7L`سxOWi`&s fP}2:ʺ\W 9DoOOjɣOн7ˑ#)< o.!1>."63pG_C}D5g >@M> '\cw6l? .P<*79fu4W=!4;#{Ldosm}a-Ѱoc7aŚKڲWxO3_gcי*[}:!&ՇٞSCχ_:i:/-#0J*jTWl&Dzyq_<;axt:+_))*ϭq'ߚ˕~#^[<$uphAsʜfǯ(3-g7Rzzw6ov[ysy c<׾h[l-D51:T.2^\OrЂnmEaϷwts8a4o?|IUъ:'I3yz7^-_~LxޙG5ye"QP\IXB\@qZK(S:WP;tֵj]UJ gU;UVQ #U n{W <}PXPD@*M#ÇE: z+u؈B)SؓhwNHn7=g2wjh0pIJs939waCw%yM,`jx6"éF /&͍GI}-ަ6 6L+h uM5 9ɋzܒ>M) kIDATo%'"&|2ryk4Y fw8-͵n/~I4xMd\ BR 1PRf-NuiH0s(_}&[Z$u&G|^AJ"i֌8gpiDԇ+6F/U}"H6w=XNހw 9u1w~Hh&ב- _;͍ 'e7!QCky=oVKsm2 kU!#d@Y g$^ 4 9#7dQst\aZzu\uĈ=--rJSX*[#^r?"k(v1Ud5~m _YT1G #k$ H%Fl[&q\kT #3YE.~l$1~|Մhޙ`k-tzY{i;2z}S=əh޹Ι1c -a vNQuI%!ތ/p.=]N v\s(g?veaX7oQ/!1;9W3[kܕ:~t eZR?igC]|ځ9~}%|pz6Ȇچ-5^;%N|$KԮΗC\rw]LE$Ys1y3vo(2S|I9M2)6EþNsƮ_nQTV}_÷q >p%R})w&~r2+ s燢=4ּOїi8dYZju~:q]@\bj|#Y1}q?f6N0TU^W ӑ@@4Xܖe`' 1u>8bK"R{l>K WՃM6Je.ĕK(,iK(~齱z~27(+h>$[i,4v@ٯ!\.xr+Gǧ ,I qtXZLjO(cݭx~AJw0Iy£So||ӪZIˤ9ϓ67n~93*Y2nD]ܾ;8o'~K;EJ˞a~dAc|GW;ob6] l*dO !^ȑ2Mr{U3vhDeug!rˋT=2},bS2 ]IbVl, Pk] .C4{#{Qysfl'Dѭe/mgfa@mߟ煢 #slE*Ǚ;@!ub-D+⃇ʃNQlW+J s燍 aWn^}4/ՌǙֲX }:vd֙:盕Kf7JM/Fnf .!,ˡE+x֗2` `jite@ b+@Pݐ\9/eJ/=4N6;pBfIٯx)' ^%puWB7#Ъh}afjJfڞ,}{|,%)/J7dI?#ILEBi\ }D+|BB'BMciwXcLKwDu,K DRH@ S&O\oˮ"2 @c;<=DMXDѫ)SyMo.xXVuAݡOggvJHXCfB@ X8?]Ha߳pK̹Cԋ9h,#!xBӍ)$a]8c4G@ $@434h! !^ : wI @ Y \yIENDB`transmission-remote-cli-1.7.0/screenshots/screenshot-mainfull-v1.3.png000066400000000000000000002012071233471375700260410ustar00rootroot00000000000000PNG  IHDRrbKGD pHYs  tIME 5#DWiTXtCommentCreated with GIMPd.e IDATxw|E%w 5H!&EA@DPT@'Xy" J" *ED;! HҀq I6KL~ܲev;s3huB!B!x0) !B!BB!B@~~eκB1^51__~.G_a:W#YaXRgk$,ȹl`n֝'1S%[W! }_VL>N<9V aBby9*/hLJ/e?O:0m3 Es;}Cd2RUX}W>_C-%,ԗy/I[96UrWX۱ll6cCC KM[$%Fu~~~hl|v},9yOrӑsz72˴䭣xa.jYQMvӎI[yK1!7;y=6Ui1+{6qTt/@yӐ9D9ƦF4;zvǮU0[AޓFGeK<70 q%E䋿T0i~0q6Ht}g7 *He!tGĹ?v,; y]k]gz#i[=MqQ%U4UJftghB |773} N2u>>>>*Nߓ^tnΔExyy r*{^ToO#b{,GE߮:^0eϧ Vk5g o"N.E hԏ 3{qgB%#Q5eHoZ|P˳jwf䴷%O M}WGNǔUҸ"䬁 e}UF&Ԛ jD߯vs#W̚Ҧ& xfdz0,X715}odiTBj42iG2#<Z?J1QCAo¹=tm׆n?mjͶ j'ϖ'KkgW~J_y]!ƽsds|D(_':0Yz)6IV8tg??c͝s8ĽIs%yK??Lp;h٪K(ߺ^u Q`&r8z7j֪f72qצWvmuV봢J{J8zxHǨ매_Ҧ(PZ%=ozU:0el¯?m~\%=)\%#d91$!I ^}sz6mpQ4hfqعw}[we\5x)ad,jFP駙%=`ƛy GpjǗ>!2v7_.Lw~dH"ȐRO\XcwiK#t't\2\S :U7u%=d.JiFDp|{<# O/]8Le~Ũyc3G[pYԖxŨk;aLL8Sb7¹h(Rp JЈ8|4eN_D@kvֿ:SװQ51j+ uig˅zKqWuu$볢"&lBZۥ_mJ\9lK[?^7JAH9s2}ۃx6V.xTA|kzyi$$s9l_ȅ=鑃|ܣ%.]Q+K7dg] y}/P?] -ώ7r9%=_7M企 1WjP20ws&-ZVÈK`ڽ68AT>>W q/U#SZN-w<^lӷ";Τhv yM_JhWkƔ 粭-Fzk B,o扉x2xxR摞؎sq:}QKEqJbјxNÄMb]ԟ8MqqǿƍI/wXtG8]^ ̺WNߞ燐s;)KDǓvU"q&<\@)SHIV\=0%qGٸ>! (V_Owg3& ](3|YcX˅$][˽m^KW̕^ ?/=GԶn3dۣ{]k:zz&RrV㭝{ޗ%bt)-k`<}2>P%$r?S7sR58|+z&D/0OzN9==+\I6.hjQnrc犸PPΛw}.Fʶǿԝ?nt)_r@^ K|{;9 ;Sgڃ}&t@u/w2,10M5>у#?bWTt. &wLN&<\ Ln8'>Qvm?r4 \:*7ܵo٘onr!ΛŜ! /нM%*N5^z7,],OE.ґzEEK (wz `UNא))ZapzB'aL Q$DBZݳJ ʞYG][/øJߢs>Rq8[!nq))߰.]c&) Ұ Rnp䍫!_3/Ki^9s) iJKIIxm I~Ò5qwЛ6+;=7<#\)fu\6L{ _8c-dž ttԮqٽ#\ yu/>x^96%!(GŽ#*fΗ;yPɂ_b>]j'#-m^8޲/Zx7Ǩn i>|#W5: UjN۵qKGٴ:Er>|;mwm<,/.yl=nw.bX(|r˘| ./'/k^r׋kq}BÌ_!+?2ݗwoWGk[0D?;'S"7zåB3Ήka0guƂe;nTYmA-3-r/˚ؿ!GJRelgʕ,Japqd磠_^[pbT sU\HYdMh&8/݂GNhyy/>˛:;YNhՋU jL8͆!mo4m5y\t3R$c[an׎ZZϱT]KMŝƹsԃnq5 hYqjY.'(t9ۉ k/{4(7>l.V!͖bِGtJ̦o6Q Ңk>؍Qb I"6[*ڸ>!  nLۯC\9ܮNӞ5ag1+grlyЪ|-SfœW,׊{VS7ؽpQޘ|rr>ʫ!r"%w$ԟ} nڦapKc;:'}^[٦r{{0S'#%w(bt( WB^AXu+/6ݟP1^=s3,EA8]_q}[^~hwu؅c:npLdLI 9v>c63{>[Cr;)al^Wݮ%n=b[cXr I \:wlnYZyhZ*v^$X>IOc٦[_ˆI+mH]~k$&1sX0vY?{7[ˋɗ`39xkbQ`ͱK'߳;Y=xS o|5s7Ds; 2 T7; Q@5_'&1;7"ٻ+Ɩ[;o[-eٛ?zRpTŠ|W]ir_ERb ,d9|\{]^f{syIqv߾7M;Q3m a/g-/6>W:gs}Vt 2zKok8]_l [?jtΦ[cL!)?ZW?1zL P/'0bH}b}B& 'Y?flʈHv.w ;ouN=-s^ Eaɯ"Bͦ`^#5KqO.q.OQ6Γ: Yvs'Q> M{~F[ | 9gkMo=;tGrH[B]gyù ]NɋHE;[~trs7_̨'D'FX~ :> ;on΍e?q# S Ovdxu~@T_:Aabk3ex)dh. uML4!xL{>O( "Bf1^o6XGa7]ۿ1I.''!.MVNl12`d-e -"3<9y5_ww/79[}x!ByL/";)_4uE5812TfE%+B!YGYo}Ҥ@!Bw;o' C_c9 xy0!B!(LHBCC q\2] B!B!DY"X.ce5x+Mz&iߋIWq>[i}Q.kkVsB^"5i_M:6_v)wyO Yo&]3ϟi_tE2!"4!B!D~ tV^dXx2׎Jcg8֖ ǝ\Eԧ$yfY!@!B!,T ԇR>nrXzzM>J.1} Ω0s2B!Bi%`P9`4HH]+Qm<^eRB9-:..҃ >Khҫ5ZvLvv vӛV__֎9=|&&OԤc[YQM/MZ;漨&t&}XI{k14#t1MZ;ştM`%iҍ5iVK;5M.Y9FV&gx/G6koruL^+#{JyЎ>IѤh 4VMe./?eVaqJ}&SM:ZyY)_Gg!(<95+͓S62y삥B`0s;p%2pcC]V+SWʿm b+{񮶒+ZIפs_*?幘/6?71Vv/<ұBد`Z6ulBj==scR߯Aq<1 Zab4{4!B!S:m2֘L*OodB!BD Ǒ;iܱ [wԿ.eX,C vc*Y.KڠB!"' 0"Wcy ^R0m=FK |ZeLkK l6s1ە6ǴmDP,* m4c6XAlw+1SG _qBי)Br~wk|-2Rѧ6̜9ӏфAKH[.f ìH.<=ޅV}&ǩ\6ch<Wuͼ$Ѝ'-&'V!>[k~)Ť ]ϰW7qc%;$C{ E>ߑR>K/twLT8m/5t$_.Nޤ[E,/܃:>8xKY#Uև&G1{޾ſ7_ح/?{BQRt+`۱J+2 V"N:4[m6c>S^u{-siashӐ9a-?Գ㮍zv=zFJ5_{}b<Ɗr&_׈fN~k{@|m_M)O8%Mk IDATw8T>[pLKe^7aӴ ݁sO yod Ҍ3㟢FPEj=3;2})u9#eNg^j@]6CuE ɫ.]c;S+ПG1RgMBvti,ÊML>q3)(([RO]?=z㟝㸸"v~ӅW;XFgJ$pʼ+T&|Jc MO{Zg1<Je}! b`GCy_ ǻf52o9czu}Oewf_]rF3v 2mǻTu$rjݾSˎ32ݹl/෪WAiM3w)ޮ|S>FP]fLQwWe[2ա תjغ3!JjŪPSgGS)/\Wwڿ{S"3-hڵ5d!¥y;Kڳ-Ut6vS+VͼlR{׃u-sSO}z˽Ww=7u\7;^98v.7sOWTM,˅<=Zm8gVf&^6|ntuNϪQ+O)٬.zwF?=3BOuU3+4ꜺhqT1l1va؀tjP!sK{ : LsŅm|Fٸ[5T\_OlLBvf!92 .E[[_ǒqx\ɭ9߮gB'b| g{t0MY;q-DMx:LjDS98hǞx,>l^|bljiS!6Ηr-Kz26K:W~}1`JvEߝ܁iƿ^`gΫ$禞=eYN^W֛:CwS.7EzM$Hvw ltdx-O/ >)}*" 1!}NB]WHL_,0Ҿ31:$'Se6XP_*-^1޴nbQqG1.n#Cf[NL)֖P<\ xf*?,Ϣ3@=*HgJƓgy\֯/~(!&t;2ns91?3~, {de Fz}-~pUR0O;zgخw4A1Myo=.j\r<|zQ_QS.7 D/*>R=ƁFV1nM;>nNer›p39*.uƲ.펻D F{7s>oJr@q2x!5LFVHkzZc71H8p>{(v]*KJWTtw`I8ˮѲ?mpҜ wD!fJY,Szteܳ.r :v QQl;Vtj2Qu+OmG%w|!&=khW!M)w uH'|_I[Zm qA48)/[岞bya۽֛:#;rX@_HwԿv? ;+{q߆4#i+9h2笽b4Mm7•d)܊ 0VaSIf Jlnq+_2G|cy!SCcx`Y՚j4[]P"hAω Uo"NEbqZ~!,ѝҵ{ɤuv`Id#48>uit÷3|0 ŜS=מ狿8b乤{U1잱ÀioWo[gjan6a ֝#QF7ɸɽ) ɹ('Bڥq7S3|85'g-6ߊbJ<ߩ Ł;,_x?ZxUs|<=,c?o#Fw_ά)wg)xgP{׃u׃Q,ҿ?&k;4A{=7uG B[ᔛ>h[yOĸ1n+^;{7]dq*K@+0'\@ }+pv(YmkNN[]7";eu]$wU<7lijLM:_ [sc2IMkOA4+٬P?M2j72-f2O]LSܝ圼Uկ{*٬Q_{S L6fW֏f9=୪~fܿJ}oZulI'Y|Q֩3M`~5ɒ5rWK>f:52dRA>ƲOTfe6Gw~NRV {cPTuZM~vnNhjفlb|UMVD[ jƪf9WNRinzC7;gعs=TOUP­9YX3)L;?ǑNSՆ\r9acǸkKk'n;^9"2J)~UilX0)yjaK<i)2KP+QgS'VUսSrʐ=KzQ[dL2$Dvzט !E!(KӴ[5Wl₦@hhh8ĕK;hy3ؔϿKFq1Y}{u9&B!BQp%öY i ؽZq'ƯXD$'6Let?;B!B!9b B!BPab B!B!.9>Z!B!B B!B!@ B!B@!B!HB!B!!B!B B!B!@B!B4!B!i B!B!@ B!B@!B!HB!B!!B!B B!B!@B!B4!B!0W2B!B! :Bs,V'SزK48Mp&I&o.M~uk#\wQnIoФie5V*W+gU4۳urI4x3> #Y jפ]Ki'4 MQM&bO[vyct&]JX9^mפOi ^s\6k.g䟻&}NI%:Xf/sy6Y?h,.4jTڸVʓɱsϦ˨&}1X>〕lc;I/i=쿭1Mz&]JhIWFז|JԤ?1V_e%FY)dx%| tN}8O}hC‗RtxoMOImLhMS0Vއ`(r@ B!"'0K`4q?wIQjj#@v6bql |GXKÃw'\܍$'U'B!B!39wI@OO`RFE>X t#kOx!p%o~f(G* B!Bq?)XR}eHEb'A` wXS_4T9e5>}g _(,cshh11ڱ/vLgMZCY^f%ʿ׾r.s\;f#Z75ip+Z^N&XB{/D{`+ӎ)IkǼR^-I6G?leR^~ksVDx}Զ>kwRln>2*geC\_=9\o2[1sʿcUu1>++ﺕ&}JKime\_WB_2`ƺ l5%>iI3E$aj&ng;))-J^͛?<}'\W*AJ!B!Ȏv>ܗ&`[43凅1:moa|!+s!njBy;)7W3[Z{R8WB!SSHN&_,o7HE,=`ߴw.XuAN/yB!B..Xz \ X=("}؀?50\7ce m+D;^;S;&}|}/vv =%bma% kǔjj Xٞמ/7ϧy%ٹ<ۺ?Z޴]˴׎ߡIWѤKXٿ*VWs/+!HާIkV;Ú6柵b-^iWi+o+-V֧-_ć\|N7Y*e-~1hW%wl?w+磼"J6gEQ;+!p8NX&)4=oy9,s*W}Iz3gRl!lj B!B#{W e8@9ž&'u}f, OrED=Ð i B!B0uS?ze7<@]׻ߕVpIQx78eB!B!B`h~AQ[\B!rk&PD!r%Ց,/Xd B!B!7Gy !B!D\@GB!B!!B!B<( Xv|mN2?\03fsv؝CVϓ-bINXP\ JEjsl>l&Ō vN>R WːDZƜgf3fs>-q:f(=l&|bF+IB(,oܨufl|兜_X4*5c̜9 ̙39sܲlMx:s&3˭p7< B%Wq;;T'?-d9jxP{B&68ǝjRѿM-og]wV@?Z!4xB_WڌX-]n|ϫͪPZc^ZO>q^j~~W;oJq*Hܪcބ8Uf!Mમ9BnW}ľ+!qW_Q }]:WŌ9| X\ڹt t܂N%0^4+q cȘ +(] k@ڝu1R^<&4r:˝;ߙQW9k®r+),eեN7;acD͘Ɣjd?=^okK=7G=D4dN'zOq׶)~q\ܷ? !1 nEp*1G7hԛn)_3?p"ED%py4Tyi6>M_!:}ޠWK)83)jU3o.ڗ[3Rt0T ~߅_NMm3dZW$kݐ>3 ~S/ufڴ.qN?);McPV lB`sԖOQb*o%삏c^ ~xחtT+Ӕ[ x*p)f3mlS {nyU/Xԯ>!h"LSepyeF^ܞۧ>!!=wzeN8zO_iT'LԝY_>!`뜸.U&St׋krTyw=.8~JTj:K-R`ہꁁT!*wa{t~UR6i{$qW{_q\ܿq`:Nx_mF[MA@x2NWɶ3ч||iGoLfΜ+9>ZxT۬VsW-g PSKkUUhr`5xn5Wx7D.Svj|wfz[;VV5Jۿ 5h5wn4ە|[2w?6`}-/TJŔ]]}.L#6iׇw8ҬNlB=[0۫wVQFPn/2d vw;,;r^&]_SAFb (\)ī=RMVΑo7rlxZ?mԒT"(\aΨk*:eG3S[ʕbz0"Ԣsnic>5UOo[ꮧ{fkJ^9wܰww=>*ǷH{b'L BCCs鬞u5@uu{Õ9zrj٬Mݧ3,he;R,>f:(l'r1=3kbcm3?ðԠB.4~,7Xefrnx̶SǞ™(vKere\S96]#֜oWb(JqDz5{ nޛI{3g\KZWug"u7$5sDY{,9Ivb-éKO-w;x9eK&4[Nd%+6Η IDATeU[OfW/̳FF ol0͜;04?S%puǙ.rLqQ*/3m8v^%97^4(7mіr]Ogǡ;_zW ]_W=e;= LeZJFq37s6:Ҽ;ѿc?kHNM7 mŽi?b#WS G Ÿ,Ac{ך[NLTֱYs\Qxx֠GL M,vF1'V=G ύlu&;Io~$O'ze?RIJ\qWXZe.xgde Fz}-~pUR0O;zgخw4KLR?&D;nKS^[fz:tԿ9urTKG ;ƫ 8g/M@z#A8VO !&?goJR廼Ǘ]21S1c$b ZAru\}2Mht\&q#|wT~?H$aZ~pERYTdŕ'*ͩ“p]eU/~s+9ArmDG^N.e:`x;?OxҠ7Fɸh-t?3g|t[ܸ㎯3ĤDzNL^>سR^C)섫`;9xM53)Ĝv^O2l>➃i^Ŷ#ތmUGO(ߺ^'v֩ػozچziSÇ+rozZo><7 cy =@bS7E6E*J|B`-~.rТ@bv.ݫA̭æ $:Ȥ/tщ.AqOgOٳq2;hBL]6QZS|Q\F}ТKJD~c/#9q8m2Rğ:_cI4uHV䙚p7SvO>ԝ,~-btH`3|v-Hv}+Hvohg3ަCRx])А^.⋠ ]n{i9a,kJ'>/58~N V6|N^pu'+n(Mz2nr̓#8OY4FwJ|Ƿ[Q\;U;8p' /~G<[Jz'N{ís'}[mKÙ5%l6ſOL g,0`oÙ~~]Oyw=hzZw=򩻩?q*m2]ƾCC{zYqILxl-pv-C4uj\ʨYޤʴޣNl>v2M n 9X[=Q2*57՚0dcY&f3୪~g-r*yAjձ%9dE>ɣw jN󞪺ӽ'޹$OUXlOqtZ4])ۉ`: lNtl'uZ7^Ƙ?^xTuxeP-٩NlT~Z [0lTۡ|b-zT#Բ@諚 6#UḮs?o,F^Oow<-T*}3l63/Qc)SN']^\Ꙥ˦zZoﮂ:n=Ι|w5ŚYWOeP{>UmDr wVOȏ-ϯax8.{K?Sf^Mw'YܽL:hu,ߩzjӡT|ze㣕!9>Z9{!0g FdHS;z1BʋBQ9RC%"@hhh8ĕK;hy3ؔϿKFq1Y}{u9&B!BQp%__66@`/{O_ϱHNl@ˈ~%wB!Bs2@!B!ȡ4@B!B\r| 1B!B!A B!B@!B!HB!B!!B!B B!BwtTE&:HjXE)tQQ"""H!@ ^в!dC}>pf{ܹw~;wVDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDDP@DDDDDDD$DDDD.&S^cǮkڴQ.+VP܃f"""""""z VS6l:"K)-K;R+')uRSHWM5E/EZ,|5-+Him Y3m'+Ӹ' Xb?~i.|rΠؽUB)Ww^\FR\OL-w0_BӦB (cT?ƍRp4CuG%"prkKgB8IQE """"P|%ܺC/Rn#?bu1W&[] |<рMld/đwY pzQ"vq1qpks+5C*pv3G HST[*E:>ܞ"}-E)ґ)ҕHHNIN٣Hޟ"">wGDDDDĂgUOe\h!1 00Gg g)W1P%]\vPmZjeäK0f%j|Y΀Վfnj[/aqx/c ;=r6oϲ oL NHFq'p\Yb~<r״Gšs,$ 8S:;3.~~U}Flj1?]f<84wjܽcJ≍(|^Mt ~]9C#OˬS6Vº9qHm}l{NAL( pYQv_-LGu<=9z͛Db|f#oMe^]1@8Eb/pj5 -곇RPÇo GBƅ"m~,Tl?$P@DDDD䑓eQq:^)@)i/֩/*-Ѫ;=Y6d %qu7[]7%ϽNZ ^gUfd`!G{Sߛy~˦=+g{0;tcpԫzG;+>;s8لr{G^= ISyNTk:s|^BvMٓ45zks<|\9wp۸Aw{q4%b hBGJ<}i/3F8w*[م`itϳ7p* 3w!NІ96N^[N#h?ѹ/Xc,3`Udhy{@O{{/@>N9GÏAq4)\vYxϭ'UpOᆑ+~ Ng)Pr;TksYCaO (G9IYmQ{䙻d>FZb!\))!9""}2E%E;E: ^IN)H"jtR怈X+_zӴ3< ;TcN+>E ]֣ۨBFֻs1Egp |[nb;XP>E;rii!GѽBg\`u+ujGD@b;UH 9B.&&HR|^Ž$ݝʯ^@ݸW |y"K<,<+烓L؃)NjouRrp|=^Ah$Q*` əaؒL<*vRxFQ怃>] 2n{Ӄ*gxN/M$3˾ɞ8sIh͚SNx\EQ;OM~ufoH%K E>(4R7Ǐl$pQRYڏl. H|+SC,&Zl)yT""""vb2U!OjZV.;O~j6GɘxǮkڴQ.+VPb٢ɚA """""(/.ߢry@(@ """""""`\:T""""r;?DǛf"""""""^ X|!WLn=?GV< e&g{w(`_Y' C[dp; '{oRɶג1d a4/r2GX׻:3/X7E iKj5]6C!L&v,dP"^R[}H[X=ֿ'+uAN?ae_>_շۊيդUa SLQCզ.E< i]gLCyG`| |SLos93n3 d=A>>߉mue\G%%Qַhjg7yVjDNj%)[jm'R{~y< 7~Lѕyο]הd(tƒ]7x>sSa[:Ni*N.Ih{&x}SOYY}pmLQE!;/ZE0Qi|(S6E_"ۉnR|WߧkqV0z@O> GQV\uٷ ={߳0P~9ˋٞc:kqhLehZd֡hsHnͷz!#n׉Mr:0:QpF@fs3)vϊ>4jѩZV"nxI"3K$Dqn/ /[,3:Q74OstHX)=m; lFƅp‰BM^'_8jtr/[50vd9u8mdg8[-ʺD}zψ18TԊ>SY8gǒw  "˃ { ,\)rmr;;9~Mnj\ޞCNj}B!@EٶY'DUo_ ꘃݦi!bGUxknnERXk}sn܌/mj/A&& .y9i~w{NFqBM&L{Y5+Sf\,Jk~}sd=Xo><)ۼ<疬U:QuoSn6cprk98sryeBf0M,3W_q*3`/$kgT6_r؞ڿ[Fy?_>ۂ _cfu8nmr}<_ o0|KNz=Z[.ܙ*uiRV#y^Ȃ^YXڵ%RBFF>T)笚mN2n=4ezG 8ğdl.{v3\I1-|J#XS ۹UBpkY+P(_!r8~gs]"HOm=d%=2y퐃le]&2b6xF[{3 SĂN)W*u.^ϋ#K@ΗGl|-NzPT:dlɎÆIu{T͂3Y|xj/և%: ^I?Uƚ)~CqoBЈ^3OP}w*\ǎn4CFd/9Pe[8,J9O#voRhwZ{򔺹ߍD;ws-?ӿ7gœ4M:6S>bf$+ ʒ ' R IDAT@4){CPdC۞c~'3%h-@;GT\$g˧s%7Eiև??gy x5۵@G6Qc7y4W p{h?;B@,1%R 5wfa}gc\J j Lpw\la}iѭ[园7&p!=&ݘ~ԏ~Bϊ1 j'qgp)kqr+@`G@}xX6\n.ےP[ s6Xb)Hϼ&ɉ'1j.K᲍9"/}߽箅V̑X> ~ۅ?MLޟ2߳DEqnR][k|LcA鯻Պ.p/ d 6c&W2s8F֣qt/ ̷^weM{>gq-= ":/c[ s/FO䅓m}aqiѳ;>ɔM\zVf 祧[{=:h}9[u6ηGb߰8ޒ^jvC|Gf=]^ʗʗIA.DGsvNd͏́;_@#Ỷ$]񅓕sB7ߤS0KnB|Z!L\&6x9NE[v峟f^WD.FLm4Rٻ`4|X56] D;{UwoU&,c[@TOH\>r5K+$p`;^ϗTH [ rt[OtTz}JU;fm(}O?!\M z~AS_gJ|šEq=K,<} ,/w%|͏dbOd?\3zRsi *Ca”ݖW2㛺7Lh/և,>(oD]n^bϚ#sSF myJ}#V*yXsf;{V]-g+rHT>Z]] =5;K>e븈ua@70߸ 6Kvwp ADbn 2m4o b=fh]ލp.Oɤ^̘x#](}xhbwhISB,>;1P/4ۤ /!kmhrx31BWRcӰ/LᵽV(Yy5e=ֳ)"{Hca%%xJC(4~ߖ}O'$Y/p-|YsX[,Ty M/g/_0Y^dO/y|+emE^g8ݟP {G֟9<^ov]NfJ(KZg{֟q \Ǥ~cx(뱎Mz%E{>w'Bť{$]e[JQطu:o|a[[`s\~: oڽ}K&]kb̉g&=8_ҩ J>W o#6s:*",7f^?u[#l}f7PCAt(;F1^sGb9?5S(^c5:κ#j9eD#:{Dpq2@j1?kϭ/9Θsr|_[s[ғ_7>R)ύ;9(3w>^Cj nH=Hح\grs M'$YzqD\}.זOtcC|̃*]ʝv✧9#ør6Z9r1>õ87ry9B^Ե L9k(KxFQ.&41_Oe5$fRq2:pw Fh52_ʽOsPNw];~@!Xg˷El?[et]ߢY~<7fܧ|{C{{>_ׯc |5-K~w#Ȥ˟=ka#+Bf{vtj/91f%;pߕۼfX̉$tQVrft#O6 ےY2 |śo]-ݐ.A]o6b`b/,=7°y?W bByoWjw_iv+Jǎu2btEɆ}:բm*L%Qk(Ẇѕ\e[1tT3esLl?FJy҈4V7o%w[izb̜QtmY]0=SCu8ڤI)W nſV۱ ?|Cʍх,+>ŝ:;!,7 يB1wYRYy9ctΊ_Nԙɬ1V/B-(_h7޽cVZF ynZ9>u IƒIAӗdۗ|yc%0w[8+FgjɿsI~BӚbu}XOw^\NّJ~יPLE1x/^NNx;C>g:ڽݯ֖Ǒ,00M}ηvݲT>Xڽ~m7$}y7Y*nF9Y׏ij޽M8qWpK|!f}%𯍜Ǎ /@ ٶˆ.;Iq.<"ٳ @f}:c Mm+/8S;}ʿ6 ,U-~Έt I=1Sq<$g#~^O$.O zWd7;;MdSgև?EO2ᄽ>2Q _vXWZa҃D[ڝNwq"W~KYi -+-rpLԭT=6#l{?-֧DH~U cߠVtK{be,{C>6!c8Dٻn1ԎMwߣڿuyZ4 -g_pAG{%516"z 0A!AY=zޫҪbm}X}Ψ ;js6TH7|a~9G/Z >Ear48b)+^^VqFnK/U~ڻ~m7HX0$7^Pr$&Hؾd,8M|f~v A7c!+8m-!b蘡ºƳ"8؞!1*^@\P;j1q.uǐ}J;}ɺ_¹ ?VziӦOqX79fR.]ϙ'\-Y3ѻYɈChSog#*.^ȑ ۙ4;V:^c|9߳]Ɉslƨ~JDa 0_??v^26)cRj#v amQy\-.1*Lk """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""L&JBDDDDD BhܘA} ٿbj-{OXۿ]w($$"뛩pm4~·Hv*?y2hHFeɖ4ӫ*Ehշf߷,߉JubL>T'Bl}k4!Z.胏%6O;|̱ehWxЍ|U\LHndžT+~c{Ūɐ!Ħ7t}?s?mE ~8NKI#oxyS f'taKinʹ0C'-UkղHۻ"7;hnHȗ"]P^tPE O""c4"]Fw.ù_}gY~'oK^{ly(f$""""w99ˊ)WBpmDnDl{*D58qH'~S :?*rR&ˮql3!3f%jSQ'|_ȳjd ^F\NFE)d" ۵@.X,D`Qn_OCqW;DgSBͷ(~,1\ }FqF{c6 z\8cN#`.ۘ/6=r|AWM+#@Mnp. _ivk|q_A oaX%3'S YK[G -҅XcUZpI{{Zvi}bjV,BDDDDR㉌wfH21izv>˫ j<뎿!#k{}]?2oM{~V_5v>\vt%#,f%Es-/WT*3k I p'*6 \Jg)NIqw91oNozM Hu~#0{ ӊ4Di;dzk=RU%r2a~ }R%xJ޸9ؘs,1I0Y\E,N(}W+.(덃s1:Mp4&cѵK/y_ -fϣgϴ.՘gF# gsxԄd"p Rgbc>WȾӱj<@.Y&Ns'DOuDI{>FRCwu؏ ԝub͖ϴgL{U,UVH8j怏9"i8Y$ $o,?r)<"w-1XHhTE4۫eܿ=+S,M'""""r7/*v}=S{>˛'s*މ&eV|\7[?ڞk$]cv9:?I*oWP4^./rKWn6ju@ĝ'bי5_68Zy0vCll@XZd0@ """""2}=*>t,.kqlz Nh7/4ӯ-!;c@v`IXbe8ge|pI{ ;2zGc5fCVU#ԣzVDDDDD-*Đf>{EkKjZV.;hIHZHK* QafG* 5m8e&g DDDDDDDDQ }<?'7E1Ln[p3عt|}Ʉ#Oػz?+ڟ7>`Ԣѯ۶V"W#~U}#՟&ttLaʔ |TAyTѕ7%J-IA6f6=J.#>1p̺W|;on3g-D3ϭ|ۺ ;}JH"Vl~XѕËGӿ{wL܊X-um"hi‡o4s=:A4m>|{r7΂Ov_VMP1l"5=5^9[Փ{[t"wE(grzt-f Dst%x5`qp+`2ڳ3e)ܨ?9@Ʉ)x/&wrvGOu|17 9ۖM~Y| 2geJY"^m;{0L_0+*ubêLL'غ` /5ڞϖ8nMgA,Uִfo#\د91$G'Q6vOs|]>ܾkmܵ_=U7XZ]q\5P}+sEPx_'tLqbTsz^,ni=+ҭM.{W &oI˜Km}M| xFyBSa|y4Ԩ)[[M%L6̝lk>98sryš"唋YpB<8@NAǸ]\ӟ xWǂ9 <}_Bf0M,3W_q*3?{k|Į[/XnYR\/vx-ʖQϏ@fqOڿ[Fy?_>ۂ _cfV島<[ә;6:ߜӒ:2;e@M 9\v?/~*_k?;WO[{yɬ Baq 8hUڝۧ2v))$Fy50/9dPuj;|d.lL\-hl{Gx9_k?^̆Xޚd{՚d{{s[Tc.󸪞>|($џ~yFޘq)i䪞f܍̋/4J޿n=oU.쯬_l[6uW%l'e:hm}X/4m IDATvLu^Zg3w_zYޞޫy]]-9{ǼrtsmOY]V7;4μ)&q>kiw}^ڻ߰wbk=|d2,s7nkڴSPde鵻?1*̜y3OXK7_J%.!W5>maW=o"sy_Ys].`ʈA-V)h9͏d"|(zׂĦqlK+s/~מef쯞gdϙf"j)O)}y N&]|Ȅ$ _׭moY;-$]e[JQطu:o|a[[ǥ`v/7;BBۂ퍚1uj<⹜r ۵@.$_b 6w='hDyGA~ެ'e:hu}X51KDp|w1$&/gfB&,h6E/cD;cq^>n:v쯞08zRLTwctP b܊R'K/yϣX ]M{x"X8c:aERe'6õ87ry9BB/I{;YkYǺp [mᯏZ3LPvGb,xOq9#bWl:Őzu(b=<&46ŝIqX>+5[c/k%耋RS3EQ&/kc?imϮ/A|)w+֊f/vooVǑf쀳1{˺q*c~uyykiwڽ~m7_=maH/a)?O/BOVs>1:4d#Yi9nm*Frrԋ[:We薋 2u|۫,8QxhAܮBז|0ÄS)W +.F#nӬ>9Bvܹ!Kcɤ ڌKr2M>b켱ԾqBK/iU.7nF7Thð-sFľ" L}ržLxOE=ϩ{163?*/k;Pv^(ۯZȒygh8+,]-ݐ.d?; ^+YcPLE1x/^NNx;C>g:ڽY]ViZw|7RK^ XU}6#g08`D;ׯ˧ߵk߰WԵ$8ߟ7젻 ]gLCy 89I@vO84ǙS4!fAl3v&ϲg/Vҽ' >j2ve팚1gaNp| >J$.Ӣi8l9C:+Aɶ_n3w<6$<:_D<4vvܰ ` lێ G5B|0{jljt+cc~>2{M@hj?@NГ!0$=౰3fOZ_{u6fS'G k*Ρt{=ۍi#7PdVNv|*|\M%dɴv>}^ZYv?/~ת믽~^S^ c!1*^[j]RʟyL_c.*7*FЪOhڴqM~";y-""""" u  vI>Xׇz1 G{E, ']/giޚI|n꟝xi߿_1 xy?$\O"""""Y AqϑGᅮq0dOToLjY[aIetGO1Y\$'@a'OIl>I̵<3{դн}$uXvՌ|o=ys~y\iG.qr&/2yzs3{h\lf~ժmeL?(Bg'BU Lq#'s46%sLTTO_}^eпIP/&>ƌk y-olZ/KlZ7P";.YgQ8$̬bqU2y*J}F+ ud8t|+MHOuEБ&Z/[N:ܵ"py#;,3~[a:skREt2鹿εHGY0/\ ߝ߻"P29sf^ն-UYSŠGڴ.jb'A """""y,eoxu 1ߵz^I87t*n!lgkl7>dwNwq6_gjfc KeldPᾳ$+**)*nEJP-QP,ɾ 0f 3̜a8s]s}׹еg71 w6iݪpp/5]c~ }r\].XS@ """""Ii$8HO qwG>uCO߽8#M}#hхA;{8̡_y_LE/ck&ŧcatzp萺cJ ͇zv; JdD """""rўq?ToOq 9zJO18 ǃRX,_}ȆyLEڵ"Y~M< P$6_?(쿦! PY۳o, l}8Kt29d]qWtղn,i_ [u,鯿_un5ղl;7KfI?t0K:X֙O֯˒OdլQ,G̒>u,?lCR_K+9W~EDDD$ $$r0) J9IK| S|ߴ?{Gofғ+fzh%.>0k>N-1ߣE7=uC}ܮ8GE6ګK9pl_ϯ@ͻS7K:@VN]B3xw5uAw5H~ o8?L$Tn3l =I1iB?H&Ϲ1n߁ђylCݜԌPJSk{O,Q8l>ܮT(KN 7:ZvC(x;y+Ց?}ΈV{xaITbUhvw%iL@X׮ʒ>b#_!&c+^d;dIt`YҫfYCrCYAez:635s.KQIɄfxxu)fS =ΖؗNHAUyh! BC DDDDDDDD """""""rA """"""xE|||8&hǍǍ;!_! ro7ԄwdK+ }Dtqq.y-^[nAuͥqPN/L<I!~6T "R`$IX7M71>n%zt` :^?D|2.a va1siKZ!^7+{F 5Mt6wUNh,m1<>h&.Q%w Aj y͏/Ob;:'Jע.c;^d %jeSgR.R՞wN/m(LhB.yOn7g|$?]Ɗ%*b4mQL)(5wduYĆwѦJww2Իu$_~&ej?O}yT-t/5;ɲiȺ)YP%jkd73(p=[CRvD*ӘJ@ϋٕbTߗnSAhk^4/C_|kv'vrVpR)#Ѻ:;qҗxzmp[f\sw[miY:Qfڝx|&}cv=+RfF.=BrGvu[ktkVy+P}[/w?v3Wr~!K~Ae طoh 8+ũX#SֽokqEI+y xI)4s8Noe\Ǜ6=6sT*'‡cYt>Ie⇇B.sR\gOۦq|k;Qe k%!VZ`?2$]qw6͇)G=i9p*[x S W[qnROW~{=.^ך7Mm||[JZ_ھ跏a?I)IaMaeR.6c; 5_{lG̭{`t)V >Q׷`]_w^|x8.ncA hgu6}Q'ğ]42p3QGl\GG DDQ˝N.&_Iؾg-^]ߢ]TF@/8xd-T.M{nf>p8FArWsĨ_i$8HO p/`x遴6⚭La>lje;sv}ؑc_^eQo>C+Z{"|/:.k4@DH对NMT.= y|-TF|o_딏=vv9Wy+P}[@㮟eōFc_^e-!ԫҮm, ԋ‘|!'},g=ʹ[D]wT.-nxx8.6.F@P qn~p-kA N[þ$TwI?~9rxU:y9?lp_lxJ~Ⱥ?*X\DZus"gUh$ w딽 D6*9=PHF4vYȧ][qj 8ERJaU ?+q.O<cRʀ  S.InbSTՓRU~Hk<9=kW_$))cǰʺ\l}f,cǑ!܈J=-sӝw|9ޓɣ{ggo|4MM*#/T(s_?2y*ky B1Q{LdvݸvJj~l(AUdG)惛)![M xz]vK1~* ̡͟_{ai?qJ),Z}T3z}}}WrF|tF3˰/9xrkϐɓ+ſ?׃r*#Vԗ?L4n] p3MfLb>c{?a1}is7?SZ_8/p[/㮝xe3?cW`t IDAT2?h:~y?m\Zj}ƑycXW5sf˔(el&~X&/8w$"""""""?G?]iɟE7EQ\)iL`U:"""""""W}b """""""@DDDDDDDPA """""""DDDDDDD5j @ """""""@DDDDDDDPA """""""DDDDDDD5j @ """""""5:""""""u:C|||9gԢ֒.eƒƒɒv^2}[ҍ|Ԓ.R,St%~Ϛ?Ҽ|tg%>H{ږIKz5>wؒ[ռFKϭ%%K|er=}?Zҕ,i%}ܒ.e)l4@DDDDDDD< Q) 9K-\ {:L20hY'}r.zg<+H 4CҲ]LCJX.ʁ^gv"3oC ~+Z;-31K:֒ew%mBjҺՒnbI],,;,m^%ɒgI9/Yw%]֒ne}.[7￳Xͽ*Kz%]ƒe,ikR^x_{9ޖ>Gk+GZocIxoK?ieg |nor^u7-,^9s|Vq}x楼X)Jܓ _{[2ݳxEds=d {p8xsBQ[#ug^1Ռ")H.=y@7d[@Q#!xDs<EkI.ߥ'{ @ꁅLcp""""""+V*x2JOe'qP~/SCe`8'K>zM E """"""@柋'Cz'?71\80de3$g"S^2rcI/cuLwsq}+-i^ΏE:ٶ z9^[1ZA5sκwr<1彜T//Z!\z=X}\uK볎;^K5{c)^ƛ߽,_K}%>7{ϫs $z)e}[;|mR%ID$OCa:0%2a@yr p{˩u&{&jUEh6} `Pu>Ĩ;aɑKwI V*TBݬH5-O" AEoN_|tbm91ch{`:.tՍIʬ"NɃx'hzA׌DwxCN\'"ug-1?dtFwE.T/G#/ZVDsu V{\DӪ}5Bm}o A0y,߼g++N`pXYOt-O16-fҿZ. }ٚݸ\.)bY"]h\27Zn k;gc-fA^̭D˺Yd?gLe%גJپLD|3RsyroGv`/yq_bӻ(K),^_†L~)%$To-^ מ|5i M ڍWdl?׃o6 }g1uڵi4hMPb;6׌nֿvϟȸ2QMB*oyPo w}|p/\(dMX;74qÀH֙V9A>? 5y\e}[S25ti۩w#mT1ߒ]on1wO㕛 圔m1TA~s(͐]uChĨǫldPۙp 'v RDf<Ŝͩ[F}w )b;UBp}S%q+f 6~`y f݄չ ~kTk*zbZ&ԈAq4:,*Ow16΄ݘڭ/C輇iS+dQvEtUeOwNl,:>ˆ]:<{kw9qdXq=U:P@u#yfjj]xo7s׾eZF3^ ϗ|e'̼͌ۢM`^i/g3M n[ڮga`k~Yi_.WZwww%{?5GQMý\||W.r-5#3/*oyҮCFwfa|sS+1\Gg%sdO'Axz3o-=CΜvr&ӏ|d;~ИkS%*q_}%#f{y.[XFxmo9{01yϦR(gX1jçξT&ŏS J_~S@&2pQgoPp_pwvu{ęt%>k?]xZH_^Yuq2Hs=} lԿ(Tr/ +sݨI[fswLˢ\^q7qÏJ/Hk8pֵ)ɇD%%È iE' Lp~a{<\Ջ0ȹo'"ZQ5,s=KʂmU4vF b^<7$۸ՠ a0m6Xz^VQ)'c?Ut17sOy:c=Yu&&҉34nʔgg)xz _]] h2v5!2(Ȋ=IF"{mGǫ:lJ+oHA?w㮝ǸxuK$ _ jI۳5p\>~Ja9L 7L24j߀0~χ$PSq\f  NAlffe\fgvNrn5{2{i=re1{,ٙ HS/ r_|iV'YK+d*v||.r̮g7W0A6&refo\l::Ìlw˕`-| |jpWfȠ {=|wW}pɵ^Cl9;QRi|[f}8qyk e¹f~2;2o3cg7;^iϗIl?׃v&򩞶[*Gl\ƵipWk'2yz{ut`o@9WyvyߟG/xuEK ,k:Mf1f; {5&{}Ww 1A """""""@DDDDDDDPA """""""DDDDDDD5j @ """""""@DDDDDDDPA """""""DDDDDDD5j  B:!>>zAՀ˲D+K%]3p%fIW}jDKz%֒lI;,|~?o݃8~""""""q߂(p癪3ٿX gZro=3xs$aA@DDDDD@K`30XdZv+,0cv:.mF܌H^DDDDDD7E)@ ܕL˾ 4B@o=:.4=˒_a% taIfIcX1K-}ll<5^cUÒ^gIKs//ʒNr<ΟuX,/,/c eGrXKONr7=\$`0ؠ!TF7eݓ]myA """""N`C=_)C~O#@$I\ڸ0wWG@^o0{DSE'g֝5uΣuꂋq[Ee}y<{Oq @S hxPԹVGݙo i5qP({H^dL  la#C.WpσХD~yucFR,dҴK/{k4/͖t~&"#Fp\l]1Wݘ2rg:m"-g)-*N{*ܿ$e/EоWWQi.+fTKa)]=By杯YW+\O)nu弜;C{ɓ'sC~m)r֜%L< y rdAނ`$r(wFd@ "ђkr^9uTI{z%-[ TF) gZg .ۍImn-DP-^)\sXfE$ӄuODjh7|ƾZ!:9KR4!8z5E4 7O^GCHqW\~c:1޶Q1w0bs *uzՂxW l A.8f& GFޯQ41;]E,V "ra&UҼL_R8s =:~4t[vw_]AŶzKer[)c6&Hhx?I<1x2ߌ7!:A"[~?/M9u&v,aq )RB(Syj ZVDsu V{\DӪ}5B}n@f.єclY-( k'cژ7bNsa -os`R IDATREh *߆c潞>[Y1wǞQω)ٽܦLwl]l/I>cln\. b|^,].4`.}n_e˅˵3}YK'Gxy=]7 f} 7|koΐDygUxzk^]8f@[;KqZrwoGv`/yq_bӻN ,Ih{} 2,Jxk\Iib]nw<-u~%Z=%9+Sٷx RqWLS\= hr=Ј69B<> o_3κN[RO=v#bTF5e O18Ź)~/WRAϯnItKū+*\rT?61g`ܟߠU"/MV9Ҿ $ :@ډ9]U UE rwApM|,oGqTLyxv*Ds/~ sǷd[[*{Lxhu9'oA=ckФ߇8J=k3dFW$oݐ"1,م1Tv&ĉ]){vÁ<G1g`sjVQF5%{Ef<;ໂxRL|X){n~廏GsO"_7"Эm7kE类qwjV3Z@W]S6Fl }}1oLqhxWi~z;U!Aw&Ɣnƿ|APiE?R!8V¶_]fyӦV,՛=ȢRK=m3˞HXju| 5ruxrvϟryhGʛ_R*=eqz沽?{-v㟟Օ_.y VݕRחIn (pDjr{I")QeҜ"4@Jqq`&"ڶ@Qg/-yѼ[p0rE[}+g#[fV~Tt\P53x*3iDD0lLm:D洯l*L,mǴؿ*EY7oW/ iuzլexe*Y7)h-o tkc8MHUAaffr2~F 7GbvmmOhf".P/w#; ̈́",lYSClCpj3YdxGM͠ ﷣fڷL_~mXCH-3d.L\tG3]nz063n6yf_6.>nk{mrv+9ӼX@?w⮿˥ㆿՕ_.+T9b/m?ewkw|#R˸/6Oux批ۍ2?x +ϻ.lԕS<%3ӃZ|lh䗆3d@:__)kKKX,7']Y;[oFP D.~~"7[c\˩ړ孴?8\Ϸy8m~"TÜѱ̸{(AP~A"I? =uSX7%If㬅$F% 70O仭Y{*BS~v<[_>2 Bsnƀ sYeHҙw)iAfR9DxH+P l#6_]ffX'iy ec?{[zyyϋʛ_dبTk˫nQ(k^-.Wbg̏xuK$@leO4Nq <7ɖӁ!gNs9JfI| 7 /зz 'RO`ޠfD'6L獯]Y˟I?AcZ4MĵgǖZl{y..#<ɶ=ҽ-R(W*'^Ō>=y*@<[L'm[΁b=( Y'hP ws}8l#3oC=/0=''Τ(хOɴ],v+FAp\F *]vGe/3y"-%|zQ_篣PydoϘeh-|]VO/ry\ ?ƫ+-\"UCFZVh&X"aD4¢s$"1-Lɂ;yqձ$Z sMA=?b[8bY&E v,=FWEKXŲuX|ΆpyW/œ#jVT ǜsr+UZ|Cه%#~S=<q:y뢐TGd?GZZ&H~(td $/f )jF0ȹAE7ٛtZ!AȩUxUF *WƽRXUVYv$c=귧FX._Aʩ1m*2 jۆ_a\N~w=h&O](,hviqd7;/˶qulA?c㮝׸xuKZ=|b4ms%KS Gp%{Xh}H ï>k$:m﹃, N8u 1l̈́Sxw[]Uqڎ ?1:O^MC_ 'Ec+uL̍/?Kzф9(ӠϿҍbs$ɤ;SJ=5+U (JÇg0?W䁞pP#{NXn㞧W29È׃gGe۔Oc ){YGuzv#28pz|;7Ẃ>agաjךH'ΰԺy(SǟާW%Ī7f0GILۡL>\Mi'w=m'VdkCdP{4E{=m<\KL"ӑG Ԓp"?w㮝ǸxuK$ _ jI۳5p\g|ΠX= A)RF_\]t֌aۺ%(u /Hl(Bng N{ɹq}1I#Tlqynb>=ͧLփͤN˸\;&*"M>ckeҌ{dc',gg208"Mc$e~a^~ŦYdE/.I;+f/ךi2m{7˸\ %nq&‘I}x9Ìllw7N}̧I |e `ްSqw79713{]y)m~,޸'7.6o L8ٌOfG-buu?B 2;>c>yq\fϳ+ܖS=m3U:0~wkB3zُNeP{ut`o@9WydpC?¬ƠC;J!4?w}}} ǫ+2\W`Yi6c1{޹Tܫ4`3Aƥf~g' : ]Kss˟\e6|;Ӽtw98Pب_aʜ1zv'un }^XKΎpb """"""KiDDDDDDDpi4@DDDDDDD4A """""""DDDDDDD5j @ """""""@DDDDDDDPA """""""DDDDDDD5j @ """""""@DDDDDDD'kt"DDDDDD t .Bs,A """""""j !ǁVYUK:Ēn++loc]_cK:-qQ|QeO${N@e}UÒ-魖tK:GdO!Ғے^xz9^+cIhIhIWq}M㼬Ϻ7tU/i+Y$-,H*.`,g}=L% OyJ*o/;u98,з@DDDDDp ]qKXt!T6kvݾ5u@DDDDDDNp{ra@[e`?!M Cmzu9qj7=)ʼnѓxH/jX--iK%Yk|߿r(t]kIo[G|Ux+fIGyI[Y4(bIaIksSrI`(>=Cq9< MhxA~s ⑏3jFSWW\DDDDD$ `v^&}\x+=''J_9θTC]z": \XȤi9^8ש+."""""GUc@Ar're*Á;=[{ \/:7A """""O.C>.g,ktZ%eϲ1n*dTkvK:=Y-i1-鵖uwt uf',DK-F^K%YWsoâ8C,(ztaDn$K15E-F/coXcĊ49{w8ް;kۙY݀:dzS a&0)sfM.x @ @ <+26\d (ge&2-01[ @ @P,Q2c%3㼠" Js@ @ {䦒54({?Bo~m^W˽ J\J"@ f@jauE@ R @ KCsAa! @ @ @ @ ^ h7UDaƷ/VZZ-G0H#kg,6g&>j\ (h3=(6DL3MeZ-!GerJ ()[| eEke[e@pl<ȅ y׀%䅥/gS8|,y\TQrSzƼe7g:cLѸ$MUqwq%DM5lnӐqC9=o^um&n\A mG&._ͤM]N54 RX3!g&AUϪ,aLU 8 1@/Ry׏W2d|FW@:=hw o,cX +(\a OPI bΨ6@j'A]T#NQc>fѾ*7} [`-+ b; Otb??ǩ䐽|7i mּ F*hJ\zϢ땉z$x.JI](0hGm '=~hŦ#1@`L_#/de(v&ØnF ~%)0jnn?\˒A,  s CT\ͨ$ 49tVͽ=꡽wvé,r71BA`Vɏ1KG̩>9uctC&ौrڨMxX9L7? CagH-?JlihW 07rVxʬv `fAPRLUjhݭ L<&?Vɋg}^݆;rDj nӲ]g3qL +O]gjцǎ#h\gl{ZpY8"5L]߳Zp0@dˋ`6DŽ{2jT}Jeĭg;pZ- ͧOi'9+eep~зbR53lHY|P,pqOř|Mc# B}^#\͌g^)y)o^{?²ª2o4*Hd(J g 1'g3W\5hMqqvūv{Ʈi`U}7ua3BEMw|z|OUoWS-RnƆwQ֣͋?Z?iH6Gw:Yt wIŦT輘 cm2uyRZLLP'K%q8 E\u+Ȥ6>LhƆ rLw7`6bKrT)qU}Y5!^n^4~X9%.cϱ/ߦnej͢;|lvH+{Z0!廬7cpR؛# cFƣU5լӲe'GU*]ae?-'d>G(36k+͸ف2l6Lgڮ{(=PSˈ5Яo?#+!//89msR[ $bŒKO_&zps?<]$sςe~Gݼ;+Nݸ%\T"rJWi`igIğ$G<1{NJ |mrԣnH: QumKzRciRs.uJZTyՍ>^q6RV+iL ~Kщիn-m u(WyYq^ZDM҉]$wj-MJZX:T,VwFOW RRHXT @.^ #/ 6t+/O=n҈NK\ei,9зd|t:xQ)ߏ;RŎJ'N/:7-X+n^ ɭ |T`i/RQSN_$3PznR_Ok%6\7gEu: JZuWV#9_(aRi ץ™Ao"'?c~tnsʍ&ټGKEQ1ztCWs1|D%99T%ѼTVGB QGYqW^@P<~C\:n|;9v"WJ"6Io qrΕWq|\T&+S&W➲WQe|dViߘዶr4 Z3oɵEiO3R*fXY@%qǣ"`Yj<)R!ɋ`/OEJ#:d$ei,9dзaYqs:5wt%?>5^T4-s0x^U_}1+-//~@~S6 1 +lj$R~M+w3ڵN24#|]g;*-1{-p4ұ IDATwJ[?"pzkjk@pw@+ڏvr?%:ɬ#Xekly$$o!lB %\1h5q(Mt-v!F&JPP ̈́C8~x]:2]=_zۿŘԻDeowWttwSXW.y1V`?]YW~9XO!ni;03r`K:^XϝЯ7QPhvEګWV^^ 6֨Gr +IP&W  >P0(ͯ1e E(gS!Ug񙾗@b(GQf>B"9qWf˽wsu=}FR)*զe_,]=1ʓUlr/+":-HI3Ly:7zH\ګ)>/1d4>FWhoj7lau~u]?IA'(K9asܓ,}p^\ag QJ6] G;5.5@=)^,1P`Q|ܷP*Q{ӢkkF tټ$>&ɧj3%\qϧZrlY~|NZQ)UT݇/cgZ£`47+aľffJiS[ u3歝ÈbgRiM`l^_ xU|Cv)\]5bb{BiV q27_fޟԝ-QqׇY Q)4/S;|IP)UՃ ϝM:/;7i7o&dث>=bn;n̵$Eq'EDj |ӓݞH6\P[qi5E' ġy14#ټ$ -b.P;6O1@嶳xL]B+[Qb[/;&7vg`|s0zȒw=ք=2/nIU:Qϵ4JjѱؓJ_2h)k9#)7Mj(RvIT& Kɱ`i=RPDF/J/.5)ULrh1FZtMk҉?KcZJ*LR~mKZV KZ0C\=Dݼ $jz߹ҶV"SzHϋJ(Ca!i fo<,gŵ;&-`>~kkVO]L|7Iv?R[iƣUVj#3 O=&;q/>ۥ}$36j4Q 8J{1?J{_ϰH?i"˵ᜅKgioǤ{}sޟ\gl{Z3ɱ4iPIJ7JHfn%[^lҥk;T{(V^GZ4+GvJ5nT( c۫WM^0-/1jKb5 vޝLz]:CZ5c9oOiVO3uߌ[gݤ-6B3,!iRYK뒩JaldI /Nky@bjO ȝ\/;uT"qƥ iF)s%֕0`T;H@ K]o{:9P c/oI]\ 򞹴 F ZG @ @ @ )IK ZV@ ɭܟ{Iq@ (b^"c]Xb @ @$@ @ dT#y/{eo0}[f5h)/:ށ38RA{" EnOh1؝W}}[Q"%w #ZGpbDTIAXf|#ID<;/0'M3ڻ =p%A*ݐK0鞣[p__enSr9,^k'p[fPE6Dc,cNSܧtj.x ,H/˟ ?eTZQvfmDsϘz_MhZQWr P=L-lT%fgv& !U~ K0n  ˂S|+63fdT\EYV\|֔ ኌz$-0Wc֌_~Ñ_9`s;\fl^W>; j dAV>Ⅴ^&}{ HE3AޤGQ 9NbEeZ"~ Ke,܏cK)eX >$yh~ͪfL֊?!teoѮn Me?>b=1Gi7xtWu,S߮0g<P9aN|\bڧi y^) R/b:zsCJ&>E$5QSw4US$Bܪ00a(.pHNOx֛<˷F=CU:ƿA[B&ozKciqubs97 0b_tL‰o:)ۿ0\3>|@g35Y/_{oLTdG .) &c .j5]kufeew'$2p ̀{hDcgq_VQCrO0%z7NE؂I85쥌YARɄu=}J1['au7(^ ;Aa3hVJA ^A"reHR% K?: \56 | 3wi\A3f+|<]y)R.xyCDDs_,TrB_,}.$E!F8syptf.g)Z]VѨAz6eAN2nSܴrR_kBw}쮒 | mJq7#iR\Y{q]cYg 'lm]47z49+I0wr9s 8Y<9@Ru#leg`Ofy3`b@ xQtI^Gf_𞂅@EbjRm3ެ'͚ŐnC\J@7N}{giR1 *,l]dG2֏<$_Ð5'I`{6Gs|(U #nceY:ϙ;SBXY〵5u0k^p-@55!sM*Xc}tl )?Y'oXXRFf~Մe;*1`Wm茭UZO\wl (ExBij f4+8zV>IUޝ%V4>$Yȝs̘ JYC2guʤJtYU/2PcCR>r`C74D # H=^;@aG"ѼQ 7}TݜݖURbnIQ#q+M '`Y0-΢W X[Z_f/𧴡}#yul^Dӹпs}z2sϮ7?35*dyڝqבQ}$W+)PSE}c=Wٍ{0? 'F00CDJ[YTSƶ)v׼b5> o駍ii̦kX[Qv/fmIS0 ֥xG݄loI.hb '00spWP@G%׀`(' ,W9_to+ |e\nх@ xI/͍k#Q,GєONΆ{a 3]z~2N<<\^!HTsn%`mAz2Ѻ\5o*Z5 C@JsxfWmg>1-ecyb:[ HAGyl4AڼvRUoo_0t{+M7-t#Z8S2g,GǺz(ߺb_?E潼๨5qs]5pwXORFYO _QiTMDO%LB90ž/NsG#rOitߺYm~CNuwYAW|X{ 'f7)Sw54lUMmGZT1ƫ G_Ė`".۾=.-驿r˕+;ukWz-'㑤ЇlS]bFtƉ(kbiEJ{ G O ͫ?kh"2kf7Y1 &:k- `"<%Kȱg#cfBfu2m 9֞gUf"^9?[2&'b_sU֗];>̽$9G2@iaMC3MR^Xjvnyr1ڀQd,0&#գTqOuܬIk97d£2T٩leߴeM \Np ME0kz w;OŹ'8t"3?/ze?[m?]KON%϶rupŒT4žKqTI 8DqǙ:#|3KRV,We+!!KGzj(t];e7\PXR8`NfxڄmD;__HH{G|_KQQ}ҋBbw [54ȱb#Azj,vrƜnˬOugeTL6s>6tƀ3 Ħ@N-_!UCxK;2l출_PmxU}4Mg3wz>B/ G>yx]wmbĭ<|V/+RxpuK&PJO]uuD1wR>"EBtA2ݳگ\PU5z3$K%z&”ꇎ[ǏX1bo姟/J?"OYvԉ ܑpٮH|E?2!lXnp h{` Ɠ񅃳dlRxL [g,2+,2(KCͤIΕK0ɂԸ\Ei(Up ~Ԝk4SAM#&~А؝_긻l0ogaxۼƻmXٿ>]1~d_D*ˆ|ygOf'(jpri/kя5[Va([2 ݺz`9]ߢR5uU4\^<8?oW0(̼r6V.x֣2L) UvP<][Xm쀹>5~BƆ]eAG*)-,w6|VzYB=.zVbz+A{?}ZM6l3#mjFuQ*=vϹ]]~tP>w4,!kdvBIDATRo1p6}W c96o ;@p]D+{1&Qv^Vy хO^5D C9fjh$(LjXzK)M{@ `^"c]`ƥ+B݈-fD{{b1 #:EGG9:Cw-~CoDrpI҆9,O7Os@ 2b+9U>EKeX#LؚQzТ ~T+:?E9}EK.;:vz@ @PPXcvXB݆(1^ %\D3@ @j# @ @UC2IENDB`transmission-remote-cli-1.7.0/transmission-remote-cli000077500000000000000000005002611233471375700230430ustar00rootroot00000000000000#!/usr/bin/env python ######################################################################## # This is transmission-remote-cli, whereas 'cli' stands for 'Curses # # Luminous Interface', a client for the daemon of the BitTorrent # # client Transmission. # # # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation, either version 3 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, # # but WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # GNU General Public License for more details: # # http://www.gnu.org/licenses/gpl-3.0.txt # ######################################################################## VERSION = '1.7.0' TRNSM_VERSION_MIN = '1.90' TRNSM_VERSION_MAX = '2.82' RPC_VERSION_MIN = 8 RPC_VERSION_MAX = 15 # error codes CONNECTION_ERROR = 1 JSON_ERROR = 2 CONFIGFILE_ERROR = 3 # use simplejson if available because it seems to be faster try: import simplejson as json except ImportError: try: # Python 2.6 comes with a json module ... import json # ...but there is also an old json module that doesn't support .loads/.dumps. json.dumps ; json.dumps except (ImportError,AttributeError): quit("Please install simplejson or Python 2.6 or higher.") import time import datetime import re import base64 import httplib import urllib2 import socket socket.setdefaulttimeout(None) import ConfigParser from optparse import OptionParser, SUPPRESS_HELP import sys import os import signal import unicodedata import locale import curses import curses.ascii from textwrap import wrap from subprocess import call, Popen import netrc import operator import urlparse from distutils.spawn import find_executable locale.setlocale(locale.LC_ALL, '') ENCODING = locale.getpreferredencoding() or 'UTF-8' # optional features provided by non-standard modules features = {'dns':False, 'geoip':False, 'ipy':False} try: import adns; features['dns'] = True # resolve IP to host name except ImportError: features['dns'] = False try: import GeoIP; features['geoip'] = True # show country peer seems to be in except ImportError: features['geoip'] = False try: import IPy; features['ipy'] = True # extract ipv4 from ipv6 addresses except ImportError: features['ipy'] = False if features['ipy']: IPV6_RANGE_6TO4 = IPy.IP('2002::/16') IPV6_RANGE_TEREDO = IPy.IP('2001::/32') IPV4_ONES = 0xffffffff if features['geoip']: def country_code_by_addr_vany(geo_ip, geo_ip6, addr): if '.' in addr: return geo_ip.country_code_by_addr(addr) if not ':' in addr: return None if features['ipy']: ip = IPy.IP(addr) if ip in IPV6_RANGE_6TO4: addr = str(IPy.IP(ip.int() >> 80 & IPV4_ONES)) return geo_ip.country_code_by_addr(addr) elif ip in IPV6_RANGE_TEREDO: addr = str(IPy.IP(ip.int() & IPV4_ONES ^ IPV4_ONES)) return geo_ip.country_code_by_addr(addr) if hasattr(geo_ip6, 'country_code_by_addr_v6'): return geo_ip6.country_code_by_addr_v6(addr) # define config defaults config = ConfigParser.SafeConfigParser() config.add_section('Connection') config.set('Connection', 'password', '') config.set('Connection', 'username', '') config.set('Connection', 'port', '9091') config.set('Connection', 'host', 'localhost') config.set('Connection', 'path', '/transmission/rpc') config.set('Connection', 'ssl', 'False') config.add_section('Sorting') config.set('Sorting', 'order', 'name') config.add_section('Filtering') config.set('Filtering', 'filter', '') config.set('Filtering', 'invert', 'False') config.add_section('Misc') config.set('Misc', 'compact_list', 'False') config.set('Misc', 'blank_lines', 'True') config.set('Misc', 'torrentname_is_progressbar', 'True') config.set('Misc', 'file_viewer', 'xdg-open %%s') config.set('Misc', 'file_open_in_terminal', 'True') config.add_section('Colors') config.set('Colors', 'title_seed', 'bg:green,fg:black') config.set('Colors', 'title_download', 'bg:blue,fg:black') config.set('Colors', 'title_idle', 'bg:cyan,fg:black') config.set('Colors', 'title_verify', 'bg:magenta,fg:black') config.set('Colors', 'title_paused', 'bg:black,fg:white') config.set('Colors', 'title_error', 'bg:red,fg:white') config.set('Colors', 'download_rate', 'bg:black,fg:blue') config.set('Colors', 'upload_rate', 'bg:black,fg:red') config.set('Colors', 'eta+ratio', 'bg:black,fg:white') config.set('Colors', 'filter_status', 'bg:red,fg:black') config.set('Colors', 'dialog', 'bg:black,fg:white') config.set('Colors', 'dialog_important', 'bg:red,fg:black') config.set('Colors', 'button', 'bg:white,fg:black') config.set('Colors', 'button_focused', 'bg:black,fg:white') config.set('Colors', 'file_prio_high', 'bg:red,fg:black') config.set('Colors', 'file_prio_normal', 'bg:white,fg:black') config.set('Colors', 'file_prio_low', 'bg:yellow,fg:black') config.set('Colors', 'file_prio_off', 'bg:blue,fg:black') class ColorManager: def __init__(self, config): self.config = dict() self.term_has_colors = curses.has_colors() curses.start_color() if self.term_has_colors: curses.use_default_colors() for name in config.keys(): self.config[name] = self._parse_color_pair(config[name]) if self.term_has_colors: curses.init_pair(self.config[name]['id'], self.config[name]['fg'], self.config[name]['bg']) def _parse_color_pair(self, pair): # BG and FG are intentionally switched here because colors are always # used with curses.A_REVERSE. (To be honest, I forgot why, probably # has something to do with how highlighting focus works.) bg_name = pair.split(',')[1].split(':')[1].upper() fg_name = pair.split(',')[0].split(':')[1].upper() color_pair = { 'id': len(self.config.keys()) + 1 } try: color_pair['bg'] = eval('curses.COLOR_' + bg_name) except AttributeError: color_pair['bg'] = -1 try: color_pair['fg'] = eval('curses.COLOR_' + fg_name) except AttributeError: color_pair['fg'] = -1 return color_pair def id(self, name): return self.config[name]['id'] class Normalizer: def __init__(self): self.values = {} def add(self, id, value, max): if not id in self.values.keys(): self.values[id] = [ float(value) ] else: if len(self.values[id]) >= max: self.values[id].pop(0) self.values[id].append(float(value)) return self.get(id) def get(self, id): if not id in self.values.keys(): return 0.0 return sum(self.values[id]) / len(self.values[id]) authhandler = None session_id = 0 vmode_id = -1 # Handle communication with Transmission server. class TransmissionRequest: def __init__(self, host, port, path, method=None, tag=None, arguments=None): self.url = create_url(host, port, path) self.open_request = None self.last_update = 0 if method and tag: self.set_request_data(method, tag, arguments) def set_request_data(self, method, tag, arguments=None): request_data = {'method':method, 'tag':tag} if arguments: request_data['arguments'] = arguments self.http_request = urllib2.Request(url=self.url, data=json.dumps(request_data)) def send_request(self): """Ask for information from server OR submit command.""" global session_id try: if session_id: self.http_request.add_header('X-Transmission-Session-Id', session_id) self.open_request = urllib2.urlopen(self.http_request) except AttributeError: # request data (http_request) isn't specified yet -- data will be available on next call pass # authentication except urllib2.HTTPError, e: try: msg = html2text(str(e.read())) except: msg = str(e) # extract session id and send request again m = re.search('X-Transmission-Session-Id:\s*(\w+)', msg) try: session_id = m.group(1) self.send_request() except AttributeError: quit(str(msg) + "\n", CONNECTION_ERROR) except urllib2.URLError, msg: try: reason = msg.reason[1] except IndexError: reason = str(msg.reason) quit("Cannot connect to %s: %s\n" % (self.http_request.host, reason), CONNECTION_ERROR) def get_response(self): """Get response to previously sent request.""" if self.open_request == None: return {'result': 'no open request'} response = self.open_request.read() # work around regression in Python 2.6.5, caused by http://bugs.python.org/issue8797 if authhandler: authhandler.retried = 0 try: data = json.loads(unicode(response)) except ValueError: quit("Cannot parse response: %s\n" % response, JSON_ERROR) self.open_request = None return data # End of Class TransmissionRequest # Higher level of data exchange class Transmission: STATUS_STOPPED = 0 # Torrent is stopped STATUS_CHECK_WAIT = 1 # Queued to check files STATUS_CHECK = 2 # Checking files STATUS_DOWNLOAD_WAIT = 3 # Queued to download STATUS_DOWNLOAD = 4 # Downloading STATUS_SEED_WAIT = 5 # Queued to seed STATUS_SEED = 6 # Seeding TAG_TORRENT_LIST = 7 TAG_TORRENT_DETAILS = 77 TAG_SESSION_STATS = 21 TAG_SESSION_GET = 22 LIST_FIELDS = [ 'id', 'name', 'downloadDir', 'status', 'trackerStats', 'desiredAvailable', 'rateDownload', 'rateUpload', 'eta', 'uploadRatio', 'sizeWhenDone', 'haveValid', 'haveUnchecked', 'addedDate', 'uploadedEver', 'errorString', 'recheckProgress', 'peersConnected', 'uploadLimit', 'downloadLimit', 'uploadLimited', 'downloadLimited', 'bandwidthPriority', 'peersSendingToUs', 'peersGettingFromUs', 'seedRatioLimit', 'seedRatioMode', 'isPrivate' ] DETAIL_FIELDS = [ 'files', 'priorities', 'wanted', 'peers', 'trackers', 'activityDate', 'dateCreated', 'startDate', 'doneDate', 'totalSize', 'leftUntilDone', 'comment', 'creator', 'hashString', 'pieceCount', 'pieceSize', 'pieces', 'downloadedEver', 'corruptEver', 'peersFrom' ] + LIST_FIELDS def __init__(self, host, port, path, username, password): self.host = host self.port = port self.path = path if username and password: password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() password_mgr.add_password(None, create_url(host, port, path), username, password) global authhandler authhandler = urllib2.HTTPBasicAuthHandler(password_mgr) opener = urllib2.build_opener(authhandler) urllib2.install_opener(opener) # check rpc version request = TransmissionRequest(host, port, path, 'session-get', self.TAG_SESSION_GET) request.send_request() response = request.get_response() self.rpc_version = response['arguments']['rpc-version'] self.version = response['arguments']['version'].split()[0] # rpc version too old? version_error = "Unsupported Transmission version: " + str(response['arguments']['version']) + \ " -- RPC protocol version: " + str(response['arguments']['rpc-version']) + "\n" min_msg = "Please install Transmission version " + TRNSM_VERSION_MIN + " or higher.\n" try: if response['arguments']['rpc-version'] < RPC_VERSION_MIN: quit(version_error + min_msg) except KeyError: quit(version_error + min_msg) # rpc version too new? if response['arguments']['rpc-version'] > RPC_VERSION_MAX: quit(version_error + "Please install Transmission version " + TRNSM_VERSION_MAX + " or lower.\n") # setup compatibility to Transmission <2.40 if self.rpc_version < 14: Transmission.STATUS_CHECK_WAIT = 1 << 0 Transmission.STATUS_CHECK = 1 << 1 Transmission.STATUS_DOWNLOAD_WAIT = 1 << 2 Transmission.STATUS_DOWNLOAD = 1 << 2 Transmission.STATUS_SEED_WAIT = 1 << 3 Transmission.STATUS_SEED = 1 << 3 Transmission.STATUS_STOPPED = 1 << 4 # Queue was implemented in Transmission v2.4 if self.rpc_version >= 14: self.LIST_FIELDS.append('queuePosition'); self.DETAIL_FIELDS.append('queuePosition'); # set up request list self.requests = {'torrent-list': TransmissionRequest(host, port, path, 'torrent-get', self.TAG_TORRENT_LIST, {'fields': self.LIST_FIELDS}), 'session-stats': TransmissionRequest(host, port, path, 'session-stats', self.TAG_SESSION_STATS, 21), 'session-get': TransmissionRequest(host, port, path, 'session-get', self.TAG_SESSION_GET), 'torrent-details': TransmissionRequest(host, port, path)} self.torrent_cache = [] self.status_cache = dict() self.torrent_details_cache = dict() self.peer_progress_cache = dict() self.hosts_cache = dict() self.geo_ips_cache = dict() if features['dns']: self.resolver = adns.init() if features['geoip']: self.geo_ip = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE) try: self.geo_ip6 = GeoIP.open_type(GeoIP.GEOIP_COUNTRY_EDITION_V6, GeoIP.GEOIP_MEMORY_CACHE); except AttributeError: self.geo_ip6 = None except GeoIP.error: self.geo_ip6 = None # make sure there are no undefined values self.wait_for_torrentlist_update() self.requests['torrent-details'] = TransmissionRequest(self.host, self.port, self.path) def update(self, delay, tag_waiting_for=0): """Maintain up-to-date data.""" tag_waiting_for_occurred = False for request in self.requests.values(): if time.time() - request.last_update >= delay: request.last_update = time.time() response = request.get_response() if response['result'] == 'no open request': request.send_request() elif response['result'] == 'success': tag = self.parse_response(response) if tag == tag_waiting_for: tag_waiting_for_occurred = True if tag_waiting_for: return tag_waiting_for_occurred else: return None def parse_response(self, response): def get_main_tracker_domain(torrent): if torrent['trackerStats']: trackers = sorted(torrent['trackerStats'], key=operator.itemgetter('tier', 'id')) return urlparse.urlparse(trackers[0]['announce']).hostname else: # Trackerless torrents return None # response is a reply to torrent-get if response['tag'] == self.TAG_TORRENT_LIST or response['tag'] == self.TAG_TORRENT_DETAILS: for t in response['arguments']['torrents']: t['uploadRatio'] = round(float(t['uploadRatio']), 2) t['percentDone'] = percent(float(t['sizeWhenDone']), float(t['haveValid'] + t['haveUnchecked'])) t['available'] = t['desiredAvailable'] + t['haveValid'] + t['haveUnchecked'] if t['downloadDir'][-1] != '/': t['downloadDir'] += '/' try: t['seeders'] = max(map(lambda x: x['seederCount'], t['trackerStats'])) t['leechers'] = max(map(lambda x: x['leecherCount'], t['trackerStats'])) except ValueError: t['seeders'] = t['leechers'] = -1 t['isIsolated'] = not self.can_has_peers(t) t['mainTrackerDomain'] = get_main_tracker_domain(t) if response['tag'] == self.TAG_TORRENT_LIST: self.torrent_cache = response['arguments']['torrents'] elif response['tag'] == self.TAG_TORRENT_DETAILS: # torrent list may be empty sometimes after deleting # torrents. no idea why and why the server sends us # TAG_TORRENT_DETAILS, but just passing seems to help.(?) try: torrent_details = response['arguments']['torrents'][0] torrent_details['pieces'] = base64.decodestring(torrent_details['pieces']) self.torrent_details_cache = torrent_details self.upgrade_peerlist() except IndexError: pass elif response['tag'] == self.TAG_SESSION_STATS: self.status_cache.update(response['arguments']) elif response['tag'] == self.TAG_SESSION_GET: self.status_cache.update(response['arguments']) return response['tag'] def upgrade_peerlist(self): for index,peer in enumerate(self.torrent_details_cache['peers']): ip = peer['address'] peerid = ip + self.torrent_details_cache['hashString'] # make sure peer cache exists if not self.peer_progress_cache.has_key(peerid): self.peer_progress_cache[peerid] = {'last_progress':peer['progress'], 'last_update':time.time(), 'download_speed':0, 'time_left':0} this_peer = self.peer_progress_cache[peerid] this_torrent = self.torrent_details_cache # estimate how fast a peer is downloading if peer['progress'] < 1: this_time = time.time() time_diff = this_time - this_peer['last_update'] progress_diff = peer['progress'] - this_peer['last_progress'] if this_peer['last_progress'] and progress_diff > 0 and time_diff > 5: download_left = this_torrent['totalSize'] - \ (this_torrent['totalSize']*peer['progress']) downloaded = this_torrent['totalSize'] * progress_diff this_peer['download_speed'] = \ norm.add(peerid+':download_speed', downloaded/time_diff, 10) this_peer['time_left'] = download_left/this_peer['download_speed'] this_peer['last_update'] = this_time # infrequent progress updates lead to increasingly inaccurate # estimates, so we go back to elif time_diff > 60: this_peer['download_speed'] = 0 this_peer['time_left'] = 0 this_peer['last_update'] = time.time() this_peer['last_progress'] = peer['progress'] # remember progress this_torrent['peers'][index].update(this_peer) # resolve and locate peer's ip if features['dns'] and not self.hosts_cache.has_key(ip): try: self.hosts_cache[ip] = self.resolver.submit_reverse(ip, adns.rr.PTR) except adns.Error: pass if features['geoip'] and not self.geo_ips_cache.has_key(ip): self.geo_ips_cache[ip] = country_code_by_addr_vany(self.geo_ip, self.geo_ip6, ip) if self.geo_ips_cache[ip] == None: self.geo_ips_cache[ip] = '?' def get_rpc_version(self): return self.rpc_version def get_global_stats(self): return self.status_cache def get_torrent_list(self, sort_orders): def sort_value(value): try: return value.lower() except AttributeError: return value try: for sort_order in sort_orders: self.torrent_cache.sort(key=lambda x: sort_value(x[sort_order['name']]), reverse=sort_order['reverse']) except IndexError: return [] return self.torrent_cache def get_torrent_by_id(self, id): i = 0 while self.torrent_cache[i]['id'] != id: i += 1 if self.torrent_cache[i]['id'] == id: return self.torrent_cache[i] else: return None def get_torrent_details(self): return self.torrent_details_cache def set_torrent_details_id(self, id): if id < 0: self.requests['torrent-details'] = TransmissionRequest(self.host, self.port, self.path) else: self.requests['torrent-details'].set_request_data('torrent-get', self.TAG_TORRENT_DETAILS, {'ids':id, 'fields': self.DETAIL_FIELDS}) def get_hosts(self): return self.hosts_cache def get_geo_ips(self): return self.geo_ips_cache def set_option(self, option_name, option_value): request = TransmissionRequest(self.host, self.port, self.path, 'session-set', 1, {option_name: option_value}) request.send_request() self.wait_for_status_update() def set_rate_limit(self, direction, new_limit, torrent_id=-1): data = dict() if new_limit <= -1: new_limit = None limit_enabled = False else: limit_enabled = True if torrent_id < 0: type = 'session-set' data['speed-limit-'+direction] = new_limit data['speed-limit-'+direction+'-enabled'] = limit_enabled else: type = 'torrent-set' data['ids'] = [torrent_id] data[direction+'loadLimit'] = new_limit data[direction+'loadLimited'] = limit_enabled request = TransmissionRequest(self.host, self.port, self.path, type, 1, data) request.send_request() self.wait_for_torrentlist_update() def set_seed_ratio(self, ratio, torrent_id=-1): data = dict() if ratio == -1: ratio = None mode = 0 # Use global settings elif ratio == 0: ratio = None mode = 2 # Seed regardless of ratio elif ratio >= 0: mode = 1 # Stop seeding at seedRatioLimit else: return data['ids'] = [torrent_id] data['seedRatioLimit'] = ratio data['seedRatioMode'] = mode request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data) request.send_request() self.wait_for_torrentlist_update() def increase_bandwidth_priority(self, torrent_id): torrent = self.get_torrent_by_id(torrent_id) if torrent == None or torrent['bandwidthPriority'] >= 1: return False else: new_priority = torrent['bandwidthPriority'] + 1 request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, {'ids': [torrent_id], 'bandwidthPriority':new_priority}) request.send_request() self.wait_for_torrentlist_update() def decrease_bandwidth_priority(self, torrent_id): torrent = self.get_torrent_by_id(torrent_id) if torrent == None or torrent['bandwidthPriority'] <= -1: return False else: new_priority = torrent['bandwidthPriority'] - 1 request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, {'ids': [torrent_id], 'bandwidthPriority':new_priority}) request.send_request() self.wait_for_torrentlist_update() def move_queue(self, torrent_id, new_position): args = {'ids': [ torrent_id ] } if new_position in ('up', 'down', 'top', 'bottom'): method_name = 'queue-move-' + new_position elif isinstance(new_position, int): method_name = 'torrent-set' args['queuePosition'] = min(max(new_position, 0), len(self.torrent_cache)-1) else: raise ValueError("Is not up/down/top/bottom/: %s" % new_position) request = TransmissionRequest(self.host, self.port, self.path, method_name, 1, args) request.send_request() self.wait_for_torrentlist_update() def toggle_turtle_mode(self): self.set_option('alt-speed-enabled', not self.status_cache['alt-speed-enabled']) def add_torrent(self, location): args = {} try: with open(location, 'rb') as fp: args['metainfo'] = unicode(base64.b64encode(fp.read())) # If the file doesnt exist or we cant open it, then it is either a url or needs to # be open by the server except IOError: args['filename'] = location request = TransmissionRequest(self.host, self.port, self.path, 'torrent-add', 1, args) request.send_request() response = request.get_response() if response['result'] != 'success': return response['result'] else: return '' def stop_torrents(self, ids): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-stop', 1, {'ids': ids}) request.send_request() self.wait_for_torrentlist_update() def start_torrents(self, ids): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-start', 1, {'ids': ids}) request.send_request() self.wait_for_torrentlist_update() def start_now_torrent(self, id): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-start-now', 1, {'ids': [id]}) request.send_request() self.wait_for_torrentlist_update() def verify_torrent(self, id): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-verify', 1, {'ids': [id]}) request.send_request() self.wait_for_torrentlist_update() def reannounce_torrent(self, id): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-reannounce', 1, {'ids': [id]}) request.send_request() self.wait_for_torrentlist_update() def move_torrent(self, torrent_id, new_location): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set-location', 1, {'ids': torrent_id, 'location': new_location, 'move': True}) request.send_request() self.wait_for_torrentlist_update() def remove_torrent(self, id): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-remove', 1, {'ids': [id]}) request.send_request() self.wait_for_torrentlist_update() def remove_torrent_local_data(self, id): request = TransmissionRequest(self.host, self.port, self.path, 'torrent-remove', 1, {'ids': [id], 'delete-local-data':True}) request.send_request() self.wait_for_torrentlist_update() def add_torrent_tracker(self, id, tracker): data = { 'ids' : [id], 'trackerAdd' : [tracker] } request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data) request.send_request() response = request.get_response() return response['result'] if response['result'] != 'success' else '' def remove_torrent_tracker(self, id, tracker): data = { 'ids' : [id], 'trackerRemove' : [tracker] } request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, data) request.send_request() response = request.get_response() self.wait_for_torrentlist_update() return response['result'] if response['result'] != 'success' else '' def increase_file_priority(self, file_nums): file_nums = list(file_nums) ref_num = file_nums[0] for num in file_nums: if not self.torrent_details_cache['wanted'][num]: ref_num = num break elif self.torrent_details_cache['priorities'][num] < \ self.torrent_details_cache['priorities'][ref_num]: ref_num = num current_priority = self.torrent_details_cache['priorities'][ref_num] if not self.torrent_details_cache['wanted'][ref_num]: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'low') elif current_priority == -1: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'normal') elif current_priority == 0: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'high') def decrease_file_priority(self, file_nums): file_nums = list(file_nums) ref_num = file_nums[0] for num in file_nums: if self.torrent_details_cache['priorities'][num] > \ self.torrent_details_cache['priorities'][ref_num]: ref_num = num current_priority = self.torrent_details_cache['priorities'][ref_num] if current_priority >= 1: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'normal') elif current_priority == 0: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'low') elif current_priority == -1: self.set_file_priority(self.torrent_details_cache['id'], file_nums, 'off') def set_file_priority(self, torrent_id, file_nums, priority): request_data = {'ids': [torrent_id]} if priority == 'off': request_data['files-unwanted'] = file_nums else: request_data['files-wanted'] = file_nums request_data['priority-' + priority] = file_nums request = TransmissionRequest(self.host, self.port, self.path, 'torrent-set', 1, request_data) request.send_request() self.wait_for_details_update() def get_file_priority(self, torrent_id, file_num): priority = self.torrent_details_cache['priorities'][file_num] if not self.torrent_details_cache['wanted'][file_num]: return 'off' elif priority <= -1: return 'low' elif priority == 0: return 'normal' elif priority >= 1: return 'high' return '?' def wait_for_torrentlist_update(self): self.wait_for_update(7) def wait_for_details_update(self): self.wait_for_update(77) def wait_for_status_update(self): self.wait_for_update(22) def wait_for_update(self, update_id): self.update(0) # send request while True: # wait for response if self.update(0, update_id): break time.sleep(0.1) def get_status(self, torrent): if torrent['status'] == Transmission.STATUS_STOPPED: status = 'paused' elif torrent['status'] == Transmission.STATUS_CHECK: status = 'verifying' elif torrent['status'] == Transmission.STATUS_CHECK_WAIT: status = 'will verify' elif torrent['isIsolated']: status = 'isolated' elif torrent['status'] == Transmission.STATUS_DOWNLOAD: status = ('idle','downloading')[torrent['rateDownload'] > 0] elif torrent['status'] == Transmission.STATUS_DOWNLOAD_WAIT: status = 'will download (%d)' % torrent['queuePosition'] elif torrent['status'] == Transmission.STATUS_SEED: status = 'seeding' elif torrent['status'] == Transmission.STATUS_SEED_WAIT: status = 'will seed (%d)' % torrent['queuePosition'] else: status = 'unknown state' return status def can_has_peers(self, torrent): """ Will return True if at least one tracker was successfully queried recently, or if DHT is enabled for this torrent and globally, False otherwise. """ # Torrent has trackers? if torrent['trackerStats']: # Did we try to connect a tracker? if any([tracker['hasAnnounced'] for tracker in torrent['trackerStats']]): for tracker in torrent['trackerStats']: if tracker['lastAnnounceSucceeded']: return True # We didn't try yet; assume at least one is online else: return True # Torrent can use DHT? # ('dht-enabled' may be missing; assume DHT is available until we can say for sure) if not self.status_cache.has_key('dht-enabled') or \ (self.status_cache['dht-enabled'] and not torrent['isPrivate']): return True # No ways of finding peers remaining return False def get_bandwidth_priority(self, torrent): if torrent['bandwidthPriority'] == -1: return '-' elif torrent['bandwidthPriority'] == 0: return ' ' elif torrent['bandwidthPriority'] == 1: return '+' else: return '?' # End of Class Transmission # User Interface class Interface: TRACKER_ITEM_HEIGHT = 6 def __init__(self): self.filter_list = config.get('Filtering', 'filter') self.filter_inverse = config.getboolean('Filtering', 'invert') self.sort_orders = parse_sort_str(config.get('Sorting', 'order')) self.compact_list = config.getboolean('Misc', 'compact_list') self.blank_lines = config.getboolean('Misc', 'blank_lines') self.torrentname_is_progressbar = config.getboolean('Misc', 'torrentname_is_progressbar') self.file_viewer = config.get('Misc', 'file_viewer') self.file_open_in_terminal = config.getboolean('Misc', 'file_open_in_terminal') self.torrents = server.get_torrent_list(self.sort_orders) self.stats = server.get_global_stats() self.torrent_details = [] self.selected_torrent = -1 # changes to >-1 when focus >-1 & user hits return self.highlight_dialog = False self.search_focus = 0 # like self.focus but for searches in torrent list self.focused_id = -1 # the id (provided by Transmission) of self.torrents[self.focus] self.focus = -1 # -1: nothing focused; 0: top of list; <# of torrents>-1: bottom of list self.scrollpos = 0 # start of torrentlist self.torrents_per_page = 0 # will be set by manage_layout() self.rateDownload_width = self.rateUpload_width = 2 self.details_category_focus = 0 # overview/files/peers/tracker in details self.focus_detaillist = -1 # same as focus but for details self.selected_files = [] # marked files in details self.file_index_map = {} # Maps local torrent's file indices to server file indices self.scrollpos_detaillist = 0 # same as scrollpos but for details self.compact_torrentlist = False # draw only one line for each torrent in compact mode self.exit_now = False self.keybindings = { ord('?'): self.call_list_key_bindings, curses.KEY_F1: self.call_list_key_bindings, 27: self.go_back_or_unfocus, curses.KEY_BREAK: self.go_back_or_unfocus, 12: self.go_back_or_unfocus, curses.KEY_BACKSPACE: self.leave_details, ord('q'): self.go_back_or_quit, ord('o'): self.o_key, ord('\n'): self.enter_key, curses.KEY_RIGHT: self.right_key, ord('l'): self.l_key, ord('s'): self.show_sort_order_menu, ord('f'): self.f_key, ord('u'): self.global_upload, ord('d'): self.global_download, ord('U'): self.torrent_upload, ord('D'): self.torrent_download, ord('L'): self.seed_ratio, ord('t'): self.t_key, ord('+'): self.bandwidth_priority, ord('-'): self.bandwidth_priority, ord('J'): self.J_key, ord('K'): self.K_key, ord('p'): self.pause_unpause_torrent, ord('P'): self.pause_unpause_all_torrent, ord('N'): self.start_now_torrent, ord('v'): self.verify_torrent, ord('y'): self.verify_torrent, ord('V'): self.V_key, ord('r'): self.r_key, curses.KEY_DC: self.r_key, ord('R'): self.remove_torrent_local_data, curses.KEY_SDC: self.remove_torrent_local_data, curses.KEY_UP: self.movement_keys, ord('k'): self.movement_keys, curses.KEY_DOWN: self.movement_keys, ord('j'): self.movement_keys, curses.KEY_PPAGE: self.movement_keys, curses.KEY_NPAGE: self.movement_keys, curses.KEY_HOME: self.movement_keys, curses.KEY_END: self.movement_keys, ord('g'): self.movement_keys, ord('G'): self.movement_keys, curses.ascii.ctrl(ord('f')): self.movement_keys, curses.ascii.ctrl(ord('b')): self.movement_keys, curses.ascii.ctrl(ord('n')): self.movement_keys, curses.ascii.ctrl(ord('p')): self.movement_keys, ord("\t"): self.move_in_details, curses.KEY_BTAB: self.move_in_details, ord('e'): self.move_in_details, ord('c'): self.move_in_details, ord('C'): self.toggle_compact_torrentlist, ord('h'): self.file_pritority_or_switch_details, curses.KEY_LEFT: self.file_pritority_or_switch_details, ord(' '): self.space_key, ord('a'): self.a_key, ord('A'): self.A_key, ord('m'): self.move_torrent, ord('n'): self.reannounce_torrent, ord('/'): self.dialog_search_torrentlist, curses.KEY_SEND: lambda c: self.move_queue('bottom'), curses.KEY_SHOME: lambda c: self.move_queue('top'), curses.KEY_SLEFT: lambda c: self.move_queue('ppage'), curses.KEY_SRIGHT: lambda c: self.move_queue('npage') } self.sort_options = [ ('name','_Name'), ('addedDate','_Age'), ('percentDone','_Progress'), ('seeders','_Seeds'), ('leechers','Lee_ches'), ('sizeWhenDone', 'Si_ze'), ('status','S_tatus'), ('uploadedEver','Up_loaded'), ('rateUpload','_Upload Speed'), ('rateDownload','_Download Speed'), ('uploadRatio','_Ratio'), ('peersConnected','P_eers'), ('downloadDir', 'L_ocation'), ('mainTrackerDomain', 'Trac_ker') ] # queue was implemmented in transmission 2.4 if server.get_rpc_version() >= 14: self.sort_options.append(('queuePosition', '_Queue Position')) self.sort_options.append(('reverse','Re_verse')) try: self.init_screen() self.run() except: self.restore_screen() (exc_type, exc_value, exc_traceback) = sys.exc_info() raise exc_type, exc_value, exc_traceback else: self.restore_screen() def init_screen(self): os.environ['ESCDELAY'] = '0' # make escape usable self.screen = curses.initscr() curses.noecho() ; curses.cbreak() ; self.screen.keypad(1) curses.halfdelay(10) # STDIN timeout hide_cursor() self.colors = ColorManager(dict(config.items('Colors'))) # http://bugs.python.org/issue2675 try: del os.environ['LINES'] del os.environ['COLUMNS'] except: pass # http://bugs.python.org/issue2675 try: del os.environ['LINES'] del os.environ['COLUMNS'] except: pass signal.signal(signal.SIGWINCH, lambda y,frame: self.get_screen_size()) self.get_screen_size() def restore_screen(self): curses.endwin() def enc(self, text): return text.encode(ENCODING, 'replace') def get_screen_size(self): time.sleep(0.1) # prevents curses.error on rapid resizing while True: curses.endwin() self.screen.refresh() self.height, self.width = self.screen.getmaxyx() # Tracker list breaks if width smaller than 73 if self.width < 73 or self.height < 16: self.screen.erase() self.screen.addstr(0,0, "Terminal too small", curses.A_REVERSE + curses.A_BOLD) time.sleep(1) else: break self.manage_layout() def manage_layout(self): self.recalculate_torrents_per_page() self.pad_height = max((len(self.torrents)+1) * self.tlist_item_height, self.height) self.pad = curses.newpad(self.pad_height, self.width) self.detaillistitems_per_page = self.height - 8 if self.selected_torrent > -1: self.rateDownload_width = self.get_rateDownload_width([self.torrent_details]) self.rateUpload_width = self.get_rateUpload_width([self.torrent_details]) self.torrent_title_width = self.width - self.rateUpload_width - 2 # show downloading column only if torrents is downloading if self.torrent_details['status'] == Transmission.STATUS_DOWNLOAD: self.torrent_title_width -= self.rateDownload_width + 2 elif self.torrents: self.visible_torrents_start = self.scrollpos/self.tlist_item_height self.visible_torrents = self.torrents[self.visible_torrents_start : self.scrollpos/self.tlist_item_height + self.torrents_per_page + 1] self.rateDownload_width = self.get_rateDownload_width(self.visible_torrents) self.rateUpload_width = self.get_rateUpload_width(self.visible_torrents) self.torrent_title_width = self.width - self.rateUpload_width - 2 # show downloading column only if any downloading torrents are visible if filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.visible_torrents): self.torrent_title_width -= self.rateDownload_width + 2 else: self.visible_torrents = [] self.torrent_title_width = 80 def get_rateDownload_width(self, torrents): new_width = max(map(lambda x: len(scale_bytes(x['rateDownload'])), torrents)) new_width = max(max(map(lambda x: len(scale_time(x['eta'])), torrents)), new_width) new_width = max(len(scale_bytes(self.stats['downloadSpeed'])), new_width) new_width = max(self.rateDownload_width, new_width) # don't shrink return new_width def get_rateUpload_width(self, torrents): new_width = max(map(lambda x: len(scale_bytes(x['rateUpload'])), torrents)) new_width = max(max(map(lambda x: len(num2str(x['uploadRatio'], '%.02f')), torrents)), new_width) new_width = max(len(scale_bytes(self.stats['uploadSpeed'])), new_width) new_width = max(self.rateUpload_width, new_width) # don't shrink return new_width def recalculate_torrents_per_page(self): self.lines_per_entry = 3 if self.blank_lines else 2 self.tlist_item_height = self.lines_per_entry if not self.compact_list else 1 self.mainview_height = self.height - 2 self.torrents_per_page = self.mainview_height / self.tlist_item_height def run(self): self.draw_title_bar() self.draw_stats() self.draw_torrent_list() while True: server.update(1) # I'm catching all exceptions here because resizing the terminal # can make a huge mess, e.g. we might be drawing to areas that # don't exist anymore. The proper way would probably be to write a # wrapper around self.pad.addstr() and anything else that uses # coordinates, but it's not worth the effort as this part is # called continuously. try: if self.selected_torrent == -1: self.draw_torrent_list() else: self.draw_details() except: pass self.stats = server.get_global_stats() self.draw_title_bar() # show shortcuts and stuff self.draw_stats() # show global states self.screen.move(0,0) # in case cursor can't be invisible self.handle_user_input() if self.exit_now: sort_str = ','.join(map(lambda x: ('','reverse:')[x['reverse']] + x['name'], self.sort_orders)) config.set('Sorting', 'order', sort_str) config.set('Filtering', 'filter', self.filter_list) config.set('Filtering', 'invert', str(self.filter_inverse)) config.set('Misc', 'compact_list', str(self.compact_list)) config.set('Misc', 'blank_lines', str(self.blank_lines)) config.set('Misc', 'torrentname_is_progressbar', str(self.torrentname_is_progressbar)) save_config(cmd_args.configfile) return def go_back_or_unfocus(self, c): if self.focus_detaillist > -1: # unfocus and deselect file self.focus_detaillist = -1 self.scrollpos_detaillist = 0 self.selected_files = [] elif self.selected_torrent > -1: # return from details self.details_category_focus = 0 self.selected_torrent = -1 self.selected_files = [] else: if self.focus > -1: self.scrollpos = 0 # unfocus main list self.focus = -1 elif self.filter_list: self.filter_list = '' # reset filter def leave_details(self, c): if self.selected_torrent > -1: server.set_torrent_details_id(-1) self.selected_torrent = -1 self.details_category_focus = 0 self.scrollpos_detaillist = 0 self.selected_files = [] def go_back_or_quit(self, c): if self.selected_torrent == -1: self.exit_now = True else: # return to list view server.set_torrent_details_id(-1) self.selected_torrent = -1 self.details_category_focus = 0 self.focus_detaillist = -1 self.scrollpos_detaillist = 0 self.selected_files = [] def space_key(self, c): # File list if self.selected_torrent > -1 and self.details_category_focus == 1: self.select_unselect_file(c) # Torrent list elif self.selected_torrent == -1: self.enter_key(c) def A_key(self, c): # File list if self.selected_torrent > -1 and self.details_category_focus == 1: self.select_unselect_file(c) # Do nothing in other detail tabs elif self.selected_torrent > -1: pass else: self.add_torrent_by_hash() def a_key(self, c): # File list if self.selected_torrent > -1 and self.details_category_focus == 1: self.select_unselect_file(c) # Trackers elif self.selected_torrent > -1 and self.details_category_focus == 3: self.add_tracker() # Do nothing in other detail tabs elif self.selected_torrent > -1: pass else: self.add_torrent() def V_key(self, c): # File list if self.selected_torrent > -1 and self.details_category_focus == 1: self.select_unselect_file(c) def o_key(self, c): if self.selected_torrent == -1: self.draw_options_dialog() elif self.selected_torrent > -1: self.details_category_focus = 0 def l_key(self, c): if self.focus > -1 and self.selected_torrent == -1: self.enter_key(c) elif self.selected_torrent > -1: self.file_pritority_or_switch_details(c) def t_key(self, c): if self.selected_torrent == -1: server.toggle_turtle_mode() elif self.selected_torrent > -1: self.details_category_focus = 3 def f_key(self, c): if self.selected_torrent == -1: self.show_state_filter_menu(c) elif self.selected_torrent > -1: self.details_category_focus = 1 def r_key(self, c): # Torrent list if self.selected_torrent == -1: self.remove_torrent(c) # Trackers elif self.selected_torrent > -1 and self.details_category_focus == 3: self.remove_tracker() def J_key(self, c): if self.selected_torrent > -1 and self.details_category_focus == 1: self.move_to_next_directory_in_filelist() else: self.move_queue('down') def K_key(self, c): if self.selected_torrent > -1 and self.details_category_focus == 1: self.move_to_previous_directory_in_filelist() else: self.move_queue('up') def right_key(self, c): if self.focus > -1 and self.selected_torrent == -1: self.enter_key(c) else: self.file_pritority_or_switch_details(c) def add_torrent(self): location = self.dialog_input_text("Add torrent from file or URL", homedir2tilde(os.getcwd()+os.sep), tab_complete='files') if location: error = server.add_torrent(tilde2homedir(location)) if error: msg = wrap("Couldn't add torrent \"%s\":" % location) msg.extend(wrap(error, self.width-4)) self.dialog_ok("\n".join(msg)) def add_torrent_by_hash(self): hash = self.dialog_input_text("Add torrent by hash") if hash: error = server.add_torrent('magnet:?xt=urn:btih:{}'.format(hash)) if error: msg = wrap("Couldn't add torrent \"%s\":" % hash) msg.extend(wrap(error, self.width-4)) self.dialog_ok("\n".join(msg)) def enter_key(self, c): # Torrent list if self.focus > -1 and self.selected_torrent == -1: self.screen.clear() self.selected_torrent = self.focus server.set_torrent_details_id(self.torrents[self.focus]['id']) server.wait_for_details_update() # File list elif self.selected_torrent > -1 and self.details_category_focus == 1: self.open_torrent_file(c) def show_sort_order_menu(self, c): if self.selected_torrent == -1: choice = self.dialog_menu('Sort order', self.sort_options, map(lambda x: x[0]==self.sort_orders[-1]['name'], self.sort_options).index(True)+1) if choice != -128: if choice == 'reverse': self.sort_orders[-1]['reverse'] = not self.sort_orders[-1]['reverse'] else: self.sort_orders.append({'name':choice, 'reverse':False}) while len(self.sort_orders) > 2: self.sort_orders.pop(0) def show_state_filter_menu(self, c): if self.selected_torrent == -1: options = [('uploading','_Uploading'), ('downloading','_Downloading'), ('active','Ac_tive'), ('paused','_Paused'), ('seeding','_Seeding'), ('incomplete','In_complete'), ('verifying','Verif_ying'), ('private','P_rivate'), ('isolated', '_Isolated'), ('invert','In_vert'), ('','_All')] choice = self.dialog_menu(('Show only','Filter all')[self.filter_inverse], options, map(lambda x: x[0]==self.filter_list, options).index(True)+1) if choice != -128: if choice == 'invert': self.filter_inverse = not self.filter_inverse else: if choice == '': self.filter_inverse = False self.filter_list = choice def global_upload(self, c): current_limit = (-1,self.stats['speed-limit-up'])[self.stats['speed-limit-up-enabled']] limit = self.dialog_input_number("Global upload limit in kilobytes per second", current_limit) if limit == -128: return server.set_rate_limit('up', limit) def global_download(self, c): current_limit = (-1,self.stats['speed-limit-down'])[self.stats['speed-limit-down-enabled']] limit = self.dialog_input_number("Global download limit in kilobytes per second", current_limit) if limit == -128: return server.set_rate_limit('down', limit) def torrent_upload(self, c): if self.focus > -1: current_limit = (-1,self.torrents[self.focus]['uploadLimit'])[self.torrents[self.focus]['uploadLimited']] limit = self.dialog_input_number("Upload limit in kilobytes per second for\n%s" % \ self.torrents[self.focus]['name'], current_limit) if limit == -128: return server.set_rate_limit('up', limit, self.torrents[self.focus]['id']) def torrent_download(self, c): if self.focus > -1: current_limit = (-1,self.torrents[self.focus]['downloadLimit'])[self.torrents[self.focus]['downloadLimited']] limit = self.dialog_input_number("Download limit in Kilobytes per second for\n%s" % \ self.torrents[self.focus]['name'], current_limit) if limit == -128: return server.set_rate_limit('down', limit, self.torrents[self.focus]['id']) def seed_ratio(self, c): if self.focus > -1: if self.torrents[self.focus]['seedRatioMode'] == 0: # Use global settings current_limit = '' elif self.torrents[self.focus]['seedRatioMode'] == 1: # Stop seeding at seedRatioLimit current_limit = self.torrents[self.focus]['seedRatioLimit'] elif self.torrents[self.focus]['seedRatioMode'] == 2: # Seed regardless of ratio current_limit = -1 limit = self.dialog_input_number("Seed ratio limit for\n%s" % self.torrents[self.focus]['name'], current_limit, floating_point=True, allow_empty=True) if limit == -1: limit = 0 if limit == -2: # -2 means 'empty' in dialog_input_number return codes limit = -1 server.set_seed_ratio(float(limit), self.torrents[self.focus]['id']) def bandwidth_priority(self, c): if c == ord('-') and self.focus > -1: server.decrease_bandwidth_priority(self.torrents[self.focus]['id']) elif c == ord('+') and self.focus > -1: server.increase_bandwidth_priority(self.torrents[self.focus]['id']) def move_queue(self, direction): # queue was implemmented in Transmission v2.4 if server.get_rpc_version() >= 14 and self.focus > -1: if direction in ('ppage', 'npage'): new_position = self.torrents[self.focus]['queuePosition'] if direction == 'ppage': new_position -= 10 else: new_position += 10 else: new_position = direction server.move_queue(self.torrents[self.focus]['id'], new_position) def pause_unpause_torrent(self, c): if self.focus > -1: if self.selected_torrent > -1: t = self.torrent_details else: t = self.torrents[self.focus] if t['status'] == Transmission.STATUS_STOPPED: server.start_torrents([t['id']]) else: server.stop_torrents([t['id']]) def start_now_torrent(self, c): if self.focus > -1: if self.selected_torrent > -1: t = self.torrent_details else: t = self.torrents[self.focus] server.start_now_torrent(t['id']) def pause_unpause_all_torrent(self, c): focused_torrent = self.torrents[ max(0,self.focus) ] if focused_torrent['status'] == Transmission.STATUS_STOPPED: server.start_torrents([t['id'] for t in self.torrents]) else: server.stop_torrents([t['id'] for t in self.torrents]) def verify_torrent(self, c): if self.focus > -1: if self.torrents[self.focus]['status'] != Transmission.STATUS_CHECK \ and self.torrents[self.focus]['status'] != Transmission.STATUS_CHECK_WAIT: server.verify_torrent(self.torrents[self.focus]['id']) def reannounce_torrent(self, c): if self.focus > -1: server.reannounce_torrent(self.torrents[self.focus]['id']) def remove_torrent(self, c): if self.focus > -1: name = self.torrents[self.focus]['name'][0:self.width - 15] if self.dialog_yesno("Remove %s?" % name) == True: if self.selected_torrent > -1: # leave details server.set_torrent_details_id(-1) self.selected_torrent = -1 self.details_category_focus = 0 server.remove_torrent(self.torrents[self.focus]['id']) self.focus_next_after_delete() def remove_torrent_local_data(self, c): if self.focus > -1: name = self.torrents[self.focus]['name'][0:self.width - 15] if self.dialog_yesno("Remove and delete %s?" % name, important=True) == True: if self.selected_torrent > -1: # leave details server.set_torrent_details_id(-1) self.selected_torrent = -1 self.details_category_focus = 0 server.remove_torrent_local_data(self.torrents[self.focus]['id']) self.focus_next_after_delete() def focus_next_after_delete(self): """ Focus next torrent after user deletes torrent """ new_focus = min(self.focus + 1, len(self.torrents) - 2) self.focused_id = self.torrents[new_focus]['id'] def add_tracker(self): if server.get_rpc_version() < 10: self.dialog_ok("You need Transmission v2.10 or higher to add trackers.") return tracker = self.dialog_input_text('Add tracker URL:') if tracker: t = self.torrent_details response = server.add_torrent_tracker(t['id'], tracker) if response: msg = wrap("Couldn't add tracker: %s" % response) self.dialog_ok("\n".join(msg)) def remove_tracker(self): if server.get_rpc_version() < 10: self.dialog_ok("You need Transmission v2.10 or higher to remove trackers.") return t = self.torrent_details if (self.scrollpos_detaillist >= 0 and \ self.scrollpos_detaillist < len(t['trackerStats']) and \ self.dialog_yesno("Do you want to remove this tracker?") is True): tracker = t['trackerStats'][self.scrollpos_detaillist] response = server.remove_torrent_tracker(t['id'], tracker['id']) if response: msg = wrap("Couldn't remove tracker: %s" % response) self.dialog_ok("\n".join(msg)) def movement_keys(self, c): if self.selected_torrent == -1 and len(self.torrents) > 0: if c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')): self.focus, self.scrollpos = self.move_up(self.focus, self.scrollpos, self.tlist_item_height) elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')): self.focus, self.scrollpos = self.move_down(self.focus, self.scrollpos, self.tlist_item_height, self.torrents_per_page, len(self.torrents)) elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')): self.focus, self.scrollpos = self.move_page_up(self.focus, self.scrollpos, self.tlist_item_height, self.torrents_per_page) elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')): self.focus, self.scrollpos = self.move_page_down(self.focus, self.scrollpos, self.tlist_item_height, self.torrents_per_page, len(self.torrents)) elif c == curses.KEY_HOME or c == ord('g'): self.focus, self.scrollpos = self.move_to_top() elif c == curses.KEY_END or c == ord('G'): self.focus, self.scrollpos = self.move_to_end(self.tlist_item_height, self.torrents_per_page, len(self.torrents)) self.focused_id = self.torrents[self.focus]['id'] elif self.selected_torrent > -1: # file list if self.details_category_focus == 1: # focus/movement if c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')): self.focus_detaillist, self.scrollpos_detaillist = \ self.move_up(self.focus_detaillist, self.scrollpos_detaillist, 1) elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')): self.focus_detaillist, self.scrollpos_detaillist = \ self.move_down(self.focus_detaillist, self.scrollpos_detaillist, 1, self.detaillistitems_per_page, len(self.torrent_details['files'])) elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')): self.focus_detaillist, self.scrollpos_detaillist = \ self.move_page_up(self.focus_detaillist, self.scrollpos_detaillist, 1, self.detaillistitems_per_page) elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')): self.focus_detaillist, self.scrollpos_detaillist = \ self.move_page_down(self.focus_detaillist, self.scrollpos_detaillist, 1, self.detaillistitems_per_page, len(self.torrent_details['files'])) elif c == curses.KEY_HOME or c == ord('g'): self.focus_detaillist, self.scrollpos_detaillist = self.move_to_top() elif c == curses.KEY_END or c == ord('G'): self.focus_detaillist, self.scrollpos_detaillist = \ self.move_to_end(1, self.detaillistitems_per_page, len(self.torrent_details['files'])) # visual mode global vmode_id if vmode_id > -1: if vmode_id < self.focus_detaillist: self.selected_files = range(vmode_id, self.focus_detaillist + 1) else: self.selected_files = range(self.focus_detaillist, vmode_id + 1) list_len = 0 # peer list movement if self.details_category_focus == 2: list_len = len(self.torrent_details['peers']) # tracker list movement elif self.details_category_focus == 3: list_len = len(self.torrent_details['trackerStats']) # pieces list movement elif self.details_category_focus == 4: piece_count = self.torrent_details['pieceCount'] margin = len(str(piece_count)) + 2 map_width = int(str(self.width-margin-1)[0:-1] + '0') list_len = int(piece_count / map_width) + 1 if list_len: if c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')): if self.scrollpos_detaillist > 0: self.scrollpos_detaillist -= 1 elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')): if self.scrollpos_detaillist < list_len - 1: self.scrollpos_detaillist += 1 elif c == curses.KEY_PPAGE or c == curses.ascii.ctrl(ord('b')): self.scrollpos_detaillist = \ max(self.scrollpos_detaillist - self.detaillistitems_per_page - 1, 0) elif c == curses.KEY_NPAGE or c == curses.ascii.ctrl(ord('f')): if self.scrollpos_detaillist + self.detaillistitems_per_page >= list_len: self.scrollpos_detaillist = list_len - 1 else: self.scrollpos_detaillist += self.detaillistitems_per_page elif c == curses.KEY_HOME or c == ord('g'): self.scrollpos_detaillist = 0 elif c == curses.KEY_END or c == ord('G'): self.scrollpos_detaillist = list_len - 1 # Disallow scrolling past the last item that would cause blank # space to be displayed in pieces and peer lists. if self.details_category_focus in (2, 4): self.scrollpos_detaillist = min(self.scrollpos_detaillist, max(0, list_len - self.detaillistitems_per_page)) def file_pritority_or_switch_details(self, c): if self.selected_torrent > -1: # file priority OR walk through details if c == curses.KEY_RIGHT or c == ord('l'): if self.details_category_focus == 1 and \ (self.selected_files or self.focus_detaillist > -1): if self.selected_files: files = set([self.file_index_map[index] for index in self.selected_files]) server.increase_file_priority(files) elif self.focus_detaillist > -1: server.increase_file_priority([self.file_index_map[self.focus_detaillist]]) else: self.scrollpos_detaillist = 0 self.next_details() elif c == curses.KEY_LEFT or c == ord('h'): if self.details_category_focus == 1 and \ (self.selected_files or self.focus_detaillist > -1): if self.selected_files: files = set([self.file_index_map[index] for index in self.selected_files]) server.decrease_file_priority(files) elif self.focus_detaillist > -1: server.decrease_file_priority([self.file_index_map[self.focus_detaillist]]) else: self.scrollpos_detaillist = 0 self.prev_details() def select_unselect_file(self, c): if self.selected_torrent > -1 and self.details_category_focus == 1 and self.focus_detaillist >= 0: # file selection with space if c == ord(' '): try: self.selected_files.pop(self.selected_files.index(self.focus_detaillist)) except ValueError: self.selected_files.append(self.focus_detaillist) curses.ungetch(curses.KEY_DOWN) # move down # (un)select directory elif c == ord('A'): file_id = self.file_index_map[self.focus_detaillist] focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) if self.selected_files.count(self.focus_detaillist): for focus in range(0, len(self.torrent_details['files'])): file_id = self.file_index_map[focus] if self.torrent_details['files'][file_id]['name'].startswith(focused_dir): try: while focus in self.selected_files: self.selected_files.remove(focus) except ValueError: pass else: for focus in range(0, len(self.torrent_details['files'])): file_id = self.file_index_map[focus] if self.torrent_details['files'][file_id]['name'].startswith(focused_dir): self.selected_files.append(focus) self.move_to_next_directory_in_filelist() # (un)select all files elif c == ord('a'): if self.selected_files: self.selected_files = [] else: self.selected_files = range(0, len(self.torrent_details['files'])) elif c == ord('V'): global vmode_id if self.selected_files: self.selected_files = [] if vmode_id != -1: vmode_id = -1 else: try: self.selected_files.pop(self.selected_files.index(self.focus_detaillist)) except ValueError: self.selected_files.append(self.focus_detaillist) vmode_id = self.focus_detaillist def move_to_next_directory_in_filelist(self): if self.selected_torrent > -1 and self.details_category_focus == 1: self.focus_detaillist = max(self.focus_detaillist, 0) file_id = self.file_index_map[self.focus_detaillist] focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \ and self.focus_detaillist < len(self.torrent_details['files'])-1: self.movement_keys(curses.KEY_DOWN) file_id = self.file_index_map[self.focus_detaillist] def move_to_previous_directory_in_filelist(self): if self.selected_torrent > -1 and self.details_category_focus == 1: self.focus_detaillist = max(self.focus_detaillist, 0) file_id = self.file_index_map[self.focus_detaillist] focused_dir = os.path.dirname(self.torrent_details['files'][file_id]['name']) while self.torrent_details['files'][file_id]['name'].startswith(focused_dir) \ and self.focus_detaillist > 0: self.movement_keys(curses.KEY_UP) file_id = self.file_index_map[self.focus_detaillist] def open_torrent_file(self, c): if self.focus_detaillist >= 0: details = server.get_torrent_details() stats = server.get_global_stats() file_server_index = self.file_index_map[self.focus_detaillist] file_name = details['files'][file_server_index]['name'] download_dir = details['downloadDir'] incomplete_dir = stats['incomplete-dir'] + '/' file_path = None possible_file_locations = [ download_dir + file_name, download_dir + file_name + '.part', incomplete_dir + file_name, incomplete_dir + file_name + '.part' ] for f in possible_file_locations: if (os.path.isfile(f)): file_path = f break if file_path is None: self.get_screen_size() self.dialog_ok("Could not find file:\n%s" % (file_name)) return viewer_cmd=[] for argstr in self.file_viewer.split(" "): viewer_cmd.append(argstr.replace('%s', file_path)) try: if self.file_open_in_terminal: self.restore_screen() call(viewer_cmd) self.get_screen_size() else: devnull = open(os.devnull, 'wb') Popen(viewer_cmd, stdout=devnull, stderr=devnull) devnull.close() except OSError, err: self.get_screen_size() self.dialog_ok("%s:\n%s" % (" ".join(viewer_cmd), err)) def move_in_details(self, c): if self.selected_torrent > -1: if c == ord("\t"): self.next_details() elif c == curses.KEY_BTAB: self.prev_details() elif c == ord('e'): self.details_category_focus = 2 elif c == ord('c'): self.details_category_focus = 4 def call_list_key_bindings(self, c): self.list_key_bindings() def toggle_compact_torrentlist(self, c): self.compact_list = not self.compact_list self.recalculate_torrents_per_page() self.follow_list_focus() def move_torrent(self, c): if self.focus > -1: location = homedir2tilde(self.torrents[self.focus]['downloadDir']) msg = 'Move "%s" from\n%s to' % (self.torrents[self.focus]['name'], location) path = self.dialog_input_text(msg, location, tab_complete='dirs') if path: server.move_torrent(self.torrents[self.focus]['id'], tilde2homedir(path)) def handle_user_input(self): c = self.screen.getch() if c == -1: return 0 f = self.keybindings.get(c, None) if f: f(c) try: if self.selected_torrent == -1: self.draw_torrent_list() else: self.draw_details() except: pass def filter_torrent_list(self): unfiltered = self.torrents if self.filter_list == 'downloading': self.torrents = [t for t in self.torrents if t['rateDownload'] > 0] elif self.filter_list == 'uploading': self.torrents = [t for t in self.torrents if t['rateUpload'] > 0] elif self.filter_list == 'paused': self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_STOPPED] elif self.filter_list == 'seeding': self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_SEED \ or t['status'] == Transmission.STATUS_SEED_WAIT] elif self.filter_list == 'incomplete': self.torrents = [t for t in self.torrents if t['percentDone'] < 100] elif self.filter_list == 'private': self.torrents = [t for t in self.torrents if t['isPrivate']] elif self.filter_list == 'active': self.torrents = [t for t in self.torrents if t['peersGettingFromUs'] > 0 \ or t['peersSendingToUs'] > 0 \ or t['status'] == Transmission.STATUS_CHECK] elif self.filter_list == 'verifying': self.torrents = [t for t in self.torrents if t['status'] == Transmission.STATUS_CHECK \ or t['status'] == Transmission.STATUS_CHECK_WAIT] elif self.filter_list == 'isolated': self.torrents = [t for t in self.torrents if t['isIsolated']] # invert list? if self.filter_inverse: self.torrents = [t for t in unfiltered if t not in self.torrents] def follow_list_focus(self): if self.focus == -1: return # check if list is empty or id to look for isn't in list ids = [t['id'] for t in self.torrents] if len(self.torrents) == 0 or self.focused_id not in ids: self.focus, self.scrollpos = -1, 0 return # find focused_id self.focus = min(self.focus, len(self.torrents)-1) if self.torrents[self.focus]['id'] != self.focused_id: for i,t in enumerate(self.torrents): if t['id'] == self.focused_id: self.focus = i break # make sure the focus is not above the visible area while self.focus < (self.scrollpos/self.tlist_item_height): self.scrollpos -= self.tlist_item_height # make sure the focus is not below the visible area while self.focus > (self.scrollpos/self.tlist_item_height) + self.torrents_per_page-1: self.scrollpos += self.tlist_item_height # keep min and max bounds self.scrollpos = min(self.scrollpos, (len(self.torrents) - self.torrents_per_page) * self.tlist_item_height) self.scrollpos = max(0, self.scrollpos) def draw_torrent_list(self, search_keyword=''): self.torrents = server.get_torrent_list(self.sort_orders) self.filter_torrent_list() if search_keyword: matched_torrents = [t for t in self.torrents if search_keyword.lower() in t['name'].lower()] if matched_torrents: self.focus = 0 if self.search_focus >= len(matched_torrents): self.search_focus = 0 self.focused_id = matched_torrents[self.search_focus]['id'] self.highlight_dialog = False else: self.highlight_dialog = True curses.beep() else: self.search_focus = 0 self.follow_list_focus() self.manage_layout() ypos = 0 for i in range(len(self.visible_torrents)): ypos += self.draw_torrentlist_item(self.visible_torrents[i], (i == self.focus-self.visible_torrents_start), self.compact_list, ypos) self.pad.refresh(0,0, 1,0, self.mainview_height,self.width-1) self.screen.refresh() def draw_torrentlist_item(self, torrent, focused, compact, y): # the torrent name is also a progress bar self.draw_torrentlist_title(torrent, focused, self.torrent_title_width, y) rates = '' if torrent['status'] == Transmission.STATUS_DOWNLOAD: self.draw_downloadrate(torrent, y) if torrent['status'] == Transmission.STATUS_DOWNLOAD or torrent['status'] == Transmission.STATUS_SEED: self.draw_uploadrate(torrent, y) if not compact: # the line below the title/progress if torrent['percentDone'] < 100 and torrent['status'] == Transmission.STATUS_DOWNLOAD: self.draw_eta(torrent, y) self.draw_ratio(torrent, y) self.draw_torrentlist_status(torrent, focused, y) return self.lines_per_entry # number of lines that were used for drawing the list item else: # Draw ratio in place of upload rate if upload rate = 0 if not torrent['rateUpload']: self.draw_ratio(torrent, y - 1) return 1 def draw_downloadrate(self, torrent, ypos): self.pad.move(ypos, self.width-self.rateDownload_width-self.rateUpload_width-3) self.pad.addch(curses.ACS_DARROW, (0,curses.A_BOLD)[torrent['downloadLimited']]) rate = ('',scale_bytes(torrent['rateDownload']))[torrent['rateDownload']>0] self.pad.addstr(rate.rjust(self.rateDownload_width), curses.color_pair(self.colors.id('download_rate')) + curses.A_BOLD + curses.A_REVERSE) def draw_uploadrate(self, torrent, ypos): self.pad.move(ypos, self.width-self.rateUpload_width-1) self.pad.addch(curses.ACS_UARROW, (0,curses.A_BOLD)[torrent['uploadLimited']]) rate = ('',scale_bytes(torrent['rateUpload']))[torrent['rateUpload']>0] self.pad.addstr(rate.rjust(self.rateUpload_width), curses.color_pair(self.colors.id('upload_rate')) + curses.A_BOLD + curses.A_REVERSE) def draw_ratio(self, torrent, ypos): self.pad.addch(ypos+1, self.width-self.rateUpload_width-1, curses.ACS_DIAMOND, (0,curses.A_BOLD)[torrent['uploadRatio'] < 1 and torrent['uploadRatio'] >= 0]) self.pad.addstr(ypos+1, self.width-self.rateUpload_width, num2str(torrent['uploadRatio'], '%.02f').rjust(self.rateUpload_width), curses.color_pair(self.colors.id('eta+ratio')) + curses.A_BOLD + curses.A_REVERSE) def draw_eta(self, torrent, ypos): self.pad.addch(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-3, curses.ACS_PLMINUS) self.pad.addstr(ypos+1, self.width-self.rateDownload_width-self.rateUpload_width-2, scale_time(torrent['eta']).rjust(self.rateDownload_width), curses.color_pair(self.colors.id('eta+ratio')) + curses.A_BOLD + curses.A_REVERSE) def draw_torrentlist_title(self, torrent, focused, width, ypos): if torrent['status'] == Transmission.STATUS_CHECK: percentDone = float(torrent['recheckProgress']) * 100 else: percentDone = torrent['percentDone'] bar_width = int(float(width) * (float(percentDone)/100)) size = "%6s" % scale_bytes(torrent['sizeWhenDone']) if torrent['percentDone'] < 100: if torrent['seeders'] <= 0 and torrent['status'] != Transmission.STATUS_CHECK: size = "%6s / " % scale_bytes(torrent['available']) + size size = "%6s / " % scale_bytes(torrent['haveValid'] + torrent['haveUnchecked']) + size size = '| ' + size title = ljust_columns(torrent['name'], width - len(size)) + size if torrent['isIsolated']: color = curses.color_pair(self.colors.id('title_error')) elif torrent['status'] == Transmission.STATUS_SEED or \ torrent['status'] == Transmission.STATUS_SEED_WAIT: color = curses.color_pair(self.colors.id('title_seed')) elif torrent['status'] == Transmission.STATUS_STOPPED: color = curses.color_pair(self.colors.id('title_paused')) elif torrent['status'] == Transmission.STATUS_CHECK or \ torrent['status'] == Transmission.STATUS_CHECK_WAIT: color = curses.color_pair(self.colors.id('title_verify')) elif torrent['rateDownload'] == 0: color = curses.color_pair(self.colors.id('title_idle')) elif torrent['percentDone'] < 100: color = curses.color_pair(self.colors.id('title_download')) else: color = 0 tag = curses.A_REVERSE tag_done = tag + color if focused: tag += curses.A_BOLD tag_done += curses.A_BOLD if self.torrentname_is_progressbar: # Estimate widths, which works for anything ASCII bar_complete = title[:bar_width] bar_incomplete = title[bar_width:] # Adjust for East-Asian (wide) characters while len_columns(bar_complete) != bar_width: if len_columns(bar_complete) > bar_width: bar_incomplete = bar_complete[-1] + bar_incomplete bar_complete = bar_complete[:-1] else: bar_complete += bar_incomplete[0] bar_incomplete = bar_incomplete[1:] self.pad.addstr(ypos, 0, self.enc(bar_complete), tag_done) self.pad.addstr(ypos, len_columns(bar_complete), self.enc(bar_incomplete), tag) else: self.pad.addstr(ypos, 0, self.enc(title), tag_done) def draw_torrentlist_status(self, torrent, focused, ypos): peers = '' parts = [server.get_status(torrent)] if torrent['isIsolated'] and torrent['peersConnected'] <= 0: if not torrent['trackerStats']: parts[0] = "Unable to find peers without trackers and DHT disabled" else: tracker_errors = [tracker['lastAnnounceResult'] or tracker['lastScrapeResult'] for tracker in torrent['trackerStats']] parts[0] = self.enc([te for te in tracker_errors if te][0]) else: if torrent['status'] == Transmission.STATUS_CHECK: parts[0] += " (%d%%)" % int(float(torrent['recheckProgress']) * 100) elif torrent['status'] == Transmission.STATUS_DOWNLOAD: parts[0] += " (%d%%)" % torrent['percentDone'] parts[0] = parts[0].ljust(20) # seeds and leeches will be appended right justified later peers = "%5s seed%s " % (num2str(torrent['seeders']), ('s', ' ')[torrent['seeders']==1]) peers += "%5s leech%s" % (num2str(torrent['leechers']), ('es', ' ')[torrent['leechers']==1]) # show additional information if enough room if self.torrent_title_width - sum(map(lambda x: len(x), parts)) - len(peers) > 18: uploaded = scale_bytes(torrent['uploadedEver']) parts.append("%7s uploaded" % ('nothing',uploaded)[uploaded != '0B']) if self.torrent_title_width - sum(map(lambda x: len(x), parts)) - len(peers) > 22: parts.append("%4s peer%s connected" % (torrent['peersConnected'], ('s',' ')[torrent['peersConnected'] == 1])) if focused: tags = curses.A_REVERSE + curses.A_BOLD else: tags = 0 remaining_space = self.torrent_title_width - sum(map(lambda x: len(x), parts), len(peers)) - 2 delimiter = ' ' * int(remaining_space / (len(parts))) line = server.get_bandwidth_priority(torrent) + ' ' + delimiter.join(parts) # make sure the peers element is always right justified line += ' ' * int(self.torrent_title_width - len(line) - len(peers)) + peers self.pad.addstr(ypos+1, 0, line, tags) def draw_details(self): self.torrent_details = server.get_torrent_details() self.manage_layout() # details could need more space than the torrent list self.pad_height = max(50, len(self.torrent_details['files'])+10, (len(self.torrents)+1)*3, self.height) self.pad = curses.newpad(self.pad_height, self.width) # torrent name + progress bar self.draw_torrentlist_item(self.torrent_details, False, False, 0) # divider + menu menu_items = ['_Overview', "_Files", 'P_eers', '_Trackers', 'Pie_ces' ] xpos = int((self.width - sum(map(lambda x: len(x), menu_items))-len(menu_items)) / 2) for item in menu_items: self.pad.move(3, xpos) tags = curses.A_BOLD if menu_items.index(item) == self.details_category_focus: tags += curses.A_REVERSE title = item.split('_') self.pad.addstr(title[0], tags) self.pad.addstr(title[1][0], tags + curses.A_UNDERLINE) self.pad.addstr(title[1][1:], tags) xpos += len(item)+1 # which details to display if self.details_category_focus == 0: self.draw_details_overview(5) elif self.details_category_focus == 1: self.draw_filelist(5) elif self.details_category_focus == 2: self.draw_peerlist(5) elif self.details_category_focus == 3: self.draw_trackerlist(5) elif self.details_category_focus == 4: self.draw_pieces_map(5) self.pad.refresh(0,0, 1,0, self.height-2,self.width) self.screen.refresh() def draw_details_overview(self, ypos): t = self.torrent_details info = [] info.append(['Hash: ', "%s" % t['hashString']]) info.append(['ID: ', "%s" % t['id']]) wanted = 0 for i, file_info in enumerate(t['files']): if t['wanted'][i] == True: wanted += t['files'][i]['length'] sizes = ['Size: ', "%s; " % scale_bytes(t['totalSize'], 'long'), "%s wanted; " % (scale_bytes(wanted, 'long'),'everything') [t['totalSize'] == wanted]] if t['available'] < t['totalSize']: sizes.append("%s available; " % scale_bytes(t['available'], 'long')) sizes.extend(["%s left" % scale_bytes(t['leftUntilDone'], 'long')]) info.append(sizes) info.append(['Files: ', "%d; " % len(t['files'])]) complete = map(lambda x: x['bytesCompleted'] == x['length'], t['files']).count(True) not_complete = filter(lambda x: x['bytesCompleted'] != x['length'], t['files']) partial = map(lambda x: x['bytesCompleted'] > 0, not_complete).count(True) if complete == len(t['files']): info[-1].append("all complete") else: info[-1].append("%d complete; " % complete) info[-1].append("%d commenced" % partial) info.append(['Pieces: ', "%s; " % t['pieceCount'], "%s each" % scale_bytes(t['pieceSize'], 'long')]) info.append(['Download: ']) info[-1].append("%s" % scale_bytes(t['downloadedEver'], 'long') + \ " (%d%%) received; " % int(percent(t['sizeWhenDone'], t['downloadedEver']))) info[-1].append("%s" % scale_bytes(t['haveValid'], 'long') + \ " (%d%%) verified; " % int(percent(t['sizeWhenDone'], t['haveValid']))) info[-1].append("%s corrupt" % scale_bytes(t['corruptEver'], 'long')) if t['percentDone'] < 100: info[-1][-1] += '; ' if t['rateDownload']: info[-1].append("receiving %s per second" % scale_bytes(t['rateDownload'], 'long')) if t['downloadLimited']: info[-1][-1] += " (throttled to %s)" % scale_bytes(t['downloadLimit']*1024, 'long') else: info[-1].append("no reception in progress") try: copies_distributed = (float(t['uploadedEver']) / float(t['sizeWhenDone'])) except ZeroDivisionError: copies_distributed = 0 info.append(['Upload: ', "%s (%d%%) transmitted; " % (scale_bytes(t['uploadedEver'], 'long'), t['uploadRatio']*100)]) if t['rateUpload']: info[-1].append("sending %s per second" % scale_bytes(t['rateUpload'], 'long')) if t['uploadLimited']: info[-1][-1] += " (throttled to %s)" % scale_bytes(t['uploadLimit']*1024, 'long') else: info[-1].append("no transmission in progress") info.append(['Ratio: ', '%.2f copies distributed' % copies_distributed]) norm_upload_rate = norm.add('%s:rateUpload' % t['id'], t['rateUpload'], 50) if norm_upload_rate > 0: target_ratio = self.get_target_ratio() bytes_left = (max(t['downloadedEver'],t['sizeWhenDone']) * target_ratio) - t['uploadedEver'] time_left = bytes_left / norm_upload_rate info[-1][-1] += '; ' if time_left < 86400: # 1 day info[-1].append('approaching %.2f at %s' % \ (target_ratio, timestamp(time.time() + time_left, "%R"))) else: info[-1].append('approaching %.2f on %s' % \ (target_ratio, timestamp(time.time() + time_left, "%x"))) info.append(['Seed limit: ']) if t['seedRatioMode'] == 0: if self.stats['seedRatioLimited']: info[-1].append('default (pause torrent after distributing %s copies)' % self.stats['seedRatioLimit']) else: info[-1].append('default (unlimited)') elif t['seedRatioMode'] == 1: info[-1].append('pause torrent after distributing %s copies' % t['seedRatioLimit']) elif t['seedRatioMode'] == 2: info[-1].append('unlimited (ignore global limits)') info.append(['Peers: ', "connected to %d; " % t['peersConnected'], "downloading from %d; " % t['peersSendingToUs'], "uploading to %d" % t['peersGettingFromUs']]) # average peer speed incomplete_peers = [peer for peer in self.torrent_details['peers'] if peer['progress'] < 1] if incomplete_peers: # use at least 2/3 or 10 of incomplete peers to make an estimation active_peers = [peer for peer in incomplete_peers if peer['download_speed']] min_active_peers = min(10, max(1, round(len(incomplete_peers)*0.666))) if 1 <= len(active_peers) >= min_active_peers: swarm_speed = sum([peer['download_speed'] for peer in active_peers]) / len(active_peers) info.append(['Swarm speed: ', "%s on average; " % scale_bytes(swarm_speed), "distribution of 1 copy takes %s" % \ scale_time(int(t['totalSize'] / swarm_speed), 'long')]) else: info.append(['Swarm speed: ', "" % \ (min_active_peers, len(active_peers))]) else: info.append(['Swarm speed: ', ""]) info.append(['Privacy: ']) if t['isPrivate']: info[-1].append('Private to this tracker -- DHT and PEX disabled') else: info[-1].append('Public torrent') info.append(['Location: ',"%s" % homedir2tilde(t['downloadDir'])]) if t['creator']: info.append(['Creator: ',"%s" % t['creator']]) ypos = self.draw_details_list(ypos, info) self.draw_details_eventdates(ypos+1) return ypos+1 def get_target_ratio(self): t = self.torrent_details if t['seedRatioMode'] == 1: return t['seedRatioLimit'] # individual limit elif t['seedRatioMode'] == 0 and self.stats['seedRatioLimited']: return self.stats['seedRatioLimit'] # global limit else: # round up to next 10/5/1 if t['uploadRatio'] >= 100: step_size = 10.0 elif t['uploadRatio'] >= 10: step_size = 5.0 else: step_size = 1.0 return int(round((t['uploadRatio'] + step_size/2) / step_size) * step_size) def draw_details_eventdates(self, ypos): t = self.torrent_details self.pad.addstr(ypos, 1, ' Created: ' + timestamp(t['dateCreated'])) self.pad.addstr(ypos+1, 1, ' Added: ' + timestamp(t['addedDate'])) self.pad.addstr(ypos+2, 1, ' Started: ' + timestamp(t['startDate'])) self.pad.addstr(ypos+3, 1, ' Activity: ' + timestamp(t['activityDate'])) if t['percentDone'] < 100 and t['eta'] > 0: self.pad.addstr(ypos+4, 1, 'Finishing: ' + timestamp(time.time() + t['eta'])) elif t['doneDate'] <= 0: self.pad.addstr(ypos+4, 1, 'Finishing: sometime') else: self.pad.addstr(ypos+4, 1, ' Finished: ' + timestamp(t['doneDate'])) if t['comment']: if self.width >= 90: width = self.width - 50 comment = wrap_multiline(t['comment'], width, initial_indent='Comment: ') for i, line in enumerate(comment): if(ypos+i > self.height-1): break self.pad.addstr(ypos+i, 50, self.enc(line)) else: width = self.width - 2 comment = wrap_multiline(t['comment'], width, initial_indent='Comment: ') for i, line in enumerate(comment): self.pad.addstr(ypos+6+i, 2, self.enc(line)) def draw_filelist(self, ypos): column_names = ' # Progress Size Priority Filename' self.pad.addstr(ypos, 0, column_names.ljust(self.width), curses.A_UNDERLINE) ypos += 1 for line in self.create_filelist(): curses_tags = 0 # highlight focused/selected line(s) while line.startswith('_'): if line[1] == 'S': curses_tags = curses.A_BOLD line = line[2:] if line[1] == 'F': curses_tags += curses.A_REVERSE line = line[2:] try: self.pad.addstr(ypos, 0, ' '*self.width, curses_tags) except: pass # colored priority (only in the first 30 chars, the rest is filename) xpos = 0 for part in re.split('(high|normal|low|off)', line[0:30], 1): if part == 'high': self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.id('file_prio_high'))) elif part == 'normal': self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.id('file_prio_normal'))) elif part == 'low': self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.id('file_prio_low'))) elif part == 'off': self.pad.addstr(ypos, xpos, self.enc(part), curses_tags + curses.color_pair(self.colors.id('file_prio_off'))) else: self.pad.addstr(ypos, xpos, self.enc(part), curses_tags) xpos += len(part) self.pad.addstr(ypos, xpos, self.enc(line[30:]), curses_tags) ypos += 1 if ypos > self.height: break def create_filelist(self): files = sorted(self.torrent_details['files'], cmp=lambda x,y: cmp(x['name'], y['name'])) # Build new mapping between sorted local files and transmission-daemon's unsorted files self.file_index_map = {} for index,file in enumerate(files): self.file_index_map[index] = self.torrent_details['files'].index(file) filelist = [] current_folder = [] current_depth = 0 pos = 0 pos_before_focus = 0 index = 0 for file in files: f = file['name'].split('/') f_len = len(f) - 1 if f[:f_len] != current_folder: [current_depth, pos] = self.create_filelist_transition(f, current_folder, filelist, current_depth, pos) current_folder = f[:f_len] filelist.append(self.create_filelist_line(f[-1], index, percent(file['length'], file['bytesCompleted']), file['length'], current_depth)) index += 1 if self.focus_detaillist == index - 1: pos_before_focus = pos if index + pos >= self.focus_detaillist + 1 + pos + self.detaillistitems_per_page/2 \ and index + pos >= self.detaillistitems_per_page: if self.focus_detaillist + 1 + pos_before_focus < self.detaillistitems_per_page / 2: return filelist return filelist[self.focus_detaillist + 1 + pos_before_focus - self.detaillistitems_per_page / 2 : self.focus_detaillist + 1 + pos_before_focus + self.detaillistitems_per_page / 2] begin = len(filelist) - self.detaillistitems_per_page return filelist[begin > 0 and begin or 0:] def create_filelist_transition(self, f, current_folder, filelist, current_depth, pos): """ Create directory transition from to , both of which are an array of strings, each one representing one subdirectory in their path (e.g. /tmp/a/c would result in [temp, a, c]). is a list of strings that will later be drawn to screen. This function only creates directory strings, and is responsible for managing depth (i.e. indentation) between different directories. """ f_len = len(f) - 1 # Amount of subdirectories in f current_folder_len = len(current_folder) # Amount of subdirectories in # current_folder # Number of directory parts from f and current_directory that are identical same = 0 while (same < current_folder_len and same < f_len and f[same] == current_folder[same]): same += 1 # Reduce depth for each directory f has less than current_folder if self.blank_lines: for i in range(current_folder_len - same): current_depth -= 1 filelist.append(' '*current_depth + ' '*31 + '/') pos += 1 else: # code duplication, but less calculation for i in range(current_folder_len - same): current_depth -= 1 # Stepping out of a directory, but not into a new directory if f_len < current_folder_len and f_len == same: return [current_depth, pos] # Increase depth for each new directory that appears in f, # but not in current_directory while current_depth < f_len: filelist.append('%s\\ %s' % (' '*current_depth + ' '*31 , f[current_depth])) current_depth += 1 pos += 1 return [current_depth, pos] def create_filelist_line(self, name, index, percent, length, current_depth): line = "%s %6.1f%%" % (str(index+1).rjust(3), percent) + \ ' '+scale_bytes(length).rjust(5) + \ ' '+server.get_file_priority(self.torrent_details['id'], self.file_index_map[index]).center(8) + \ " %s| %s" % (' '*current_depth, name[0:self.width-31-current_depth]) if index == self.focus_detaillist: line = '_F' + line if index in self.selected_files: line = '_S' + line return line def draw_peerlist(self, ypos): # Start drawing list either at the "selected" index, or at the index # that is required to display all remaining items without further scrolling. last_possible_index = max(0, len(self.torrent_details['peers']) - self.detaillistitems_per_page) start = min(self.scrollpos_detaillist, last_possible_index) end = start + self.detaillistitems_per_page peers = self.torrent_details['peers'][start:end] # Find width of columns clientname_width = 0 address_width = 0 port_width = 0 for peer in peers: if len(peer['clientName']) > clientname_width: clientname_width = len(peer['clientName']) if len(peer['address']) > address_width: address_width = len(peer['address']) if len(str(peer['port'])) > port_width: port_width = len(str(peer['port'])) # Column names column_names = 'Flags %3d Down %3d Up Progress ETA ' % \ (self.torrent_details['peersSendingToUs'], self.torrent_details['peersGettingFromUs']) column_names += 'Client'.ljust(clientname_width + 1) \ + 'Address'.ljust(address_width+port_width+1) if features['geoip']: column_names += 'Country' if features['dns']: column_names += ' Host' self.pad.addstr(ypos, 0, column_names.ljust(self.width), curses.A_UNDERLINE) ypos += 1 # Peers hosts = server.get_hosts() geo_ips = server.get_geo_ips() for index, peer in enumerate(peers): if features['dns']: try: try: host = hosts[peer['address']].check() host_name = host[3][0] except (IndexError, KeyError): host_name = "" except adns.NotReady: host_name = "" except adns.Error, msg: host_name = msg upload_tag = download_tag = line_tag = 0 if peer['rateToPeer']: upload_tag = curses.A_BOLD if peer['rateToClient']: download_tag = curses.A_BOLD self.pad.move(ypos, 0) # Flags self.pad.addstr("%-6s " % peer['flagStr']) # Down self.pad.addstr("%5s " % scale_bytes(peer['rateToClient']), download_tag) # Up self.pad.addstr("%5s " % scale_bytes(peer['rateToPeer']), upload_tag) # Progress if peer['progress'] < 1: self.pad.addstr("%3d%%" % (float(peer['progress'])*100)) else: self.pad.addstr("%3d%%" % (float(peer['progress'])*100), curses.A_BOLD) # ETA if peer['progress'] < 1 and peer['download_speed'] > 1024: self.pad.addstr(" %6s %4s " % \ ('~' + scale_bytes(peer['download_speed']), '~' + scale_time(peer['time_left']))) else: if peer['progress'] < 1: self.pad.addstr(" ") else: self.pad.addstr(" ") # Client self.pad.addstr(self.enc(peer['clientName'].ljust(clientname_width + 1))) # Address:Port self.pad.addstr(peer['address'].rjust(address_width) + \ ':' + str(peer['port']).ljust(port_width) + ' ') # Country if features['geoip']: self.pad.addstr(" %2s " % geo_ips[peer['address']]) # Host if features['dns']: self.pad.addstr(self.enc(host_name), curses.A_DIM) ypos += 1 def draw_trackerlist(self, ypos): top = ypos - 1 def addstr(ypos, xpos, *args): if ypos > top and ypos < self.height - 2: self.pad.addstr(ypos, xpos, *args) tracker_per_page = self.detaillistitems_per_page // self.TRACKER_ITEM_HEIGHT page = self.scrollpos_detaillist // tracker_per_page start = tracker_per_page * page end = tracker_per_page * (page + 1) tlist = self.torrent_details['trackerStats'][start:end] # keep position in range when last tracker gets deleted self.scrollpos_detaillist = min(self.scrollpos_detaillist, len(self.torrent_details['trackerStats'])-1) # show newly added tracker when list was empty before if self.torrent_details['trackerStats']: self.scrollpos_detaillist = max(0, self.scrollpos_detaillist) current_tier = -1 for index, t in enumerate(tlist): announce_msg_size = scrape_msg_size = 0 selected = t == self.torrent_details['trackerStats'][self.scrollpos_detaillist] if current_tier != t['tier']: current_tier = t['tier'] tiercolor = curses.A_BOLD + curses.A_REVERSE \ if selected else curses.A_REVERSE addstr(ypos, 0, ("Tier %d" % (current_tier+1)).ljust(self.width), tiercolor) ypos += 1 if selected: for i in range(4): addstr(ypos+i, 0, ' ', curses.A_BOLD + curses.A_REVERSE) addstr(ypos+1, 4, "Last announce: %s" % timestamp(t['lastAnnounceTime'])) addstr(ypos+1, 54, "Last scrape: %s" % timestamp(t['lastScrapeTime'])) if t['lastAnnounceSucceeded']: peers = "%s peer%s" % (num2str(t['lastAnnouncePeerCount']), ('s', '')[t['lastAnnouncePeerCount']==1]) addstr(ypos, 2, t['announce'], curses.A_BOLD + curses.A_UNDERLINE) addstr(ypos+2, 11, "Result: ") addstr(ypos+2, 19, "%s received" % peers, curses.A_BOLD) else: addstr(ypos, 2, t['announce'], curses.A_UNDERLINE) addstr(ypos+2, 9, "Response:") announce_msg_size = self.wrap_and_draw_result(top, ypos+2, 19, self.enc(t['lastAnnounceResult'])) if t['lastScrapeSucceeded']: seeds = "%s seed%s" % (num2str(t['seederCount']), ('s', '')[t['seederCount']==1]) leeches = "%s leech%s" % (num2str(t['leecherCount']), ('es', '')[t['leecherCount']==1]) addstr(ypos+2, 52, "Tracker knows:") addstr(ypos+2, 67, "%s and %s" % (seeds, leeches), curses.A_BOLD) else: addstr(ypos+2, 57, "Response:") scrape_msg_size += self.wrap_and_draw_result(top, ypos+2, 67, t['lastScrapeResult']) ypos += max(announce_msg_size, scrape_msg_size) addstr(ypos+3, 4, "Next announce: %s" % timestamp(t['nextAnnounceTime'])) addstr(ypos+3, 52, " Next scrape: %s" % timestamp(t['nextScrapeTime'])) ypos += 5 def wrap_and_draw_result(self, top, ypos, xpos, result): result = wrap(result, 30) i = 0 for i, line in enumerate(result): if ypos+i > top and ypos+i < self.height - 2: self.pad.addstr(ypos+i, xpos, line, curses.A_UNDERLINE) return i def draw_pieces_map(self, ypos): pieces = self.torrent_details['pieces'] piece_count = self.torrent_details['pieceCount'] margin = len(str(piece_count)) + 2 map_width = int(str(self.width-margin-1)[0:-1] + '0') for x in range(10, map_width, 10): self.pad.addstr(ypos, x+margin-1, str(x), curses.A_BOLD) start = self.scrollpos_detaillist * map_width end = min(start + (self.height - ypos - 3) * map_width, piece_count) if end <= start: return block = ord(pieces[start >> 3]) << (start & 7) format = "%%%dd" % (margin - 2) for counter in xrange(start, end): if counter % map_width == 0: ypos += 1 ; xpos = margin self.pad.addstr(ypos, 1, format % counter, curses.A_BOLD) else: xpos += 1 if counter & 7 == 0: block = ord(pieces[counter >> 3]) piece = block & 0x80 if piece: self.pad.addch(ypos, xpos, ' ', curses.A_REVERSE) else: self.pad.addch(ypos, xpos, '_') block <<= 1 missing_pieces = piece_count - counter - 1 if missing_pieces: line = "%d further piece%s" % (missing_pieces, ('','s')[missing_pieces>1]) xpos = (self.width - len(line)) / 2 self.pad.addstr(self.height-3, xpos, line, curses.A_REVERSE) def draw_details_list(self, ypos, info): key_width = max(map(lambda x: len(x[0]), info)) for i in info: self.pad.addstr(ypos, 1, self.enc(i[0].rjust(key_width))) # key # value part may be wrapped if it gets too long for v in i[1:]: y, x = self.pad.getyx() if x + len(v) >= self.width: ypos += 1 self.pad.move(ypos, key_width+1) self.pad.addstr(self.enc(v)) ypos += 1 return ypos def next_details(self): if self.details_category_focus >= 4: self.details_category_focus = 0 else: self.details_category_focus += 1 self.focus_detaillist = -1 self.scrollpos_detaillist = 0 self.pad.erase() def prev_details(self): if self.details_category_focus <= 0: self.details_category_focus = 4 else: self.details_category_focus -= 1 self.pad.erase() def move_up(self, focus, scrollpos, step_size): if focus < 0: focus = -1 else: focus -= 1 if scrollpos/step_size - focus > 0: scrollpos -= step_size scrollpos = max(0, scrollpos) while scrollpos % step_size: scrollpos -= 1 return focus, scrollpos def move_down(self, focus, scrollpos, step_size, elements_per_page, list_height): if focus < list_height - 1: focus += 1 if focus+1 - scrollpos/step_size > elements_per_page: scrollpos += step_size return focus, scrollpos def move_page_up(self, focus, scrollpos, step_size, elements_per_page): for x in range(elements_per_page - 1): focus, scrollpos = self.move_up(focus, scrollpos, step_size) if focus < 0: focus = 0 return focus, scrollpos def move_page_down(self, focus, scrollpos, step_size, elements_per_page, list_height): if focus < 0: focus = 0 for x in range(elements_per_page - 1): focus, scrollpos = self.move_down(focus, scrollpos, step_size, elements_per_page, list_height) return focus, scrollpos def move_to_top(self): return 0, 0 def move_to_end(self, step_size, elements_per_page, list_height): focus = list_height - 1 scrollpos = max(0, (list_height - elements_per_page) * step_size) return focus, scrollpos def draw_stats(self): self.screen.insstr(self.height-1, 0, ' '.center(self.width), curses.A_REVERSE) self.draw_torrents_stats() self.draw_global_rates() def draw_torrents_stats(self): if self.selected_torrent > -1 and self.details_category_focus == 2: self.screen.insstr((self.height-1), 0, "%d peer%s connected (" % (self.torrent_details['peersConnected'], ('s','')[self.torrent_details['peersConnected'] == 1]) + \ "Trackers:%d " % self.torrent_details['peersFrom']['fromTracker'] + \ "DHT:%d " % self.torrent_details['peersFrom']['fromDht'] + \ "LTEP:%d " % self.torrent_details['peersFrom']['fromLtep'] + \ "PEX:%d " % self.torrent_details['peersFrom']['fromPex'] + \ "Incoming:%d " % self.torrent_details['peersFrom']['fromIncoming'] + \ "Cache:%d)" % self.torrent_details['peersFrom']['fromCache'], curses.A_REVERSE) elif vmode_id > -1: self.screen.addstr((self.height-1), 0, "-- VISUAL --", curses.A_REVERSE) else: self.screen.addstr((self.height-1), 0, "Torrent%s:" % ('s','')[len(self.torrents) == 1], curses.A_REVERSE) self.screen.addstr("%d (" % len(self.torrents), curses.A_REVERSE) downloading = len(filter(lambda x: x['status']==Transmission.STATUS_DOWNLOAD, self.torrents)) seeding = len(filter(lambda x: x['status']==Transmission.STATUS_SEED, self.torrents)) paused = self.stats['pausedTorrentCount'] self.screen.addstr("Downloading:", curses.A_REVERSE) self.screen.addstr("%d " % downloading, curses.A_REVERSE) self.screen.addstr("Seeding:", curses.A_REVERSE) self.screen.addstr("%d " % seeding, curses.A_REVERSE) self.screen.addstr("Paused:", curses.A_REVERSE) self.screen.addstr("%d) " % paused, curses.A_REVERSE) if self.filter_list: self.screen.addstr("Filter:", curses.A_REVERSE) self.screen.addstr("%s%s" % (('','not ')[self.filter_inverse], self.filter_list), curses.color_pair(self.colors.id('filter_status')) + curses.A_REVERSE) # show last sort order (if terminal size permits it) curpos_y, curpos_x = self.screen.getyx() if self.sort_orders and self.width - curpos_x > 20: self.screen.addstr(" Sort by:", curses.A_REVERSE) name = [name[1] for name in self.sort_options if name[0] == self.sort_orders[-1]['name']][0] name = name.replace('_', '').lower() curses_tags = curses.color_pair(self.colors.id('filter_status')) + curses.A_REVERSE if self.sort_orders[-1]['reverse']: self.screen.addch(curses.ACS_DARROW, curses_tags) else: self.screen.addch(curses.ACS_UARROW, curses_tags) try: # 'name' may be too long self.screen.addstr(name, curses_tags) except curses.error: pass def draw_global_rates(self): rates_width = self.rateDownload_width + self.rateUpload_width + 3 if self.stats['alt-speed-enabled']: upload_limit = "/%dK" % self.stats['alt-speed-up'] download_limit = "/%dK" % self.stats['alt-speed-down'] else: upload_limit = ('', "/%dK" % self.stats['speed-limit-up'])[self.stats['speed-limit-up-enabled']] download_limit = ('', "/%dK" % self.stats['speed-limit-down'])[self.stats['speed-limit-down-enabled']] limits = {'dn_limit' : download_limit, 'up_limit' : upload_limit} limits_width = len(limits['dn_limit']) + len(limits['up_limit']) if self.stats['alt-speed-enabled']: self.screen.move(self.height-1, self.width-rates_width - limits_width - len('Turtle mode ')) self.screen.addstr('Turtle mode', curses.A_REVERSE + curses.A_BOLD) self.screen.addch(' ', curses.A_REVERSE) self.screen.move(self.height - 1, self.width - rates_width - limits_width) self.screen.addch(curses.ACS_DARROW, curses.A_REVERSE) self.screen.addstr(scale_bytes(self.stats['downloadSpeed']).rjust(self.rateDownload_width), curses.color_pair(self.colors.id('download_rate')) + curses.A_REVERSE + curses.A_BOLD) self.screen.addstr(limits['dn_limit'], curses.A_REVERSE) self.screen.addch(' ', curses.A_REVERSE) self.screen.addch(curses.ACS_UARROW, curses.A_REVERSE) self.screen.insstr(limits['up_limit'], curses.A_REVERSE) self.screen.insstr(scale_bytes(self.stats['uploadSpeed']).rjust(self.rateUpload_width), curses.color_pair(self.colors.id('upload_rate')) + curses.A_REVERSE + curses.A_BOLD) def draw_title_bar(self): self.screen.insstr(0, 0, ' '.center(self.width), curses.A_REVERSE) self.draw_connection_status() self.draw_quick_help() def draw_connection_status(self): status = "Transmission %s @ %s:%s" % (server.version, server.host, server.port) if cmd_args.DEBUG: status = "%d x %d " % (self.width, self.height) + status self.screen.addstr(0, 0, self.enc(status), curses.A_REVERSE) def draw_quick_help(self): help = [('?','Show Keybindings')] if self.selected_torrent == -1: if self.focus >= 0: help = [('enter','View Details'), ('p','Pause/Unpause'), ('r','Remove'), ('v','Verify')] else: help = [('/','Search'), ('f','Filter'), ('s','Sort')] + help + [('o','Options'), ('q','Quit')] else: help = [('Move with','cursor keys'), ('q','Back to List')] if self.details_category_focus == 1 and self.focus_detaillist > -1: help = [('enter', 'Open File'), ('space','(De)Select File'), ('V','Visually Select Files'), ('left/right','De-/Increase Priority'), ('escape','Unfocus/-select')] + help elif self.details_category_focus == 2: help = [('F1/?','Explain flags')] + help elif self.details_category_focus == 3: help = [('a','Add Tracker'),('r','Remove Tracker')] + help line = ' | '.join(map(lambda x: "%s %s" % (x[0], x[1]), help)) line = line[0:self.width] self.screen.insstr(0, self.width-len(line), line, curses.A_REVERSE) def list_key_bindings(self): title = 'Help Menu' message = " F1/? Show this help\n" + \ " u/d Adjust maximum global up-/download rate\n" + \ " U/D Adjust maximum up-/download rate for focused torrent\n" + \ " L Set seed ratio limit for focused torrent\n" + \ " +/- Adjust bandwidth priority for focused torrent\n" + \ " p Pause/Unpause torrent\n" + \ " P Pause/Unpause all torrents\n" + \ " N Start torrent now\n" + \ " v/y Verify torrent\n" + \ " m Move torrent\n" + \ " n Reannounce torrent\n" + \ " a Add torrent\n" + \ " A Add torrent by hash\n" + \ " Del/r Remove torrent and keep content\n" + \ " Shift+Del/R Remove torrent and delete content\n" # Queue was implemented in Transmission v2.4 if server.get_rpc_version() >= 14 and self.details_category_focus != 1: message += " J/K Move focused torrent in queue up/down\n" + \ " Shift+Lft/Rght Move focused torrent in queue up/down by 10\n" + \ " Shift+Home/End Move focused torrent to top/bottom of queue\n" else: message += " J/K Jump to next/previous directory\n" # Torrent list if self.selected_torrent == -1: message += " / Search in torrent list\n" + \ " f Filter torrent list\n" + \ " s Sort torrent list\n" \ " Enter/Right View torrent's details\n" + \ " o Configuration options\n" + \ " t Toggle turtle mode\n" + \ " C Toggle compact list mode\n" + \ " Esc Unfocus\n" + \ " q Quit" else: # Peer list if self.details_category_focus == 2: title = 'Peer status flags' message = " O Optimistic unchoke\n" + \ " D Downloading from this peer\n" + \ " d We would download from this peer if they'd let us\n" + \ " U Uploading to peer\n" + \ " u We would upload to this peer if they'd ask\n" + \ " K Peer has unchoked us, but we're not interested\n" + \ " ? We unchoked this peer, but they're not interested\n" + \ " E Encrypted Connection\n" + \ " H Peer was discovered through DHT\n" + \ " X Peer was discovered through Peer Exchange (PEX)\n" + \ " I Peer is an incoming connection\n" + \ " T Peer is connected via uTP" else: # Viewing torrent details message += " o Jump to overview\n" + \ " f Jump to file list\n" + \ " e Jump to peer list\n" + \ " t Jump to tracker information\n" + \ " Tab/Right Jump to next view\n" + \ " Shift+Tab/Left Jump to previous view\n" if self.details_category_focus == 1: # files if self.focus_detaillist > -1: message += " Left/Right Decrease/Increase file priority\n" message += " Up/Down Select file\n" + \ " Space Select/Deselect focused file\n" + \ " a Select/Deselect all files\n" + \ " A Select/Deselect directory\n" + \ " V Visually select files\n" + \ " Esc Unfocus+Unselect or Back to torrent list\n" + \ " q/Backspace Back to torrent list" else: message += "q/Backspace/Esc Back to torrent list" width = max(map(lambda x: len(x), message.split("\n"))) + 4 width = min(self.width, width) height = min(self.height, message.count("\n")+3) win = self.window(height, width, message=message, title=title) while True: if win.getch() >= 0: return def window(self, height, width, message='', title=''): height = min(self.height, height) width = min(self.width, width) ypos = int( (self.height - height) / 2 ) xpos = int( (self.width - width) / 2 ) win = curses.newwin(height, width, ypos, xpos) win.box() win.bkgd(' ', curses.A_REVERSE + curses.A_BOLD) if width >= 20: win.addch(height-1, width-19, curses.ACS_RTEE) win.addstr(height-1, width-18, " Close with Esc ") win.addch(height-1, width-2, curses.ACS_LTEE) if width >= (len(title) + 6) and title != '': win.addch(0, 1, curses.ACS_RTEE) win.addstr(0, 2, " " + title + " ") win.addch(0, len(title) + 4 , curses.ACS_LTEE) ypos = 1 for line in message.split("\n"): if len_columns(line) > width: line = ljust_columns(line, width-7) + '...' if ypos < height - 1: # ypos == height-1 is frame border win.addstr(ypos, 2, self.enc(line)) ypos += 1 else: # Do not write outside of frame border win.addstr(ypos, 2, " More... ") break return win def dialog_ok(self, message): height = 3 + message.count("\n") width = max(max(map(lambda x: len_columns(x), message.split("\n"))), 40) + 4 win = self.window(height, width, message=message) while True: if win.getch() >= 0: return def dialog_yesno(self, message, important=False): height = 5 + message.count("\n") width = max(len_columns(message), 8) + 4 win = self.window(height, width, message=message) win.keypad(True) if important: win.bkgd(' ', curses.color_pair(self.colors.id('dialog_important')) + curses.A_REVERSE) focus_tags = curses.color_pair(self.colors.id('button_focused')) unfocus_tags = 0 input = False while True: win.move(height-2, (width/2)-4) if input: win.addstr('Y', focus_tags + curses.A_UNDERLINE) win.addstr('es', focus_tags) win.addstr(' ') win.addstr('N', curses.A_UNDERLINE) win.addstr('o') else: win.addstr('Y', curses.A_UNDERLINE) win.addstr('es') win.addstr(' ') win.addstr('N', focus_tags + curses.A_UNDERLINE) win.addstr('o', focus_tags) c = win.getch() if c == ord('y'): return True elif c == ord('n'): return False elif c == ord("\t"): input = not input elif c == curses.KEY_LEFT or c == ord('h'): input = True elif c == curses.KEY_RIGHT or c == ord('l'): input = False elif c == ord("\n") or c == ord(' '): return input elif c == 27 or c == curses.KEY_BREAK: return -1 # tab_complete values: # 'files': complete with any files/directories # 'dirs': complete only with directories # 'torrent_list': complete with names from the torrent list # any false value: do not complete def dialog_input_text(self, message, input='', on_change=None, on_enter=None, tab_complete=None): width = self.width - 4 textwidth = self.width - 8 height = message.count("\n") + 4 win = self.window(height, width, message=message) win.keypad(True) show_cursor() if not isinstance(input, unicode): input = unicode(input, ENCODING) index = len(input) while True: # Cut the text into pages, each as long as the text field # The current page is determined by index position page = index // textwidth displaytext = input[textwidth*page:textwidth*(page + 1)] displayindex = index - textwidth*page color = (curses.color_pair(self.colors.id('dialog_important')) if self.highlight_dialog else curses.color_pair(self.colors.id('dialog'))) win.addstr(height - 2, 2, self.enc(displaytext.ljust(textwidth)), color) win.move(height - 2, displayindex + 2) c = win.getch() if c == 27 or c == curses.KEY_BREAK: hide_cursor() return '' elif index < len(input) and ( c == curses.KEY_RIGHT or c == curses.ascii.ctrl(ord('f')) ): index += 1 elif index > 0 and ( c == curses.KEY_LEFT or c == curses.ascii.ctrl(ord('b')) ): index -= 1 elif (c == curses.KEY_BACKSPACE or c == 127) and index > 0: input = input[:index - 1] + (index < len(input) and input[index:] or '') index -= 1 if on_change: on_change(input) elif index < len(input) and ( c == curses.KEY_DC or c == curses.ascii.ctrl(ord('d')) ): input = input[:index] + input[index + 1:] if on_change: on_change(input) elif index < len(input) and c == curses.ascii.ctrl(ord('k')): input = input[:index] if on_change: on_change(input) elif c == curses.ascii.ctrl(ord('u')): # Delete from cursor until beginning of line input = input[index:] index = 0 if on_change: on_change(input) elif c == curses.KEY_HOME or c == curses.ascii.ctrl(ord('a')): index = 0 elif c == curses.KEY_END or c == curses.ascii.ctrl(ord('e')): index = len(input) elif c == ord('\n'): if on_enter: on_enter(input) else: hide_cursor() return input elif c >= 32 and c < 127: input = input[:index] + unicode(chr(c), ENCODING) + (index < len(input) and input[index:] or '') index += 1 if on_change: on_change(input) elif c == ord('\t') and tab_complete: possible_choices = []; if tab_complete in ('files', 'dirs'): (dirname, filename) = os.path.split(tilde2homedir(input)) if not dirname: dirname = unicode(os.getcwd()) try: possible_choices = [ os.path.join(dirname, choice) for choice in os.listdir(dirname) if choice.startswith(filename) ] except OSError: continue; if tab_complete == 'dirs': possible_choices = [ d for d in possible_choices if os.path.isdir(d) ] elif tab_complete == 'torrent_list': possible_choices = [ t['name'] for t in self.torrents if t['name'].startswith(input) ] if(possible_choices): input = os.path.commonprefix(possible_choices) if tab_complete in ('files', 'dirs'): if len(possible_choices) == 1 and os.path.isdir(input) and input.endswith(os.sep) == False: input += os.sep input = homedir2tilde(input) index = len(input) if on_change: on_change(input); if on_change: win.redrawwin() def dialog_search_torrentlist(self, c): self.dialog_input_text('Search torrent by title:', on_change=self.draw_torrent_list, on_enter=self.increment_search, tab_complete = 'torrent_list') def increment_search(self, input): self.search_focus += 1 self.draw_torrent_list(input) def dialog_input_number(self, message, current_value, cursorkeys=True, floating_point=False, allow_empty=False, allow_zero=True, allow_negative_one=True): if not allow_zero: allow_negative_one = False width = max(max(map(lambda x: len(x), message.split("\n"))), 40) + 4 width = min(self.width, width) height = message.count("\n") + (4,6)[cursorkeys] show_cursor() win = self.window(height, width, message=message) win.keypad(True) input = str(current_value) if cursorkeys: if floating_point: bigstep = 1 smallstep = 0.1 else: bigstep = 100 smallstep = 10 win.addstr(height-4, 2, (" up/down +/- %-3s" % bigstep).rjust(width-4)) win.addstr(height-3, 2, ("left/right +/- %3s" % smallstep).rjust(width-4)) if allow_negative_one: win.addstr(height-3, 2, "-1 means unlimited") if allow_empty: win.addstr(height-4, 2, "leave empty for default") while True: win.addstr(height-2, 2, input.ljust(width-4), curses.color_pair(self.colors.id('dialog'))) win.move(height - 2, len(input) + 2) c = win.getch() if c == 27 or c == ord('q') or c == curses.KEY_BREAK: hide_cursor() return -128 elif c == ord("\n"): try: if allow_empty and len(input) <= 0: hide_cursor() return -2 elif floating_point: hide_cursor() return float(input) else: hide_cursor() return int(input) except ValueError: hide_cursor() return -1 elif c == curses.KEY_BACKSPACE or c == curses.KEY_DC or c == 127 or c == 8: input = input[:-1] elif len(input) >= width-5: curses.beep() elif c >= ord('1') and c <= ord('9'): input += chr(c) elif allow_zero and c == ord('0') and input != '-' and not input.startswith('0'): input += chr(c) elif allow_negative_one and c == ord('-') and len(input) == 0: input += chr(c) elif floating_point and c == ord('.') and not '.' in input: input += chr(c) elif cursorkeys and c != -1: try: if input == '': input = 0 if floating_point: number = float(input) else: number = int(input) if c == curses.KEY_LEFT or c == ord('h'): number -= smallstep elif c == curses.KEY_RIGHT or c == ord('l'): number += smallstep elif c == curses.KEY_DOWN or c == ord('j'): number -= bigstep elif c == curses.KEY_UP or c == ord('k'): number += bigstep if not allow_zero and number <= 0: number = 1 elif not allow_negative_one and number < 0: number = 0 elif number < 0: # input like -0.6 isn't useful number = -1 input = str(number) except ValueError: pass def dialog_menu(self, title, options, focus=1): height = len(options) + 2 width = max(max(map(lambda x: len(x[1])+3, options)), len(title)+3) win = self.window(height, width) win.addstr(0,1, title) win.keypad(True) old_focus = focus while True: keymap = self.dialog_list_menu_options(win, width, options, focus) c = win.getch() if c > 96 and c < 123 and chr(c) in keymap: return options[keymap[chr(c)]][0] elif c == 27 or c == ord('q'): return -128 elif c == ord("\n"): return options[focus-1][0] elif c == curses.KEY_DOWN or c == ord('j') or c == curses.ascii.ctrl(ord('n')): focus += 1 if focus > len(options): focus = 1 elif c == curses.KEY_UP or c == ord('k') or c == curses.ascii.ctrl(ord('p')): focus -= 1 if focus < 1: focus = len(options) elif c == curses.KEY_HOME or c == ord('g'): focus = 1 elif c == curses.KEY_END or c == ord('G'): focus = len(options) def dialog_list_menu_options(self, win, width, options, focus): keys = dict() i = 1 for option in options: title = option[1].split('_') if i == focus: tag = curses.color_pair(self.colors.id('dialog')) else: tag = 0 win.addstr(i,2, title[0], tag) win.addstr(title[1][0], tag + curses.A_UNDERLINE) win.addstr(title[1][1:], tag) win.addstr(''.ljust(width - len(option[1]) - 3), tag) keys[title[1][0].lower()] = i-1 i+=1 return keys def draw_options_dialog(self): enc_options = [('required','_required'), ('preferred','_preferred'), ('tolerated','_tolerated')] seed_ratio = self.stats['seedRatioLimit'] while True: options = [] options.append(('Peer _Port', "%d" % self.stats['peer-port'])) options.append(('UP_nP/NAT-PMP', ('disabled','enabled ')[self.stats['port-forwarding-enabled']])) options.append(('Peer E_xchange', ('disabled','enabled ')[self.stats['pex-enabled']])) options.append(('_Distributed Hash Table', ('disabled','enabled ')[self.stats['dht-enabled']])) options.append(('_Local Peer Discovery', ('disabled','enabled ')[self.stats['lpd-enabled']])) options.append(('Protocol En_cryption', "%s" % self.stats['encryption'])) # uTP support was added in Transmission v2.3 if server.get_rpc_version() >= 13: options.append(('_Micro Transport Protocol', ('disabled','enabled')[self.stats['utp-enabled']])) options.append(('_Global Peer Limit', "%d" % self.stats['peer-limit-global'])) options.append(('Peer Limit per _Torrent', "%d" % self.stats['peer-limit-per-torrent'])) options.append(('T_urtle Mode UL Limit', "%dK" % self.stats['alt-speed-up'])) options.append(('Tu_rtle Mode DL Limit', "%dK" % self.stats['alt-speed-down'])) options.append(('_Seed Ratio Limit', "%s" % ('unlimited',self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']])) # queue was implemented in Transmission v2.4 if server.get_rpc_version() >= 14: options.append(('Do_wnload Queue Size', "%s" % ('disabled',self.stats['download-queue-size'])[self.stats['download-queue-enabled']])) options.append(('S_eed Queue Size', "%s" % ('disabled',self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']])) options.append(('Title is Progress _Bar', ('no','yes')[self.torrentname_is_progressbar])) options.append(('Blan_k lines in non-compact', ('no','yes')[self.blank_lines])) options.append(('File _Viewer', "%s" % self.file_viewer)) max_len = max([sum([len(re.sub('_', '', x)) for x in y[0]]) for y in options]) win_width = min(max(len(self.file_viewer)+5, 15), self.width+max_len) win = self.window(len(options)+2, max_len+win_width, '', "Global Options") line_num = 1 for option in options: parts = re.split('_', option[0]) parts_len = sum([len(x) for x in parts]) win.addstr(line_num, max_len-parts_len+2, parts.pop(0)) for part in parts: win.addstr(part[0], curses.A_UNDERLINE) win.addstr(part[1:] + ': ' + option[1]) line_num += 1 c = win.getch() if c == 27 or c == ord('q') or c == ord("\n"): return elif c == ord('p'): port = self.dialog_input_number("Port for incoming connections", self.stats['peer-port'], cursorkeys=False) if port >= 0 and port <= 65535: server.set_option('peer-port', port) elif port != -128: # user hit ESC self.dialog_ok('Port must be in the range of 0 - 65535') elif c == ord('n'): server.set_option('port-forwarding-enabled', (1,0)[self.stats['port-forwarding-enabled']]) elif c == ord('x'): server.set_option('pex-enabled', (1,0)[self.stats['pex-enabled']]) elif c == ord('d'): server.set_option('dht-enabled', (1,0)[self.stats['dht-enabled']]) elif c == ord('l'): server.set_option('lpd-enabled', (1,0)[self.stats['lpd-enabled']]) # uTP support was added in Transmission v2.3 elif c == ord('m') and server.get_rpc_version() >= 13: server.set_option('utp-enabled', (1,0)[self.stats['utp-enabled']]) elif c == ord('g'): limit = self.dialog_input_number("Maximum number of connected peers", self.stats['peer-limit-global'], allow_negative_one=False) if limit >= 0: server.set_option('peer-limit-global', limit) elif c == ord('t'): limit = self.dialog_input_number("Maximum number of connected peers per torrent", self.stats['peer-limit-per-torrent'], allow_negative_one=False) if limit >= 0: server.set_option('peer-limit-per-torrent', limit) elif c == ord('s'): limit = self.dialog_input_number('Stop seeding with upload/download ratio', (-1,self.stats['seedRatioLimit'])[self.stats['seedRatioLimited']], floating_point=True) if limit >= 0: server.set_option('seedRatioLimit', limit) server.set_option('seedRatioLimited', True) elif limit < 0 and limit != -128: server.set_option('seedRatioLimited', False) elif c == ord('c'): choice = self.dialog_menu('Encryption', enc_options, map(lambda x: x[0]==self.stats['encryption'], enc_options).index(True)+1) if choice != -128: server.set_option('encryption', choice) elif c == ord('u'): limit = self.dialog_input_number('Upload limit for Turtle Mode in kilobytes per second', self.stats['alt-speed-up'], allow_negative_one=False) if limit != -128: server.set_option('alt-speed-up', limit) elif c == ord('r'): limit = self.dialog_input_number('Download limit for Turtle Mode in kilobytes per second', self.stats['alt-speed-down'], allow_negative_one=False) if limit != -128: server.set_option('alt-speed-down', limit) elif c == ord('b'): self.torrentname_is_progressbar = not self.torrentname_is_progressbar # Queue was implemmented in Transmission v2.4 elif c == ord('w') and server.get_rpc_version() >= 14: queue_size = self.dialog_input_number('Download Queue size', (0, self.stats['download-queue-size'])[self.stats['download-queue-enabled']], allow_negative_one = False) if queue_size != -128: if queue_size == 0: server.set_option('download-queue-enabled', False) elif queue_size > 0: if not self.stats['download-queue-enabled']: server.set_option('download-queue-enabled', True) server.set_option('download-queue-size', queue_size) # Queue was implemmented in Transmission v2.4 elif c == ord('e') and server.get_rpc_version() >= 14: queue_size = self.dialog_input_number('Seed Queue size', (0, self.stats['seed-queue-size'])[self.stats['seed-queue-enabled']], allow_negative_one = False) if queue_size != -128: if queue_size == 0: server.set_option('seed-queue-enabled', False) elif queue_size > 0: if not self.stats['seed-queue-enabled']: server.set_option('seed-queue-enabled', True) server.set_option('seed-queue-size', queue_size) elif c == ord('k'): self.blank_lines = not self.blank_lines elif c == ord('v'): viewer = self.dialog_input_text('File Viewer\nExample: xdg-viewer %s', self.file_viewer) if viewer: config.set('Misc', 'file_viewer', viewer.replace('%s','%%s')) self.file_viewer=viewer self.draw_torrent_list() # End of class Interface def percent(full, part): try: percent = 100/(float(full) / float(part)) except ZeroDivisionError: percent = 0.0 return percent def scale_time(seconds, type='short'): minute_in_sec = float(60) hour_in_sec = float(3600) day_in_sec = float(86400) month_in_sec = 27.321661 * day_in_sec # from wikipedia year_in_sec = 365.25 * day_in_sec # from wikipedia if seconds < 0: return ('?', 'some time')[type=='long'] elif seconds < minute_in_sec: if type == 'long': if seconds < 5: return 'now' else: return "%d second%s" % (seconds, ('', 's')[seconds>1]) else: return "%ds" % seconds elif seconds < hour_in_sec: minutes = round(seconds / minute_in_sec, 0) if type == 'long': return "%d minute%s" % (minutes, ('', 's')[minutes>1]) else: return "%dm" % minutes elif seconds < day_in_sec: hours = round(seconds / hour_in_sec, 0) if type == 'long': return "%d hour%s" % (hours, ('', 's')[hours>1]) else: return "%dh" % hours elif seconds < month_in_sec: days = round(seconds / day_in_sec, 0) if type == 'long': return "%d day%s" % (days, ('', 's')[days>1]) else: return "%dd" % days elif seconds < year_in_sec: months = round(seconds / month_in_sec, 0) if type == 'long': return "%d month%s" % (months, ('', 's')[months>1]) else: return "%dM" % months else: years = round(seconds / year_in_sec, 0) if type == 'long': return "%d year%s" % (years, ('', 's')[years>1]) else: return "%dy" % years def timestamp(timestamp, format="%x %X"): if timestamp < 1: return 'never' if timestamp > 2147483647: # Max value of 32bit signed integer (2^31-1) # Timedelta objects do not fail on timestamps # resulting in a date later than 2038 date = (datetime.datetime.fromtimestamp(0) + datetime.timedelta(seconds=timestamp)) timeobj = date.timetuple() else: timeobj = time.localtime(timestamp) absolute = time.strftime(format, timeobj) if timestamp > time.time(): relative = 'in ' + scale_time(int(timestamp - time.time()), 'long') else: relative = scale_time(int(time.time() - timestamp), 'long') + ' ago' if relative.startswith('now') or relative.endswith('now'): relative = 'now' return "%s (%s)" % (absolute, relative) def scale_bytes(bytes, type='short'): if bytes >= 1073741824: scaled_bytes = round((bytes / 1073741824.0), 2) unit = 'G' elif bytes >= 1048576: scaled_bytes = round((bytes / 1048576.0), 1) if scaled_bytes >= 100: scaled_bytes = int(scaled_bytes) unit = 'M' elif bytes >= 1024: scaled_bytes = int(bytes / 1024) unit = 'K' else: scaled_bytes = round((bytes / 1024.0), 1) unit = 'K' # handle 0 bytes special if bytes == 0 and type == 'long': return 'nothing' # convert to integer if .0 if int(scaled_bytes) == float(scaled_bytes): scaled_bytes = str(int(scaled_bytes)) else: scaled_bytes = str(scaled_bytes).rstrip('0') if type == 'long': return num2str(bytes) + ' [' + scaled_bytes + unit + ']' else: return scaled_bytes + unit def homedir2tilde(path): return re.sub(r'^'+os.environ['HOME'], '~', path) def tilde2homedir(path): return re.sub(r'^~', os.environ['HOME'], path) def html2text(str): str = re.sub(r'', "\n", str) str = re.sub(r'

', ' ', str) str = re.sub(r'<[^>]*?>', '', str) return str def hide_cursor(): try: curses.curs_set(0) # hide cursor if possible except curses.error: pass # some terminals seem to have problems with that def show_cursor(): try: curses.curs_set(1) except curses.error: pass def wrap_multiline(text, width, initial_indent='', subsequent_indent=None): if subsequent_indent is None: subsequent_indent = ' ' * len(initial_indent) for line in text.splitlines(): # this is required because wrap() strips empty lines if not line.strip(): yield line continue for line in wrap(line, width, replace_whitespace=False, initial_indent=initial_indent, subsequent_indent=subsequent_indent): yield line initial_indent = subsequent_indent def ljust_columns(text, max_width, padchar=' '): """ Returns a string that is exactly display columns wide, padded with if necessary. Accounts for characters that are displayed two columns wide, i.e. kanji. """ chars = [] columns = 0 max_width = max(0, max_width) for character in text: width = len_columns(character) if columns + width <= max_width: chars.append(character) columns += width else: break # Fill up any remaining space while columns < max_width: assert len(padchar) == 1 chars.append(padchar) columns += 1 return ''.join(chars) def len_columns(text): """ Returns the amount of columns that would occupy. """ if isinstance(text, str): text = unicode(text, ENCODING) columns = 0 for character in text: columns += 2 if unicodedata.east_asian_width(character) in ('W', 'F') else 1 return columns def num2str(num, format='%s'): if int(num) == -1: return '?' elif int(num) == -2: return 'oo' else: if num > 999: return (re.sub(r'(\d{3})', '\g<1>,', str(num)[::-1])[::-1]).lstrip(',') else: return format % num def debug(data): if cmd_args.DEBUG: file = open("debug.log", 'a') if isinstance(data, str) or isinstance(data, unicode): file.write(data.encode('UTF-8', 'replace')) else: import pprint pp = pprint.PrettyPrinter(indent=4) file.write("\n====================\n" + pp.pformat(data) + "\n====================\n\n") file.close def quit(msg='', exitcode=0): try: curses.endwin() except curses.error: pass # if this is a graceful exit and config file is present if not msg and not exitcode: save_config(cmd_args.configfile) else: print >> sys.stderr, msg, os._exit(exitcode) def explode_connection_string(connection): host, port, path = \ config.get('Connection', 'host'), \ config.getint('Connection', 'port'), \ config.get('Connection', 'path') username, password = \ config.get('Connection', 'username'), \ config.get('Connection', 'password') try: if connection.count('@') == 1: auth, connection = connection.split('@') if auth.count(':') == 1: username, password = auth.split(':') if connection.count(':') == 1: host, port = connection.split(':') if port.count('/') >= 1: port, path = port.split('/', 1) port = int(port) else: host = connection except ValueError: quit("Wrong connection pattern: %s\n" % connection) return host, port, path, username, password def create_url(host, port, path): url = '%s:%d/%s' % (host, port, path) url = url.replace('//', '/') # double-/ doesn't work for some reason if config.getboolean('Connection', 'ssl'): return 'https://%s' % url else: return 'http://%s' % url def read_netrc(file=os.environ['HOME'] + '/.netrc', hostname=None): try: login = password = '' try: login, account, password = netrc.netrc(file).authenticators(hostname) except TypeError: pass try: netrc.netrc(file).hosts[hostname] except KeyError: if hostname != 'localhost': print "Unknown machine in %s: %s" % (file, hostname) if login and password: print "Using default login: %s" % login else: exit(CONFIGFILE_ERROR) except netrc.NetrcParseError, e: quit("Error in %s at line %s: %s\n" % (e.filename, e.lineno, e.msg)) except IOError, msg: quit("Cannot read %s: %s\n" % (file, msg)) return login, password # create initial config file def create_config(option, opt_str, value, parser): configfile = parser.values.configfile config.read(configfile) if parser.values.connection: host, port, path, username, password = explode_connection_string(parser.values.connection) config.set('Connection', 'host', host) config.set('Connection', 'port', str(port)) config.set('Connection', 'path', path) config.set('Connection', 'username', username) config.set('Connection', 'password', password) # create directory if necessary dir = os.path.dirname(configfile) if dir != '' and not os.path.isdir(dir): try: os.makedirs(dir) except OSError, msg: print msg exit(CONFIGFILE_ERROR) # write file if not save_config(configfile, force=True): exit(CONFIGFILE_ERROR) print "Wrote config file: %s" % configfile exit(0) def save_config(filepath, force=False): if force or os.path.isfile(filepath): try: config.write(open(filepath, 'w')) os.chmod(filepath, 0600) # config may contain password return 1 except IOError, msg: print >> sys.stderr, "Cannot write config file %s:\n%s" % (filepath, msg) return 0 return -1 def parse_sort_str(sort_str): sort_orders = [] for i in sort_str.split(','): x = i.split(':') if len(x) > 1: sort_orders.append( { 'name':x[1], 'reverse':True } ) else: sort_orders.append( { 'name':x[0], 'reverse':False } ) return sort_orders def show_version(option, opt_str, value, parser): quit("transmission-remote-cli %s (supports Transmission %s-%s)\n" % \ (VERSION, TRNSM_VERSION_MIN, TRNSM_VERSION_MAX)) if __name__ == '__main__': # command line parameters default_config_path = os.environ['HOME'] + '/.config/transmission-remote-cli/settings.cfg' parser = OptionParser(usage="%prog [options] [-- transmission-remote options]", description="%%prog %s" % VERSION) parser.add_option("-v", "--version", action="callback", callback=show_version, help="Show version number and supported Transmission versions.") parser.add_option("-c", "--connect", action="store", dest="connection", default="", help="Point to the server using pattern [username:password@]host[:port]/[path]") parser.add_option("-s", "--ssl", action="store_true", dest="ssl", default=False, help="Connect to Transmission using SSL.") parser.add_option("-f", "--config", action="store", dest="configfile", default=default_config_path, help="Path to configuration file.") parser.add_option("--create-config", action="callback", callback=create_config, help="Create configuration file CONFIGFILE with default values.") parser.add_option("-n", "--netrc", action="store_true", dest="use_netrc", default=False, help="Get authentication info from your ~/.netrc file.") parser.add_option("--debug", action="store_true", dest="DEBUG", default=False, help="Everything passed to the debug() function will be added to the file debug.log.") (cmd_args, transmissionremote_args) = parser.parse_args() # read config from config file config.read(cmd_args.configfile) # command line connection data can override config file if cmd_args.connection: host, port, path, username, password = explode_connection_string(cmd_args.connection) config.set('Connection', 'host', host) config.set('Connection', 'port', str(port)) config.set('Connection', 'path', path) config.set('Connection', 'username', username) config.set('Connection', 'password', password) if cmd_args.use_netrc: username, password = read_netrc(hostname=config.get('Connection','host')) config.set('Connection', 'username', username) config.set('Connection', 'password', password) if cmd_args.ssl: config.set('Connection', 'ssl', 'True') # forward arguments after '--' to transmission-remote if transmissionremote_args: cmd = ['transmission-remote', '%s:%s' % (config.get('Connection', 'host'), config.get('Connection', 'port'))] if find_executable(cmd[0]) is None: quit("Command not found: %s\n" % cmd[0], 128) # one argument and it doesn't start with '-' --> treat it like it's a torrent link/url if len(transmissionremote_args) == 1 and not transmissionremote_args[0].startswith('-'): cmd.extend(['-a', transmissionremote_args[0]]) else: cmd.extend(transmissionremote_args) if config.get('Connection', 'username') and config.get('Connection', 'password'): cmd_print = cmd cmd_print.extend(['--auth', '%s:PASSWORD' % config.get('Connection', 'username')]) print "EXECUTING:\n%s\nRESPONSE:" % ' '.join(cmd_print) cmd.extend(['--auth', '%s:%s' % (config.get('Connection', 'username'), config.get('Connection', 'password'))]) else: print "EXECUTING:\n%s\nRESPONSE:" % ' '.join(cmd) try: retcode = call(cmd) except OSError, msg: quit("Could not execute the above command: %s\n" % msg.strerror, 128) quit('', retcode) norm = Normalizer() server = Transmission(config.get('Connection', 'host'), config.getint('Connection', 'port'), config.get('Connection', 'path'), config.get('Connection', 'username'), config.get('Connection', 'password')) Interface() transmission-remote-cli-1.7.0/transmission-remote-cli.1000066400000000000000000000033221233471375700231730ustar00rootroot00000000000000.\"Created with GNOME Manpages Editor Wizard .\"http://sourceforge.net/projects/gmanedit2 .TH transmission-remote-cli 1 "April 22, 2012" "" "transmission-remote-cli" .SH NAME transmission-remote-cli \- a console client for the Transmission BitTorrent client .SH SYNOPSIS .B transmission-remote-cli .RI [ options ] .RI [ filename-or-URL ] .br .SH DESCRIPTION .B transmission-remote-cli is a curses interface for the daemon of the BitTorrent client Transmission. .SH OPTIONS .B .IP "--version" Show version number and exit .B .IP "-h --help" Show usage information and a list of options .B .IP "-c \fICONNECTION\fB --connect=\fICONNECTION\fR" Point to the server. \fICONNECTION\fR must match the following pattern: .br [username:password@]host[:port][path] .br Default: localhost:9091 .B .IP "-s --ssl" Use SSL to connect to the server. .br Default: don't use SSL .B .IP "--create-config" Create configuration file with default values. .br \fINOTE:\fR A config file won't be created unless you provide this option at least once. After that, it is rewritten whenever transmission-remote-cli exits. .IP "-f \fICONFIGFILE\fB --config=\fICONFIGFILE\fR" Set path to configuration file. .br Default: ~/.config/transmission-remote-cli/settings.cfg .B .IP "-n --netrc" Get authentication info from ~/.netrc. .B .IP "-- \fIOPTIONS\fR" Use the known server connection to pass \fIOPTIONS\fR on to \fBtransmission-remote\fR. .B .SH FILES transmission-remote-cli overwrites the configuration file on exit. Keep that in mind if you edit it manually. .SH SEE ALSO .BR transmission-create (1), .BR transmission-daemon (1), .BR transmission-edit (1), .BR transmission-gtk (1), .BR transmission-qt (1), .BR transmission-remote (1), .BR transmission-show (1).