encuentro-5.0/0000775000175000017500000000000013144662314014446 5ustar facundofacundo00000000000000encuentro-5.0/source_encuentro.py0000644000175000017500000000222112330722234020367 0ustar facundofacundo00000000000000 # Copyright 2011-2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Configuration file for Apport.""" from apport.hookutils import attach_related_packages, attach_file_if_exists from encuentro import logger def add_info(report): """Add info to the report.""" # attach the log fname = logger.get_filename() attach_file_if_exists(report, fname, "EncuentroLog") # info about dependencies packages = ["python-requests", "python-defer", "python-qt4", "python-xdg"] attach_related_packages(report, packages) encuentro-5.0/COPYING0000644000175000017500000010437411633737203015511 0ustar facundofacundo00000000000000 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 . encuentro-5.0/encuentro/0000775000175000017500000000000013144662314016450 5ustar facundofacundo00000000000000encuentro-5.0/encuentro/data.py0000664000175000017500000002377512640744543017756 0ustar facundofacundo00000000000000# Copyright 2013-2014 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals, print_function """Classes to interface to and persist the episodes data.""" import cgi import logging import os import pickle from base64 import b64decode from unicodedata import normalize from encuentro import utils from encuentro.ui import dialogs logger = logging.getLogger('encuentro.data') class Status(object): """Status constants.""" none = 'none' waiting = 'waiting' downloading = 'downloading' downloaded = 'downloaded' _normalize_cache = {} def _search_normalizer(char): """Normalize always to one char length.""" try: return _normalize_cache[char] except KeyError: norm = normalize('NFKD', char).encode('ASCII', 'ignore').lower() if not norm: norm = '?' _normalize_cache[char] = norm return norm def prepare_to_filter(text): """Prepare a text to filter. It receives unicode, but return simple lowercase ascii. """ return ''.join(_search_normalizer(c) for c in text) class EpisodeData(object): """Episode data.""" # these is for the attributes to be here when unpickling old instances image_url = None downtype = None image_data = None subtitle = None def __init__(self, channel, section, title, duration, description, episode_id, url, image_url, state=None, progress=None, filename=None, downtype=None, season=None, image_data=None, subtitle=None): self.channel = channel self.section = section self.season = None if season is None else cgi.escape(season) self.title = cgi.escape(title) self.duration = duration self.description = description self.subtitle = subtitle self.episode_id = episode_id # build a nice string to show in the GUI if self.season: self.composed_title = "{}: {}".format(self.season, self.title) else: self.composed_title = self.title # urls are bytes! self.url = str(url) self.image_url = str(image_url) # image data is encoded in base64 self.image_data = None if image_data is None else b64decode(image_data) self.state = Status.none if state is None else state self.progress = progress self.filename = filename self.to_filter = None self.downtype = downtype # cache the processed title self._normalized_title = prepare_to_filter(self.composed_title) @property def normalized_title(self): """Get the normalized title, if already have it, or calculate it. This attribute may not be present because old pickled instances didn't have it. """ if not hasattr(self, '_normalized_title'): self._normalized_title = prepare_to_filter(self.composed_title) return self._normalized_title def update(self, channel, section, title, duration, description, episode_id, url, image_url, state=None, progress=None, filename=None, downtype=None, season=None, image_data=None, subtitle=None): """Update the episode data.""" self.channel = channel self.section = section self.season = None if season is None else cgi.escape(season) self.title = cgi.escape(title) self.duration = duration self.description = description self.subtitle = subtitle self.episode_id = episode_id # build a nice string to show in the GUI if self.season: self.composed_title = "{}: {}".format(self.season, self.title) else: self.composed_title = self.title # urls are bytes! self.url = str(url) self.image_url = str(image_url) # image data is encoded in base64 self.image_data = None if image_data is None else b64decode(image_data) self.state = Status.none if state is None else state self.progress = progress self.filename = filename self.downtype = downtype # cache the processed title, overwritting what may be old from the past self._normalized_title = prepare_to_filter(self.composed_title) def filter_params(self, text, only_downloaded): """Return the filtering params. If should filter, it will return (pos1, pos2) (both in None if it only filters by only_downloaded). If it should not filter, will return None. """ if only_downloaded and self.state != Status.downloaded: # need downloaded ones, sorry return t = self.normalized_title pos1 = t.find(text) if pos1 == -1: # need to match text, sorry return # return boundaries pos2 = pos1 + len(text) return (pos1, pos2) def __str__(self): args = (self.episode_id, self.state, self.channel, self.section, self.title) return "" % args class ProgramsData(object): """Holder / interface for programs data.""" # more recent version of the in-disk data last_programs_version = 2 def __init__(self, main_window, filename): self.main_window = main_window self.filename = filename print("Using data file:", repr(filename)) logger.info("Using data file: %r", filename) self.version = None self.data = None self.reset_config_from_migration = False self.load() self.migrate() logger.info("Episodes metadata loaded (total %d)", len(self.data)) def merge(self, new_data, episodes_widget): """Merge new data to current programs data.""" for d in new_data: names = ['channel', 'section', 'title', 'duration', 'description', 'episode_id', 'url', 'image_url', 'downtype', 'season', 'image_data', 'subtitle'] values = dict((name, d.get(name)) for name in names) episode_id = d['episode_id'] try: ed = self.data[episode_id] except KeyError: self.data[episode_id] = EpisodeData(**values) else: ed.update(**values) episodes_widget.reload_episodes() self.save() def load(self): """Load the data from the file.""" # if not file, all empty if not os.path.exists(self.filename): self.data = {} self.version = self.last_programs_version return # get from the file with open(self.filename, 'rb') as fh: try: loaded_programs_data = pickle.load(fh) except Exception as err: logger.warning("ERROR while opening the pickled data: %s", err) self.data = {} self.version = self.last_programs_version return # check pre-versioned data if isinstance(loaded_programs_data, dict): # pre-versioned data self.version = 0 self.data = loaded_programs_data else: self.version, self.data = loaded_programs_data def migrate(self): """Migrate metadata if needed.""" if self.version == self.last_programs_version: logger.info("Metadata is updated, nothing to migrate") return if self.version > self.last_programs_version: raise ValueError("Data is newer than code! %s" % (self.version,)) # migrate if self.version == 0: logger.info("Migrating from version 0") # actually, from 0, no migration is possible, we # need to tell the user the ugly truth dlg = dialogs.ForceUpgradeDialog() should_quit = dlg.exec_() if should_quit: self.main_window.shutdown() return # if user accessed to go on, don't really need to migrate # anything, as *all* the code is to support the new metadata # version only, so just remove it and mark the usr/pass config # to be removed self.version = self.last_programs_version self.reset_config_from_migration = True self.data = {} return if self.version == 1: logger.info("Migrating from version 1") self.version = self.last_programs_version for epis_id, episode in self.data.items(): episode.composed_title = episode.title return raise ValueError("Don't know how to migrate from %r" % (self.version,)) def __str__(self): return "" % (self.version, len(self.data)) def __nonzero__(self): return bool(self.data) def __getitem__(self, pos): return self.data[pos] def __setitem__(self, pos, value): self.data[pos] = value def values(self): """Return the iter values of the data.""" return self.data.itervalues() def __len__(self): """The length.""" return len(self.data) def items(self): """Return the iter items of the data.""" return self.data.iteritems() def save(self): """Save to disk.""" to_save = (self.last_programs_version, self.data) with utils.SafeSaver(self.filename) as fh: pickle.dump(to_save, fh) encuentro-5.0/encuentro/__init__.py0000644000175000017500000000500612637746176020577 0ustar facundofacundo00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright 2011-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals, print_function """The package.""" import sys # special import before any other imports to configure GUI to use API 2 import sip for name in "QDate QDateTime QString QTextStream QTime QUrl QVariant".split(): sip.setapi(name, 2) # API v2 FTW! IMPORT_MSG = """ ERROR! Problema al importar %(module)r Probablemente falte instalar una dependencia. Se necesita tener instalado el paquete %(package)r versión %(version)s o superior. """ class NiceImporter(object): """Show nicely successful and errored imports.""" def __init__(self, module, package, version): self.module = module self.package = package self.version = version def __enter__(self): pass def _get_version(self): """Get the version of a module.""" mod = sys.modules[self.module] for attr in ('version', '__version__', 'ver', 'PYQT_VERSION_STR'): v = getattr(mod, attr, None) if v is not None: return v return "" def __exit__(self, exc_type, exc_value, traceback): if exc_type is None: version = self._get_version() print("Módulo %r importado ok, versión %r" % (self.module, version)) else: print(IMPORT_MSG % dict(module=self.module, package=self.package, version=self.version)) # consume the exception! return True # test the packages with NiceImporter('xdg', 'python-xdg', '0.15'): import xdg # NOQA with NiceImporter('requests', 'python-requests', '2.2.1'): import requests # NOQA with NiceImporter('PyQt4.QtCore', 'PyQt4', '4.9.1'): import PyQt4.QtCore # NOQA with NiceImporter('defer', 'python-defer', '1.0.6'): import defer # NOQA encuentro-5.0/encuentro/utils.py0000644000175000017500000001074613033060645020164 0ustar facundofacundo00000000000000# Copyright 2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import print_function """Some useful functions.""" import defer import os from PyQt4 import QtNetwork, QtCore _qt_network_manager = QtNetwork.QNetworkAccessManager() class _Downloader(object): """An asynch downloader that fires a deferred with data when done.""" def __init__(self, url): self.deferred = defer.Deferred() self.deferred._store_it_because_qt_needs_or_wont_work = self self.progress = 0 request = QtNetwork.QNetworkRequest(QtCore.QUrl(url)) self.req = _qt_network_manager.get(request) self.req.error.connect(self.error) self.req.finished.connect(self.end) self.req.downloadProgress.connect(self._advance_progress) def error(self, error_code): """Request finished (*maybe*) on error.""" if error_code != 5: # different to OperationCanceledError, so we didn't provoke it exc = RuntimeError("Network Error: " + self.req.errorString()) self.deferred.errback(exc) def _advance_progress(self, dloaded, total): """Increment progress.""" self.progress = dloaded def abort(self): """Abort the download.""" self.req.abort() def end(self): """Send data through the deferred, if wasn't fired before.""" img_data = self.req.read(self.req.bytesAvailable()) content_type = self.req.header( QtNetwork.QNetworkRequest.ContentTypeHeader) data = (content_type, img_data) if not self.deferred.called: self.deferred.callback(data) def download(url): """Deferredly download an URL, non blocking. It starts a _Downloader, and supervises if it stalled. If didn't transfer anything for a whole second, abort it and create a new one. This is to overcome some QtNetwork weirdness that will probably go away in future Qt versions, but that froze the downloading somewhen somehow. """ general_deferred = defer.Deferred() state = [_Downloader(url), 0] def check(): dloader, prev_prog = state if dloader.deferred.called: # finished, passthrough the results dloader.deferred.add_callbacks(general_deferred.callback, general_deferred.errback) else: if dloader.progress == prev_prog: # stalled! need to restart it dloader.abort() state[0] = _Downloader(url) state[1] = 0 else: # keep going, update progress state[1] = dloader.progress # for both cases, new downloader or old still running, check later QtCore.QTimer.singleShot(1000, check) QtCore.QTimer.singleShot(1000, check) return general_deferred class SafeSaver(object): """A safe saver to disk. It saves to a .tmp and moves into final destination, and other considerations. """ def __init__(self, fname): self.fname = fname self.tmp = fname + ".tmp" self.fh = None def __enter__(self): self.fh = open(self.tmp, 'wb') return self.fh def __exit__(self, *exc_data): self.fh.close() # only move into final destination if all went ok if exc_data == (None, None, None): if os.path.exists(self.fname): # in Windows we need to remove the old file first os.remove(self.fname) os.rename(self.tmp, self.fname) if __name__ == "__main__": import sys app = QtCore.QCoreApplication(sys.argv) _url = "http://www.taniquetil.com.ar/facundo/imgs/felu-camagrande.jpg" @defer.inline_callbacks def _download(): """Download.""" deferred = download(_url) data = yield deferred print("All done!", len(data), type(data)) _download() sys.exit(app.exec_()) encuentro-5.0/encuentro/main.py0000644000175000017500000000336413131470344017746 0ustar facundofacundo00000000000000# Copyright 2013-2017 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Main entry point, and initialization of everything we can.""" from __future__ import print_function import logging import os import sys from encuentro import multiplatform from encuentro.config import config from encuentro.ui.main import MainUI # we put here EpisodeData only for legacy reasons: unpickle of old pickles # will try to load EpisodeData from this namespace from encuentro.data import EpisodeData # NOQA from PyQt4.QtGui import QApplication, QIcon logger = logging.getLogger('encuentro.init') def start(version, update_source): """Rock and roll.""" # set up config fname = os.path.join(multiplatform.config_dir, 'encuentro.conf') print("Using configuration file:", repr(fname)) logger.info("Using configuration file: %r", fname) config.init(fname) # the order of the lines hereafter are very precise, don't mess with them app = QApplication(sys.argv) icon = QIcon(multiplatform.get_path("encuentro/logos/icon-192.png")) app.setWindowIcon(icon) MainUI(version, app.quit, update_source) sys.exit(app.exec_()) encuentro-5.0/encuentro/update.py0000664000175000017500000001774613131470344020317 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2011-2017 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """Update the episodes metadata.""" import bz2 import json import logging import os from datetime import datetime import defer from PyQt4.QtGui import QApplication from encuentro import utils from encuentro.config import config from encuentro.ui import dialogs # main entry point to download all backends data BACKENDS_BASE_URL = "http://www.taniquetil.com.ar/encuentro/" BACKENDS_LIST = "backends-v07.list" logger = logging.getLogger('encuentro.update') class UpdateEpisodes(object): """Update the episodes info.""" def __init__(self, main_window, update_source): self.main_window = main_window self.update_source = update_source def background(self): """Trigger an update in background.""" self._update() def interactive(self): """Update episodes interactively.""" dialog = dialogs.UpdateDialog() dialog.show() self._update(dialog) @defer.inline_callbacks def _get(self, filename): """Get the content from the server or a local source.""" if self.update_source is None: # from the server url = BACKENDS_BASE_URL + filename logger.debug("Getting content from url %r", url) _, content = yield utils.download(url) else: # from a local source filepath = os.path.join(self.update_source, filename) logger.debug("Getting content from filepath %r", filepath) with open(filepath, 'rb') as fh: content = fh.read() defer.return_value(content) @defer.inline_callbacks def _update(self, dialog=None): """Update the content from source, being it server or something indicated at start.""" if dialog: # when loading from disk we won't free the CPU much, so let's # leave some time for Qt to work (here on start and on each message below) QApplication.processEvents() def tell_user(template, *elements): if elements: try: msg = template % elements except Exception as err: msg = "ERROR %s when building message (template=%r, elements=%s" % ( err, template, elements) else: msg = template dialog.append(msg) QApplication.processEvents() else: def tell_user(*t): """Do nothing.""" logger.info("Downloading backend list") tell_user("Descargando la lista de backends...") try: backends_file = yield self._get(BACKENDS_LIST) except Exception as e: logger.error("Problem when downloading backends: %s", e) tell_user("Hubo un PROBLEMA al bajar la lista de backends: %s", e) return if dialog and dialog.closed: return backends_list = [l.strip().split() for l in backends_file.split("\n") if l and l[0] != '#'] backends = {} for b_name, b_dloader, b_filename in backends_list: logger.info("Downloading backend metadata for %r", b_name) tell_user("Descargando la lista de episodios para backend %r...", b_name) try: compressed = yield self._get(b_filename) except Exception as e: logger.error("Problem when downloading episodes: %s", e) tell_user("Hubo un PROBLEMA al bajar los episodios: %s", e) return if dialog and dialog.closed: return tell_user("Descomprimiendo el archivo....") new_content = bz2.decompress(compressed) logger.debug("Downloaded data decompressed ok") content = json.loads(new_content) for item in content: item['downtype'] = b_dloader backends[b_name] = content if dialog and dialog.closed: return tell_user("Conciliando datos de diferentes backends") logger.debug("Merging backends data") new_data = self._merge(backends) tell_user("Actualizando los datos internos (%d)....", len(new_data)) logger.debug("Updating internal metadata (%d)", len(new_data)) self.main_window.programs_data.merge( new_data, self.main_window.big_panel.episodes) config.update({'autorefresh_last_time': datetime.now()}) config.save() tell_user("¡Todo terminado bien!") if dialog: dialog.accept() def _merge(self, backends): """Merge content from all backends. This is for v03-05, with only 'encuentro' and 'conectar' data to be really merged, other data just appended. """ raw_encuentro_data = backends.pop('encuentro') raw_conectar_data = backends.pop('conectar') enc_data = dict((x['episode_id'], x) for x in raw_encuentro_data) con_data = dict((x['episode_id'], x) for x in raw_conectar_data) common = set(enc_data) & set(con_data) logger.debug("Merging: encuentro=%d conectar=%d (common=%d)", len(enc_data), len(con_data), len(common)) # what is in not common in both goes untouched final_data = ([enc_data[epid] for epid in set(enc_data) - common] + [con_data[epid] for epid in set(con_data) - common]) # what is common, we need to do the merge for epid in common: enc_ep = enc_data[epid] con_ep = con_data[epid] enc_desc = enc_ep['description'] con_desc = con_ep['description'] if enc_desc == con_desc: description = enc_desc elif enc_desc is None: description = con_desc elif con_desc is None: description = enc_desc else: # not None, or they would have been the same, let's concat # both, shorter first if len(con_desc) < len(enc_desc): description = con_desc + ' ' + enc_desc else: description = enc_desc + ' ' + con_desc # if both are equal (None or not), it also works if enc_ep['duration'] is None: duration = con_ep['duration'] else: duration = enc_ep['duration'] if enc_ep['image_url'] is None: image_url = con_ep['image_url'] else: image_url = enc_ep['image_url'] if enc_ep['season'] is None: season = con_ep['season'] else: season = enc_ep['season'] d = dict(episode_id=epid, description=description, duration=duration, url=con_ep['url'], channel=con_ep['channel'], title=con_ep['title'], section=con_ep['section'], image_url=image_url, downtype=con_ep['downtype'], season=season) final_data.append(d) logger.debug("Merging: appending other data: %s", backends.keys()) for data in backends.itervalues(): final_data.extend(data) logger.debug("Merged, final: %d", len(final_data)) return final_data encuentro-5.0/encuentro/network.py0000644000175000017500000005660413131470344020520 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2011-2017 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Some functions to deal with network and Encuentro site.""" from __future__ import unicode_literals, print_function import json import logging import os import sys import time import urllib import urllib2 from threading import Thread, Event from Queue import Queue, Empty import bs4 import defer import requests if __name__ == '__main__': # special import before any other imports to configure GUI to use API 2; we # normally don't need to do this *here*, just a support for run # this as a script, for testing/development purpuses import sip for n in "QDate QDateTime QString QTextStream QTime QUrl QVariant".split(): sip.setapi(n, 2) # API v2 FTW! from PyQt4 import QtNetwork, QtCore # NOQA (import not at the top) from encuentro import multiplatform, utils # NOQA (import not at the top) from encuentro.config import config # NOQA (import not at the top) # special import sequence to get a useful version of youtube-dl try: import youtube_dl except ImportError: youtube_dl = None else: _version = getattr(youtube_dl, '__version__', getattr(youtube_dl.version, '__version__', None)) if _version < '2015.12.21': # older than builtin version for k in [x for x in sys.modules if x.startswith('youtube_dl')]: del sys.modules[k] youtube_dl = None if youtube_dl is None: # inexistant or too old, let's use the builtin version root_path = os.path.dirname(os.path.dirname(__file__)) builtin_path = os.path.join(os.path.abspath(root_path), "external", "youtube-dl") sys.path.insert(0, builtin_path) import youtube_dl AUTH_URL = "http://registro.educ.ar/cuentas/ServicioLogin/index" CHUNK = 16 * 1024 MB = 1024 ** 2 BAD_LOGIN_TEXT = b"Ingreso de usuario" DONE_TOKEN = "I positively assure that the download is finished (?)" logger = logging.getLogger('encuentro.network') def clean_fname(fname): """Transform a filename into pure ASCII, to be saved anywhere.""" try: return fname.encode('ascii') except UnicodeError: return "".join(urllib.quote(x.encode("utf-8")) if ord(x) > 127 else x for x in fname) class BadCredentialsError(Exception): """Problems with user and/or password.""" class EncuentroError(Exception): """Generic problem working with the Encuentro web site.""" def __init__(self, message, original_exception=None): self.orig_exc = original_exception super(EncuentroError, self).__init__(message) class CancelledError(Exception): """The download was cancelled.""" class Finished(Exception): """Special exception (to be ignored) used by some Downloaders to finish themselves.""" class BaseDownloader(object): """Base episode downloader.""" def __init__(self): self.deferred = defer.Deferred() self.cancelled = False def log(self, text, *args): """Build a better log line.""" new_text = "[%s.%s] " + text new_args = (self.__class__.__name__, id(self)) + args logger.info(new_text, *new_args) def shutdown(self): """Quit the download.""" return self._shutdown() def cancel(self): """Cancel a download.""" return self._cancel() def _setup_target(self, channel, section, season, title, extension): """Set up the target file to download.""" # build where to save it downloaddir = config.get('downloaddir', '') channel = multiplatform.sanitize(channel) section = multiplatform.sanitize(section) title = multiplatform.sanitize(title) if season is not None: season = multiplatform.sanitize(season) fname = os.path.join(downloaddir, channel, section, season, title + extension) else: fname = os.path.join(downloaddir, channel, section, title + extension) if config.get('clean-filenames'): cleaned = clean_fname(fname) self.log("Cleaned filename %r into %r", fname, cleaned) fname = cleaned # if the directory doesn't exist, create it dirsecc = os.path.dirname(fname) if not os.path.exists(dirsecc): os.makedirs(dirsecc) tempf = fname + str(time.time()) return fname, tempf def download(self, channel, section, season, title, url, cb_progress): """Download an episode.""" @defer.inline_callbacks def wrapper(): """Wrapp real download and feed any exception through proper deferred.""" try: yield self._download(channel, section, season, title, url, cb_progress) except Exception as err: self.deferred.errback(err) QtCore.QTimer.singleShot(50, wrapper) def _clean(self, filename): """Remove a filename in a very safe way. Note: under Windows is tricky to remove files that may still be used. """ if os.path.exists(filename): try: os.remove(filename) except Exception as err: self.log("Cleaning failed: %r", err) else: self.log("Cleaned ok") class MiBrowser(Thread): """Threaded browser to do the download.""" # we *are* calling parent's init; pylint: disable=W0231 def __init__(self, parent, authuser, authpass, url, fname, output_queue, must_quit, log): self.parent = parent self.authinfo = authuser, authpass self.url = url self.fname = fname self.output_queue = output_queue self.must_quit = must_quit self.log = log super(MiBrowser, self).__init__() def _get_download_content(self): """Get the content handler to download.""" # log in self.log("Browser download, authenticating") usr, psw = self.authinfo get_data = dict( servicio=self.parent.service, continuar=urllib.quote(self.url), ) complete_auth_url = AUTH_URL + "?" + urllib.urlencode(get_data) post_data = dict( login_user_name=usr, login_user_password=psw, r=complete_auth_url, ) sess = requests.Session() sess.post(complete_auth_url, post_data) # get page with useful link self.log("Browser download, getting html") html = sess.get(complete_auth_url).content if BAD_LOGIN_TEXT in html: self.log("Wrong user or password sent") raise BadCredentialsError() self.log("Browser download, got html len %d", len(html)) # download from the new url html = sess.get(self.url).content soup = bs4.BeautifulSoup(html) new_url = soup.find(attrs={'class': 'descargas panel row'}).find('a')['href'] self.log("Opening final url %r", new_url) content = urllib2.urlopen(new_url) try: filesize = int(content.headers['content-length']) except KeyError: self.log("No content information") else: self.log("Got content! filesize: %d", filesize) return content, filesize # ok, we don't know what happened :( self.log("Unknown error while browsing Encuentro: %r", html) raise EncuentroError("Unknown problem when getting download link") def _really_download(self, content, filesize): """Effectively download the content to disk.""" aout = open(self.fname, "wb") tot = 0 size_mb = filesize / (1024.0 ** 2) while not self.must_quit.is_set(): r = content.read(CHUNK) if r == b"": break aout.write(r) tot += len(r) m = "%.1f%% (de %d MB)" % (tot * 100.0 / filesize, size_mb) self.output_queue.put(m) content.close() self.output_queue.put(DONE_TOKEN) def run(self): """Do the heavy work.""" self.log("Browser opening url %s", self.url) try: content, filesize = self._get_download_content() self._really_download(content, filesize) except Exception as err: self.output_queue.put(err) class DeferredQueue(Queue): """A Queue with a deferred get.""" _call_period = 500 def deferred_get(self): """Return a deferred that is triggered when data.""" d = defer.Deferred() attempts = [None] * 6 def check(): """Check if we have data and transmit it.""" try: data = self.get(block=False) except Empty: # no data, check again later, unless we had too many attempts attempts.pop() if attempts: QtCore.QTimer.singleShot(self._call_period, check) else: # finish without data, for external loop to do checks d.callback(None) else: # have some data, let's check if there's more all_data = [data] try: while True: all_data.append(self.get(block=False)) except Empty: # we're done! d.callback(all_data) QtCore.QTimer.singleShot(self._call_period, check) return d class AuthenticatedDownloader(BaseDownloader): """Episode downloader for Conectar site.""" def __init__(self): super(AuthenticatedDownloader, self).__init__() self._prev_progress = None self.browser_quit = Event() self.log("Inited") def _shutdown(self): """Quit the download.""" self.browser_quit.set() self.log("Shutdown finished") def _cancel(self): """Cancel a download.""" self.cancelled = True self.log("Cancelled") @defer.inline_callbacks def _download(self, canal, seccion, season, titulo, url, cb_progress): """Download an episode to disk.""" # levantamos el browser qinput = DeferredQueue() authuser = config.get('user', '') authpass = config.get('password', '') # build where to save it fname, tempf = self._setup_target(canal, seccion, season, titulo, ".avi") self.log("Downloading to temporal file %r", tempf) self.log("Download episode %r: browser started", url) brow = MiBrowser(self, authuser, authpass, url, tempf, qinput, self.browser_quit, self.log) brow.start() # loop reading until finished self._prev_progress = None self.log("Downloader started receiving bytes") while True: # get all data and just use the last item payload = yield qinput.deferred_get() if self.cancelled: self.log("Cancelled! Quit browser, wait, and clean.") self.browser_quit.set() yield qinput.deferred_get() self._clean(tempf) self.log("Cancelled!") raise CancelledError() # special situations if payload is None: # no data, let's try again continue data = payload[-1] if isinstance(data, Exception): raise data if data == DONE_TOKEN: break # actualizamos si hay algo nuevo if data != self._prev_progress: cb_progress(data) self._prev_progress = data # movemos al nombre correcto y terminamos self.log("Downloading done, renaming temp to %r", fname) os.rename(tempf, fname) self.deferred.callback(fname) class ConectarDownloader(AuthenticatedDownloader): """Episode downloader for Conectar site.""" service = 'conectate' class EncuentroDownloader(AuthenticatedDownloader): """Episode downloader for Conectar site.""" service = 'encuentro' class _GenericDownloader(BaseDownloader): """Episode downloader for a generic site that works with urllib2.""" headers = { 'User-Agent': 'Mozilla/5.0', 'Accept': '*/*', } manager = QtNetwork.QNetworkAccessManager() file_extension = None # to be overwritten by class child def __init__(self): super(_GenericDownloader, self).__init__() self._prev_progress = None self.internal_downloader_deferred = None self.log("Inited") def _shutdown(self): """Quit the download.""" self.log("Shutdown finished") def _cancel(self): """Cancel a download.""" if self.internal_downloader_deferred is not None: self.log("Cancelled") exc = CancelledError("Cancelled by user") self.internal_downloader_deferred.errback(exc) @defer.inline_callbacks def _download(self, canal, seccion, season, titulo, url, cb_progress): """Download an episode to disk.""" url = str(url) self.log("Download episode %r", url) # build where to save it fname, tempf = self._setup_target(canal, seccion, season, titulo, self.file_extension) self.log("Downloading to temporal file %r", tempf) fh = open(tempf, "wb") def report(dloaded, total): """Report download.""" if total == -1: m = "%d MB" % (dloaded // MB,) else: size_mb = total // MB perc = dloaded * 100.0 / total m = "%.1f%% (de %d MB)" % (perc, size_mb) if m != self._prev_progress: cb_progress(m) self._prev_progress = m def save(): """Save available bytes to disk.""" data = req.read(req.bytesAvailable()) fh.write(data) request = QtNetwork.QNetworkRequest() request.setUrl(QtCore.QUrl(url)) for hk, hv in self.headers.items(): request.setRawHeader(hk, hv) def end_ok(): """Finish Ok politely the deferred.""" if not self.internal_downloader_deferred.called: self.internal_downloader_deferred.callback(True) def end_fail(exc): """Finish in error politely the deferred.""" if not self.internal_downloader_deferred.called: self.internal_downloader_deferred.errback(exc) deferred = self.internal_downloader_deferred = defer.Deferred() req = self.manager.get(request) req.downloadProgress.connect(report) req.error.connect(end_fail) req.readyRead.connect(save) req.finished.connect(end_ok) try: yield deferred except Exception as err: self.log("Exception when waiting deferred: %s (request finished? %s)", err, req.isFinished()) raise finally: if not req.isFinished(): self.log("Aborting QNetworkReply") req.abort() fh.close() # rename to final name and end logger.info("Downloading done, renaming temp to %r", fname) os.rename(tempf, fname) self.deferred.callback(fname) class GenericVideoDownloader(_GenericDownloader): """Generic downloaded that saves video.""" file_extension = ".mp4" class GenericAudioDownloader(_GenericDownloader): """Generic downloaded that saves audio.""" file_extension = ".mp3" class ThreadedYT(Thread): def __init__(self, url, fname, output_queue, must_quit, log): self.url = url self.fname = fname self.output_queue = output_queue self.must_quit = must_quit self._prev_progress = None self.log = log super(ThreadedYT, self).__init__() def _really_download(self): """Effectively download the content to disk.""" self.log("Threaded YT, start") def report(info): """Report download.""" total = info['total_bytes'] dloaded = info['downloaded_bytes'] size_mb = total // MB perc = dloaded * 100.0 / total if self.must_quit.is_set(): # YoutubeDL can't be really cancelled, we raise something and then ignore it; # opened for this: https://github.com/rg3/youtube-dl/issues/8014 raise Finished() m = "%.1f%% (de %d MB)" % (perc, size_mb) if m != self._prev_progress: self.output_queue.put(m) self._prev_progress = m conf = { 'outtmpl': self.fname, 'progress_hooks': [report], 'quiet': True, 'logger': logger, } with youtube_dl.YoutubeDL(conf) as ydl: self.log("Threaded YT, about to download") ydl.download([self.url]) self.output_queue.put(DONE_TOKEN) self.log("Threaded YT, done") def run(self): """Do the heavy work.""" try: self._really_download() except Finished: # ignore this exception, it's only used to cut YoutubeDL pass except Exception as err: self.log("Threaded YT, error: %s(%s)", err.__class__.__name__, err) self.output_queue.put(err) class YoutubeDownloader(BaseDownloader): """Downloader for stuff in youtube.""" def __init__(self): super(YoutubeDownloader, self).__init__() self.thyts_quit = Event() self.log("Inited") def _shutdown(self): """Quit the download.""" self.thyts_quit.set() self.log("Shutdown finished") def _cancel(self): """Cancel a download.""" self.log("Cancelling") self.cancelled = True @defer.inline_callbacks def _download(self, canal, seccion, season, titulo, url, cb_progress): """Download an episode to disk.""" # start the threaded downloaded qinput = DeferredQueue() # build where to save it fname, tempf = self._setup_target(canal, seccion, season, titulo, ".mp4") self.log("Downloading to temporal file %r", tempf) self.log("Download episode %r: browser started", url) thyt = ThreadedYT(url, tempf, qinput, self.thyts_quit, self.log) thyt.start() # loop reading until finished while True: # get all data and just use the last item payload = yield qinput.deferred_get() if self.cancelled: self.log("Cancelled!") self.thyts_quit.set() raise CancelledError() # special situations if payload is None: # no data, let's try again continue data = payload[-1] if isinstance(data, Exception): raise data if data == DONE_TOKEN: break # normal cb_progress(data) # rename to proper name and finish self.log("Downloading done, renaming temp to %r", fname) os.rename(tempf, fname) self.deferred.callback(fname) class ChunksDownloader(BaseDownloader): """Download several chunks and merge them in one file.""" def __init__(self): super(ChunksDownloader, self).__init__() self.should_stop = False self.log("Inited") def _cancel(self): """Cancel a download.""" self.log("Cancelling") self.cancelled = True def _shutdown(self): self.log('Stopping') self.should_stop = True @defer.inline_callbacks def _download(self, canal, seccion, season, titulo, url, cb_progress): """Download an episode to disk.""" chunk_urls = json.loads(url) self.log("ChunksDownloader, download episode with %d chunks", len(chunk_urls)) # build where to save it fname, tempf = self._setup_target(canal, seccion, season, titulo, ".mpeg") self.log("Downloading to temporal file %r", tempf) fh = open(tempf, 'wb') for i, url in enumerate(chunk_urls, 1): if self.cancelled: self._clean(tempf) self.log("Cancelled!") raise CancelledError() if self.should_stop: self._clean(tempf) self.log("Stopped") return self.log("Downloading chunk %i of %i: %r", i, len(chunk_urls), url) content_type, file_data = yield utils.download(url) self.log("(got %d bytes)", len(file_data)) progress = i * 100 / len(chunk_urls) cb_progress('{} %'.format(progress)) fh.write(file_data) fh.close() # rename to final name and end self.log("Done! renaming temp to %r", fname) os.rename(tempf, fname) self.deferred.callback(fname) # this is the entry point to get the downloaders for each type all_downloaders = { 'encuentro': EncuentroDownloader, 'conectar': ConectarDownloader, 'generic': GenericVideoDownloader, 'dqsv': GenericAudioDownloader, 'youtube': YoutubeDownloader, 'chunks': ChunksDownloader, } if __name__ == "__main__": h = logging.StreamHandler() h.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG) logger.addHandler(h) def show(avance): """Show progress.""" print("Avance:", avance) # overwrite config for the test config = dict(user="lxpdvtnvrqdoa@mailinator.com", # NOQA password="descargas", downloaddir='.') app = QtCore.QCoreApplication(sys.argv) # several versions to test # downloader = EncuentroDownloader() # _url = "http://www.encuentro.gob.ar/sitios/encuentro/Programas/ver?rec_id=120761" # downloader = ConectarDownloader() # _url = "http://www.conectate.gob.ar/sitios/conectate/busqueda/pakapaka?rec_id=103605" # downloader = GenericVideoDownloader() # _url = "http://backend.bacua.gob.ar/video.php?v=_f9d06f72" # downloader = YoutubeDownloader() # _url = "http://www.youtube.com/v/mr0UwpSxXHA&fs=1" downloader = ChunksDownloader() _url = json.dumps([ 'http://186.33.226.132/vod/smil:content/videos/clips/38650.smil/media_w612292642_b1200000_0.ts', # NOQA 'http://186.33.226.132/vod/smil:content/videos/clips/38650.smil/media_w612292642_b1200000_1.ts', # NOQA 'http://186.33.226.132/vod/smil:content/videos/clips/38650.smil/media_w612292642_b1200000_2.ts', # NOQA 'http://186.33.226.132/vod/smil:content/videos/clips/38650.smil/media_w612292642_b1200000_3.ts', # NOQA ]) @defer.inline_callbacks def download(): """Download.""" logger.info("Starting test download") try: downloader.download("test-ej-canal", "secc", "temp", "tit", _url, show) fname = yield downloader.deferred logger.info("All done! %s", fname) except CancelledError: logger.info("--- cancelado!") finally: downloader.shutdown() app.exit() download() sys.exit(app.exec_()) encuentro-5.0/encuentro/logger.py0000644000175000017500000000450712637746176020324 0ustar facundofacundo00000000000000# Copyright 2011-2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import print_function """Logging set up.""" import logging import os import sys import traceback from logging.handlers import RotatingFileHandler import xdg.BaseDirectory class CustomRotatingFH(RotatingFileHandler): """Rotating handler that starts a new file for every run.""" def __init__(self, *args, **kwargs): RotatingFileHandler.__init__(self, *args, **kwargs) self.doRollover() def exception_handler(exc_type, exc_value, tb): """Handle an unhandled exception.""" exception = traceback.format_exception(exc_type, exc_value, tb) msg = "".join(exception) print(msg, file=sys.stderr) # log logger = logging.getLogger('encuentro') logger.error("Unhandled exception!\n%s", msg) def get_filename(): """Return the log file name.""" return os.path.join(xdg.BaseDirectory.xdg_cache_home, 'encuentro', 'encuentro.log') def set_up(verbose): """Set up the logging.""" logfile = get_filename() print("Saving logs to", repr(logfile)) logfolder = os.path.dirname(logfile) if not os.path.exists(logfolder): os.makedirs(logfolder) logger = logging.getLogger('encuentro') handler = CustomRotatingFH(logfile, maxBytes=1e6, backupCount=10) logger.addHandler(handler) formatter = logging.Formatter("%(asctime)s %(name)-22s %(levelname)-8s %(message)s") handler.setFormatter(formatter) logger.setLevel(logging.DEBUG) if verbose: handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) # hook the exception handler sys.excepthook = exception_handler encuentro-5.0/encuentro/multiplatform.py0000644000175000017500000000550312637746176021741 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2011-2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """Multiplatform code.""" import os import re import subprocess import sys import user _basedir = os.path.abspath(os.path.dirname(os.path.dirname( os.path.realpath(sys.argv[0])))) # if the base directory was determined by setup.py, fix it # pylint: disable=W0212 if hasattr(sys, '_INSTALLED_BASE_DIR'): _basedir = sys._INSTALLED_BASE_DIR # if the base directory was mangled by PyInstaller, fix it _frozen = False if hasattr(sys, 'frozen'): _basedir = sys._MEIPASS _frozen = True # pylint: enable=W0212 if sys.platform == 'win32': # won't find this in linux; pylint: disable=F0401 from win32com.shell import shell, shellcon config_dir = shell.SHGetFolderPath(0, shellcon.CSIDL_PROFILE, None, 0) data_dir = shell.SHGetFolderPath(0, shellcon.CSIDL_LOCAL_APPDATA, None, 0) cache_dir = data_dir del shell, shellcon else: from xdg import BaseDirectory config_dir = BaseDirectory.xdg_config_home data_dir = BaseDirectory.xdg_data_home cache_dir = BaseDirectory.xdg_cache_home del BaseDirectory def get_path(path): """Build an usable path for media.""" parts = path.split("/") # if frozen by PyInstaller, all stuff is in the same dir if _frozen: return os.path.join(_basedir, parts[-1]) # normal work return os.path.join(_basedir, *parts) def sanitize(name): """Sanitize the name according to the OS.""" if sys.platform == 'win32': sanit = re.sub('[<>:"/|?*]', '', name) else: sanit = re.sub('/', '', name) return sanit def get_download_dir(): """Get a the download dir for the system. I hope this someday will be included in the xdg library :| """ try: cmd = ["xdg-user-dir", 'DOWNLOAD'] proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) base = proc.communicate()[0].strip() except OSError: base = user.home return os.path.join(base, 'encuentro') def open_file(fullpath): """Open the file.""" if sys.platform == 'win32': os.startfile(fullpath) else: subprocess.call(["/usr/bin/xdg-open", fullpath]) encuentro-5.0/encuentro/logos/0000775000175000017500000000000013144662314017573 5ustar facundofacundo00000000000000encuentro-5.0/encuentro/logos/icon-192.png0000664000175000017500000003774413143111137021550 0ustar facundofacundo00000000000000PNG  IHDRRlsBIT|d pHYs%%IR$tEXtSoftwarewww.inkscape.org< IDATx}w|swi%W[mlcNE&&^J^ @B ŖdJئʖmޥJ]~jy~?>:̽s.Ay;Y6uMLBiQJG Aͷw}"0E^`- xV>C|p!d!'MVX'⃨IO )Ȟ3y{_!' mƒ u %E a"Dx! (. `AqPd%Ocڱ0#"`(*x2}8wx&q@SH|SD*BlJ<cP*!Q`3Y`20>V=A|{IA7 \M{/ʓ4-yEHHO)064Pw{g`i4mY>@!j# eKFHT%y .,1mgO ܼn7"Dx/"x'j3$ŋ Mq#PȀG168k|Lms| _(6Toױ-Ezk#@[MGtȔ"@q7!Cu -`N1bҍ%刈 9 (([{^ӄI=Mb3^Rۓʊ^>D\&MECi-2Ϥt.¡_j"2. ݶ /EDžfM0O| lIby=qv}~c)~r:\8Jxj<.\-f+ZsoM?vF"IyEէ~Yh냦 I|j#L00MmF_C'lV^[}Isxn3".`MuBF{\oGDG"kz66:eMAr.B!!=dD&tl J$1񲸙&ѕK-~HQ޴,oÕ]L#7Y {"*KʱHI"2RVT_/  KaٜHLKt/% [\qu QpR8r .zZzԓ|ڕeț7ܺY3 !~H P$7^a/x`~' &!CW.~BIW iW@XpH/9#P"cVr @CRFYoH%-C(H$LP e|UFGr̽a9Rr3d(VTT^ͣs Qyo3@ *!WGK9o,~&T:1D10_R-!-[o\j"f]\8%$RTTW+7]췀s 06@^ %kB0$n]ڍH.˪(JPj=\_b-qn@&-Lv]Wp} = V"HW"aZO9%dE"iRzSqn@/I/ xKZF{07P@*BtETPYb}3 )"Dľ B^  hӾ"%wݲ !LʉņʹsX&oQ^$5-AP$.ZQ)'4r͟y7(D{nDTP-_|z drWq(Y1.C\z2x`}3Mt9D*F'tX] +·q_@bg̼٘R]I|HR ^yEէusxb"@q,0dKi| #/ ojYQZQpƫ̾2h=rHQEEuuĆw 2#@T(sޯ0z^eh_$ٌV5ceT97DGeklR\_btDAH1!5шQf3ݟ6'(q\wnqъ^6w{$q)[W!1/s X|uyU xեA۱;a01$ GH*ܛW#g 9 eOLKwaZ KNV >D=j0ޣ6,xuPH)2ETb; |+K1sn^29A(Pk2G&%LY M3_0$ࣦKf_az&MFuYtrHHr/GZrB(N@v"0=o啗0k}:5T8-3_OJ Et$ˑ!CNЖv!#t&@gxl{ZJF՝'T&Ecs+sE(z :䬫ϧoD1W'MPRSzLmMO -+7SdN1B Lrq!.5 3ZW,F_ 9,:hq4C(-} P ڬVɥ)So]#Jc`Y?|IS0P|>CN$es>@{?07a7$h4t}ni|[OR44A;5} I9}J-.<ү5#>f vُ6(~aM\ͼ!pQbN1sؔ#A;mQ̖H$H+.GּwiQl$:p#`\VEA${*0 |ȁFniۺFxBob_3g4 yA*aZY!f߼e|rAÍ @ņKC FDH_siUZ,V9Ŋh U;Cb>K |2YJQ~ e pՋ9A|{$C /8)PokP)z}DtblHQDG!wQ9f]r':#($NbEc8CZLg:Nbnv0qFiηJq(lJ^X#A ,F! u eC#h9j2 gIrXr˕?uy&1~bL_Htበ pYqʅPۇP@ {b䦔/\xUx%4O)(f /g"v riWvjp b((N|~ qw#7_vn6-J3ʲyYXE8TlV+p! $ڀo]L& ׁg^?$h9kܸЪ@tx_>ZkB@${٘uJdΝB?`r mrBBŹ:3.7@AN=bƅO8LJuK { =;vIeR`-9Wtq d 5F4HכBI`OSFnȍ O|ZJ` MpcNli"̼i .zABA)EokoPoAEՆcba lc)Y>?aw#7 愜̹30H)CNhĒ<9$v#eƅ`25椻U.t y AhoCNr^w8lBp Z4H38qgJz[z0evnDF!*>k1.d yy YWTB,/ 6C]|!8䄔\̷ڧΘ֍pjqFn\X >g9ȫ?f۩V~wr!%+ Wv} ._ b4cgݵMhsߍÕa59I]Dl̼rL;R q1B~t)hUG`k@APP $0Opt~ܸ ArVa; 8P }j;zl/#P".=e LOcH)f v{q!GFH7T?m.9/l$348aa\uzgq1IP%AHWE0Κ~ovPkSvQ(X2nYbo {.XQZT-GB/b\bsL#92rl$>8ΨT|?_FuI`s԰rH<\DVy1Ҋĭ.)/="cw< {aN8rQCʊ} a!@jID`dp9Y?@Dqb~Fn\q* VJVP3G1=ڏC=,R4[/%9:<^!^;n@U3qE>$9xZ =0oХ"\^Q6jq?BС!"@jE&K$7p$ *.ʽ]3D_$0My͝h#-^ܳif Gbt?eF9MaZbT)~U FFo4H|pFdTfA"֪x^l6l Fn\0Y v:v?,$(XP^i$Vi}{={}u/Đӧd!,HRuTH@| leZsr}k2b{l|zmr M ) ɯNbT?{w$7)IF`RsTp>//×??\QuAcH$rbnucp0yVHd.,Gɵ!^CΆU| Bx p'@˩V:. ȍ _2!s՜zѳФ΀;ޮwevRcc*vF(ig?8CK`$`ePz1+t.QRlLP Ů0~$郌%:<{KTCzw2Uv[?xP^ő_bBg! 2)rTeP9Pem%4.:-W.Ee _Pz=gFX 2NVC0Mpp'cewfƅO%T{s_IH cC#+&J(^X߄,;:!-DGpQv+/ 8W^>c @\ .G9GuI' J6jcC8&܂0r߸8DL?|'ɖW?ʼnl@)oG?GV hq9$RgyB x!2yyH2162G )*_=0oNŹBݡ v3!:p#7.L%Ș L\#:|5F3"d6$nx(ʙ:]^ӎ ȣ,@$UHY`䍥>繞s5.Fnnƅ=71M?|AQfp쓯a4A.x:.G?ȇ2v f'a( |oL լ]ѪpP'\V8ըr> :#7.8JħJ4aG?>ӄ2)߮7f5'uI1Λ[(W;k uyݩkeL/_ye*l blD#vu%gWmV|zFG$oeJ HQA 7 G اa/U49j^G(L+/m %A.$V1VװEрJ r9)xV,+ӱ2 Ҳ lԆ=GaL~{k 6\2ȫNFES^"Mș @г!.Si gq<:OM-ŁTzjAk]ޕ# M*"%YTZ^-8] cz3ƪ3"& v?a>i X1,$O ~+uX 0DpJCeƅm)ʦ `!gZק1^_0nlݿh|sf@&}9S$ Ơ*/"63'd'˖ W՘H;/8@JSقb 8U,?xx61O];x g0?[BO'a$n`ᩏrqS :ڃ+l^3rTv?\;x{](Vka+v[YFl\և0:!C4u:tl%x&Ԥ(u\х iRp3Zfʙ2~@=硦I~t: Cҧojˆjsd=pMa #|QLn6!&NacCBw/Ç]NH.e` <c#ɐ<Ť~A Gi҈w|_^ہVu7xkfT9GG=?|d^cp668]b$ /qTeolCwtr(~b~i3|}?糛O/]<~};Cp\3ĮFFSQ͚'#@a!_u3[d̆XQ ض|oZgw85:С)vEbuGG/x8yXi}paq>NH:,VoݡyNW'?>?eB16)aC*u8AznϿ Z{p` ooSJAf əPFE ?! AT BQfа:5)XŻOǜQt\9e5|0'1-JH&o:s VqVSC2-hd>^[k?u*]a1L48v%s2'N8-X[UI)wsyL\ˎ/|@^$w,`U'1QD)Isv{+@,ZkB0$x vqЃw8pa}0(W"{|?$#Iw=6O`a̮鹾L} ޠN`YzS( iו$Xg?&zx'e]P#5ƄkE/إPc:btQz NiV q<"^SJvW\"LiBrYplfDg  8O |-cxr ffo*;ՙf ^fJ#6CenlÍGnoפ:{ogo"`{I0.Ozkaҳ.~Vo7u,APcP@7~trnzw- PJϘ)OLb,R9`HL7BB>>>Ơn S? U3FpGbS1|ZHgDo$aԶ&Byk9E>4lctI␹_{" e[=U_e.%$cj|ڸZB?c"c?˭R0 x c]Z(`}+kZ2XžU>m/(8%}fWK1g,37+hV!D¼li>ѭl$xcQLLqY?3op3Ta5η*rN9SJ0H%< ߂?uMӽzWϼ|=šNX-Vnũm| ?(5oY"w)\I;o?aBTMuF{+WL"@IRo^?$pMJYkwV3Ta  2?!<Z`IqȞ>u$ˣ.כ 1P E:X>7&)~iܼq'Bԍm(\QZ8]Y趫dKAos[!føf 0 @ퟣ`-piȂ r̅B\]%\|Z>=Jq3OU!%+&8' yYհ,]'uԣЩGWa|D Q?Js`v >57`|Y L!Ze6K~k}<qY6]YA;vQJpOU1MH$gG_atڵLnh QGBdG~~|e#xȲy| #ܱpZ rrY\` |bFQ8qo bBIQfXN3 z Q$d`,~OUӀ.v3foS x´yV~=pS0iTZQ%{a6v8白|Т)(SL?KeQQFU0w+#P su8pFDrħOb6LD[@m|$vP"L^ֵ!|`Hۛӭhĭ/G?>k|37J!I1J$z5$[vMIǠh16vhl8estXR1P9{YCnUq6A*zmfjACs 3;9PWxa> hx\Śg?O xE8: y#C;V5V=$.=Έe:,)ü,=gv-VebӶ9s/H]I D=N)czhcAglfW(=vǛEvfOsni(t=EGDZzLzd H!p@$R +w6m8Ǻbw0kڸxuQ륰X J,j!d#xLK\o%0 ƆatP3G (yl=k17K 'hý%h`+"\!{y67Ffc D",sj@B;<ha1pLeaIk&dx\L')KKgAaBvh0ϳ;Am++ 9 ?g{Tou;a Z`Hp?*tFm0C38aǠ㞷'UK ƘSyr!wv*?Zi{3#cP)҂,-Ò-2,V쟆>sKkвlkF[ o]8(jPM%32o{bpOe172)#Qb-1+3ULƺCadx|i R jyarayA'oMGmo4v7Ňu_lk1SJDt$c13! QCE:X7R rZ] `4a{PB6bB H !RkODmotg`g=yML&VYMLRl޼%e] |oIbluzH<8D"T&`G x֤T6{\:  9L(#XHr8鈼&. /$"S庇uos"8Dp8^ L#񸷪S`X<ϙRRDSxiv(6j@}C$@ !CgYom2F'dH~: ?.v'$<2y, sQ?f)>h㶗f :Tv(C$@Q2`^6s^4<yHO/[.~AI d!.?t@!ms8-7Iܜۆ#qK m鐢0 P!~Bf0P`_3Xb%ly!¯ųF_cNqFk!i&u&:5&Bk2b{lrùjvB$@AG!^Zu.:+"}hrCنH0`Z<S7R`'-N_"! 8G?ǞawHaKež0wMDQՎ<{<O/R3D%$~vJ¦MG0 "A?|Ĥſ&yڹ[מDA$$L<3c5'\ f+W;r1U/@` 1I,b⌋8+ 6 w<~XVu" )>a wD՘/ĉ^>C ҸMA$򱯅͎r~C$GyxƷ-kwc"Š#'Btu q1ԅCz9; ٛ ']_Orv."p\t׽ӜC@SƑ\'\"Jmv!,2LVm`5Zغ_q0I&FT9ൃ9|nl];8С <7PJNM ؑH>E Dirs,uB:v{|8ův]58507t*Q?stÝx|n/m>lټvL9 VP{ߩI/9fQش-uH!qڏ94Xf+ zt:3Q%~\UK~9Ci\ E;\\ykqY_x{q94/~1C >J9net9qSI'("paƜy&Q?weA;k_b Ϛ7g sM$ZeI7{~"&+gOrmb)"xYQQ@[ڃWv''lrX_EC ڞRD_j_NSx|b}7AΈy(Ԡ1wF@':+U΂D*@/ u\0DaDGo|Q'< ғ%TCX^|d|(~ U)O)KryÛ]c# 'mW*ׂ5Σq5j.zh#L#t@-Ӭ̱%!$R dDBlֵF!'?GF$#IENDB`encuentro-5.0/encuentro/logos/icon-32.png0000664000175000017500000000346113143111074021446 0ustar facundofacundo00000000000000PNG  IHDR szzsBIT|d pHYsItEXtSoftwarewww.inkscape.org<IDATXklTsݵ~5m]R6-m11Mm JFMEOU"0b( PRJ0Xo>k/^Hq933w̱j㪮n0-U4ˡ+pM$nӡ~悲dgJC>GF[CZcO$8݁WaR|OD"Ƴ-':r[iKoLVX$HNz̰%}a Sz)i ,{*=elT-.$yzY2'u 8+'F@QfISϯTqQkONcM#(he<ۛJάB{ sqXVv׋δxq8!Pz[Pv ٝFJW^eYݕQQrǬ,`tpE@nrbNu"{4"6ɤ䱅އ6zgM}vߒ4aBL;?{ &ȾKԥf\>Z8E2rDEY%UROV( D?ᐂiFy-Vm pz 2EUGicؼET\Twim4MSwhQ )r+cdfaB5CAAfzuDbl:q2Η)Ufh6MX+ʺdݻ3Kt3 @o' o+BR۱5"%> g8'$ NLKPZ{Ao $@O?a[.ݴm>0ͻJQh,|F<ք'P0,smP +0tU겋7={tuv0ݥXW3b9zLu g yD8>[>Ɔ=/-@-*ǒϏ EȰ vPxH9z,?#9l miwR/KڨRHvx,"LJ] /Y\ nD5?/6mH⢡{tt]M^1ámOf9'p jϺG}a*zg& ͢~u;)xp>?.t[~Sj XyKn `II?wr:ۡ"WY= Vy|הRS@U7|3i)x`f"QҖ6Wuޮp @H4/`I'SyiO)QOBjUpkU˝ ~WKouu >u\}V(^ 呭՟-q)0~'GTk(FlCAQcWqIu~갉? uM/+v+q?¢gw(?IENDB`encuentro-5.0/encuentro/config.py0000664000175000017500000000554612407033666020304 0ustar facundofacundo00000000000000# -*- coding: UTF-8 -*- # Copyright 2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """The system configuration.""" import logging import os import pickle from encuentro import utils logger = logging.getLogger('encuentro.config') # these are the configuration variables/parameters that on one hand should # be stored in a keyring (if available), and on the other hand must not be # logged by the system SECURITY_CONFIG = ['user', 'password'] class _Config(dict): """The configuration.""" SYSTEM = 'system' def __init__(self): self._fname = None super(_Config, self).__init__() def sanitized_config(self): """Return a copied config, sanitized to log.""" safecfg = self.copy() for secure in SECURITY_CONFIG: if secure in safecfg: safecfg[secure] = '' return safecfg def init(self, fname): """Initialize and load config.""" self._fname = fname if not os.path.exists(fname): # default to an almost empty dict self[self.SYSTEM] = {} logger.debug("File not found, starting empty") return with open(fname, 'rb') as fh: saved_dict = pickle.load(fh) logger.debug("Loaded: %s", self.sanitized_config()) self.update(saved_dict) # for compatibility, put the system container if not there if self.SYSTEM not in self: self[self.SYSTEM] = {} def save(self): """Save the config to disk.""" # we don't want to pickle this class, but the dict itself raw_dict = self.copy() logger.debug("Saving: %s", self.sanitized_config()) with utils.SafeSaver(self._fname) as fh: pickle.dump(raw_dict, fh) class _Signal(object): """Custom signals. Decorate a function to be called when signal is emitted. """ def __init__(self): self.store = {} def register(self, method): """Register a method.""" self.store.setdefault(method.__name__, []).append(method) def emit(self, name): """Call the registered methods.""" meths = self.store.get(name, []) for meth in meths: meth() config = _Config() signal = _Signal() encuentro-5.0/encuentro/notify.py0000664000175000017500000000436412640746146020347 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals, print_function """The notification-to-the-desktop subsystem.""" import logging from encuentro.config import config _ERRMSG = """ ERROR! Problema al importar 'pynotify' - No es "estrictamente necesario, pero si lo instala tendrá algunas notificaciones en el escritorio. """ logger = logging.getLogger('encuentro.notification') class _Notifier(object): """A notifier that defers the import as much as possible. This is because importing 'pynotify' while PyQt is still starting causes everything to segfault. """ def __init__(self): self._inited = False def _init(self): """Initialize everything.""" self._inited = True try: import pynotify except ImportError: print(_ERRMSG) self._notify = lambda t, m: None else: pynotify.init("Encuentro") def _f(title, message): """The method that will really notify.""" if config.get('notification', True): try: n = pynotify.Notification(title, message) n.show() except Exception as err: logger.warning("Unable to notify! %s(%s) (imported is %r)", err.__class__.__name__, err, pynotify) self._notify = _f def __call__(self, title, message): if not self._inited: self._init() self._notify(title, message) notify = _Notifier() encuentro-5.0/encuentro/image.py0000664000175000017500000000543312400625227020105 0ustar facundofacundo00000000000000# Copyright 2013-2014 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Get an image from web and cache it.""" import logging import md5 import os import glob from encuentro import multiplatform, utils logger = logging.getLogger('encuentro.image') class ImageGetter(object): """Image downloader and cache object.""" def __init__(self, callback): self.callback = callback self.cache_dir = os.path.join(multiplatform.cache_dir, 'encuentro.images') if not os.path.exists(self.cache_dir): os.makedirs(self.cache_dir) def get_image(self, episode_id, url): """Get an image and show it using the callback.""" logger.info("Loading image for episode %s: %r", episode_id, url) file_name = md5.md5(url).hexdigest() file_fullname = os.path.join(self.cache_dir, file_name) img_search_result = glob.glob(file_fullname + '.*') if len(img_search_result) > 0: logger.debug("Image already available: %r", file_fullname) self.callback(episode_id, file_fullname) return def _d_callback(data, episode_id, file_fullname): """Cache the image and use the callback.""" content_type, img_data = data content, extension = content_type.split('/') if content != 'image': logger.debug("The Content-Type header is not 'image'") file_fullname = file_fullname + '.' + extension logger.debug("Image downloaded for episode_id %s, " "saving to %r, Content-Type= %s", episode_id, file_fullname, content_type) with utils.SafeSaver(file_fullname) as fh: fh.write(img_data) self.callback(episode_id, file_fullname) def _d_errback(failure): """Log the problem.""" logger.error("Problem getting image: type: %s error: %s", failure.type, failure.value) logger.debug("Need to download the image") d = utils.download(url) d.add_callback(_d_callback, episode_id, file_fullname) d.add_errback(_d_errback) encuentro-5.0/encuentro/ui/0000775000175000017500000000000013144662314017065 5ustar facundofacundo00000000000000encuentro-5.0/encuentro/ui/__init__.py0000664000175000017500000000136412133130037021167 0ustar facundofacundo00000000000000# Copyright 2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """All classes to define the User Interface of the program.""" encuentro-5.0/encuentro/ui/central_panel.py0000664000175000017500000005640512641045115022253 0ustar facundofacundo00000000000000# -*- coding: UTF-8 -*- # Copyright 2013-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """Central panels in the main window, the content part of all the interface.""" import logging import operator from PyQt4.QtGui import ( QAbstractItemView, QAbstractTextDocumentLayout, QApplication, QBrush, QColor, QHBoxLayout, QImage, QLabel, QMenu, QPixmap, QPushButton, QStyle, QStyleOptionViewItemV4, QStyledItemDelegate, QTextDocument, QTextEdit, QTreeWidgetItem, QVBoxLayout, QWidget, ) from PyQt4.QtCore import Qt, QSize, QAbstractTableModel from encuentro import data, image from encuentro.config import config, signal from encuentro.data import Status from encuentro.ui import remembering from encuentro.ui.throbber import Throbber logger = logging.getLogger("encuentro.centralpanel") class DownloadsWidget(remembering.RememberingTreeWidget): """The downloads queue.""" def __init__(self, episodes_widget): self.episodes_widget = episodes_widget super(DownloadsWidget, self).__init__('downloads') signal.register(self.save_state) _headers = ("Descargando...", "Estado") self.setColumnCount(len(_headers)) self.setHeaderLabels(_headers) self.queue = [] self.current = -1 self.downloading = False # connect the signals self.clicked.connect(self.on_signal_clicked) def on_signal_clicked(self, _): """The view was clicked.""" item = self.currentItem() self.episodes_widget.show_episode(item.episode_id) # adjust the info of episode, as going through EpisodesWidget may # be a dead end (because of filtering) episode = self.episodes_widget.main_window.programs_data[item.episode_id] self.episodes_widget.episode_info.update(episode) self.episodes_widget.main_window.check_download_play_buttons() def append(self, episode): """Append an episode to the downloads list.""" # add to the list in the GUI item = QTreeWidgetItem((episode.composed_title, "Encolado")) item.episode_id = episode.episode_id self.queue.append((episode, item)) self.addTopLevelItem(item) self.setCurrentItem(item) # fix episode state episode.state = Status.waiting def prepare(self): """Set up everything for next download.""" self.downloading = True self.current += 1 episode, _ = self.queue[self.current] episode.state = Status.downloading return episode def start(self): """Download started.""" episode, item = self.queue[self.current] item.setText(1, "Comenzando") episode.state = Status.downloading def progress(self, progress): """Advance the progress indicator.""" _, item = self.queue[self.current] item.setText(1, "Descargando: %s" % progress) def end(self, error=None): """Mark episode as downloaded.""" episode, item = self.queue[self.current] if error is None: # downloaded OK gui_msg = "Terminado ok" end_state = Status.downloaded else: # something bad happened gui_msg = str(error).decode("utf8") end_state = Status.none item.setText(1, gui_msg) item.setDisabled(True) episode.state = end_state self.episodes_widget.refresh(episode.episode_id) self.downloading = False def cancel(self): """The download is being cancelled.""" episode, item = self.queue[self.current] item.setText(1, "Cancelado") episode.state = Status.none def unqueue(self, episode): """Remove the indicated episode from the queue.""" episode.state = Status.none # search for the item, adjust the queue and remove it from the widget for pos, (queued_episode, item) in enumerate(self.queue): if queued_episode.episode_id == episode.episode_id: break else: raise ValueError( "Couldn't find episode to unqueue: " + str(episode)) del self.queue[pos] self.takeTopLevelItem(pos) # as we removed an item, the cursor goes to other (if any), fix the rest of the interface item = self.currentItem() if item is not None: self.episodes_widget.show_episode(item.episode_id) def pending(self): """Return the pending downloads quantity (including current).""" # remaining after current one q = len(self.queue) - self.current - 1 # if we're still downloading current one, add it to the count if self.downloading: q += 1 return q def save_state(self): """Save state for pending downloads.""" p = self.pending() if p > 0: pending_ids = [e.episode_id for e, _ in self.queue[-p:]] else: pending_ids = [] config[config.SYSTEM]['pending_ids'] = pending_ids def load_pending(self): """Queue the pending downloads.""" loaded_pending_ids = config[config.SYSTEM].get('pending_ids', []) main_window = self.episodes_widget.main_window for episode_id in loaded_pending_ids: try: episode = main_window.programs_data[episode_id] except KeyError: logger.debug("Tried to load pending %r, didn't find it", episode_id) else: logger.info("Queuing pending episode %s", episode) main_window.queue_download(episode) class HTMLDelegate(QStyledItemDelegate): """Custom delegate so the QTreeWidget can do HTML. This is an adaptation of a post here: http://stackoverflow.com/questions/10924175/how-do-i-use-a- qstyleditemdelegate-to-paint-only-the-background-without-coverin We only need to do background highlighting, so probably this will be trimmed as much as possible for performance reasons. Also, we only do HTML for one column, the rest is delegated to parent. """ def __init__(self, parent, html_column): self._html_column = html_column QStyledItemDelegate.__init__(self, parent) def paint(self, painter, option, index): """Render the delegate for the item.""" if index.column() != self._html_column: return QStyledItemDelegate.paint(self, painter, option, index) options = QStyleOptionViewItemV4(option) self.initStyleOption(options, index) if options.widget is None: style = QApplication.style() else: style = options.widget.style() doc = QTextDocument() doc.setHtml(options.text) options.text = "" style.drawControl(QStyle.CE_ItemViewItem, options, painter) ctx = QAbstractTextDocumentLayout.PaintContext() textRect = style.subElementRect(QStyle.SE_ItemViewItemText, options) painter.save() painter.translate(textRect.topLeft()) painter.setClipRect(textRect.translated(-textRect.topLeft())) doc.documentLayout().draw(painter, ctx) painter.restore() def sizeHint(self, option, index): """Calculate the needed size.""" options = QStyleOptionViewItemV4(option) self.initStyleOption(options, index) doc = QTextDocument() doc.setHtml(options.text) doc.setTextWidth(options.rect.width()) return QSize(doc.idealWidth(), doc.size().height()) class EpisodesWidgetModel(QAbstractTableModel): """The model for the episodes widget.""" _col_getters = [ operator.attrgetter('channel'), operator.attrgetter('section'), operator.attrgetter('filtered_title'), operator.attrgetter('duration'), ] _headers = ("Canal", "Sección", "Título", "Duración [min]") def __init__(self, main_window): super(EpisodesWidgetModel, self).__init__() self.main_window = main_window self._order_column = self._order_direction = 0 self._filter_text = '' self._filter_only_downloaded = False self.episodes, self.pos_map = self._load_episodes() def _load_episodes(self): """Fill episodes own data.""" # prepare sorting parameters is_reversed = self._order_direction == Qt.DescendingOrder order_key = self._col_getters[self._order_column] # get all episodes, apply filters text = data.prepare_to_filter(self._filter_text) episodes = [] for ep in self.main_window.programs_data.values(): params = ep.filter_params(text, self._filter_only_downloaded) if params is None: # filtered out continue pos1, pos2 = params if pos1 == pos2: # no highlighting ep.filtered_title = ep.composed_title else: # filtering by text, so highlight t = ep.composed_title ep.filtered_title = '%s%s%s' % ( t[:pos1], t[pos1:pos2], t[pos2:]) episodes.append(ep) # episodes si the data to show in the table, with some extra columns (hidden), # pos_map is a mapping to know in which position an episode is from its id episodes = sorted(episodes, key=order_key, reverse=is_reversed) pos_map = {ep.episode_id: i for i, ep in enumerate(episodes)} return episodes, pos_map def reload_episodes(self): """Reload all episodes.""" self.layoutAboutToBeChanged.emit() self.episodes, self.pos_map = self._load_episodes() self.layoutChanged.emit() def rowCount(self, parent): """Row count.""" return len(self.episodes) def columnCount(self, parent): """Column count.""" return len(self._headers) def data(self, index, role): """Return content text and format.""" if role == Qt.DisplayRole: row = index.row() col = index.column() ep = self.episodes[row] data = self._col_getters[col](ep) return data if role == Qt.TextAlignmentRole: col = index.column() if col == 3: return Qt.AlignRight if role == Qt.BackgroundRole: row = index.row() ep = self.episodes[row] if ep.state == Status.downloaded: bground = QBrush(QColor("light green")) return bground def headerData(self, section, orientation, role): """Return the headers.""" if role == Qt.DisplayRole and orientation == Qt.Horizontal: return self._headers[section] def refresh(self, episode_id): """Refresh the view of an episode (if possible, it may be filtered out).""" row = self.pos_map.get(episode_id) if row is not None: index_from = self.index(row, 0) index_to = self.index(row, len(self._headers) - 1) self.dataChanged.emit(index_from, index_to) def sort(self, n_col, order): """Sort data by given column.""" self._order_column = n_col self._order_direction = order self.reload_episodes() def set_filter(self, text, only_downloaded): """Apply a filter to the episodes list.""" self._filter_text = text self._filter_only_downloaded = only_downloaded self.reload_episodes() class EpisodesWidgetView(remembering.RememberingTableView): """The list of episodes info.""" _title_column = 2 def __init__(self, main_window, episode_info): self.main_window = main_window self.episode_info = episode_info super(EpisodesWidgetView, self).__init__('episodes') self._model = EpisodesWidgetModel(main_window) self.setModel(self._model) self.setMinimumSize(600, 300) self.setItemDelegate(HTMLDelegate(self, self._title_column)) # hide the vertical header at the left of the table and configure top header self.verticalHeader().hide() header = self.horizontalHeader() header.setStretchLastSection(False) header.setResizeMode(2, header.Stretch) header.sortIndicatorChanged.connect(self._model.sort) # other behaviour configs self.setSelectionBehavior(QAbstractItemView.SelectRows) # whole row selected instead cell self.setSelectionMode(QAbstractItemView.ExtendedSelection) # full fledged selection self.setSortingEnabled(True) # enable sorting # connect the signals self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.on_right_button) self.doubleClicked.connect(self.on_activate) self.activated.connect(self.on_activate) sm = self.selectionModel() sm.selectionChanged.connect(self.on_change) def show_episode(self, episode_id): """Show the row for the requested episode, if possible (it may be filtered out).""" row = self._model.pos_map.get(episode_id) if row is not None: index = self._model.index(row, 0) self.scrollTo(index) self._adjust_gui(episode_id) def selected_items(self): """Return the episode ids of the selected items.""" sm = self.selectionModel() indexes = sm.selectedRows() return [self._model.episodes[ind.row()].episode_id for ind in indexes] def on_change(self, delta_selection_pos, delta_selection_neg): """The view was clicked.""" sm = self.selectionModel() indexes = sm.selectedRows() for idx in indexes: row = idx.row() if len(indexes) == 1: row = indexes[0].row() episode_id = self._model.episodes[row].episode_id self._adjust_gui(episode_id) elif len(indexes) == 0: # nothing selected self.episode_info.clear("Seleccioná un programa para ver aquí la info.") self.main_window.check_download_play_buttons() else: # multiple selection self.episode_info.clear("Seleccioná sólo un programa para ver aquí la info.") self.main_window.check_download_play_buttons() def on_activate(self, index): """Double click and enter on a row.""" row = index.row() episode_id = self._model.episodes[row].episode_id episode = self.main_window.programs_data[episode_id] logger.debug("Doubleclick/Enter in %s", episode) if episode.state == Status.downloaded: self.main_window.play_episode(episode) elif episode.state == Status.none: if self.main_window.have_config(): self.main_window.queue_download(episode) else: logger.debug("Not starting download because no config.") t = "No se puede arrancar una descarga porque la configuración está incompleta." self.main_window.show_message('Falta configuración', t) def _adjust_gui(self, episode_id): """Adjust the rest of the GUI for this episode.""" episode = self.main_window.programs_data[episode_id] logger.debug("Showing episode: %s", episode) # adjust the rest of the GUI self.episode_info.update(episode) self.main_window.check_download_play_buttons() # make the view to select this episode row = self._model.pos_map[episode_id] self.selectRow(row) def on_right_button(self, point): """Right button was pressed, build a menu.""" index = self.indexAt(point) row = index.row() episode_id = self._model.episodes[row].episode_id self._adjust_gui(episode_id) episode = self.main_window.programs_data[episode_id] menu = QMenu() mw = self.main_window act_play = menu.addAction("&Reproducir", lambda: mw.play_episode(episode)) act_cancel = menu.addAction("&Cancelar descarga", lambda: mw.cancel_download(episode)) act_download = menu.addAction("&Descargar", lambda: mw.queue_download(episode)) # set menu options according status state = episode.state if state == Status.downloaded: act_play.setEnabled(True) act_cancel.setEnabled(False) act_download.setEnabled(False) elif state == Status.downloading or state == Status.waiting: act_play.setEnabled(False) act_cancel.setEnabled(True) act_download.setEnabled(False) elif state == Status.none: act_play.setEnabled(False) act_cancel.setEnabled(False) if self.main_window.have_config(): act_download.setEnabled(True) else: act_download.setEnabled(False) menu.exec_(self.viewport().mapToGlobal(point)) def set_filter(self, text, only_downloaded=False): """Apply a filter to the episodes list (just a proxy to the model).""" self._model.set_filter(text, only_downloaded) def refresh(self, episode_id): """Refresh the indicated episode in the view (just a proxy to the model).""" self._model.refresh(episode_id) def reload_episodes(self): """Reload all the episodes (just a proxy to the model).""" self._model.reload_episodes() class EpisodeInfo(QWidget): """Show the episode at the right.""" def __init__(self, main_window): self.main_window = main_window super(EpisodeInfo, self).__init__() self.current_episode = None layout = QVBoxLayout(self) # a throbber, that we don't initially show self.throbber = Throbber() layout.addWidget(self.throbber) self.throbber.hide() # the image and its getter self.image_episode = QLabel() self.image_episode.hide() layout.addWidget(self.image_episode, alignment=Qt.AlignCenter) self.get_image = image.ImageGetter(self.image_episode_loaded).get_image # text area self.text_edit = QTextEdit("Seleccionar un programa para ver aquí la info.") self.text_edit.setReadOnly(True) layout.addWidget(self.text_edit) # the button self.button = QPushButton() self.button.connected = False self.button.hide() layout.addWidget(self.button) def image_episode_loaded(self, episode_id, image_path): """An image has arrived, show it only if the path is correct.""" # only set the image if the user still have the same episode selected if self.current_episode != episode_id: return # load the image and show it pixmap = QPixmap(image_path) self.image_episode.setPixmap(pixmap) self.image_episode.show() # hide the throbber self.throbber.hide() def clear(self, msg): """Clear the episode info panel.""" self.throbber.hide() self.image_episode.hide() self.text_edit.setText(msg) self.button.hide() def update(self, episode, force_change=True): """Update all the episode info.""" if not force_change: # if not forced, only update what is being shown if the current episode # is the one to be updated if self.current_episode != episode.episode_id: return self.current_episode = episode.episode_id # image if episode.image_data is not None: # have the image data already!! qimg = QImage.fromData(episode.image_data) pixmap = QPixmap.fromImage(qimg) self.image_episode.setPixmap(pixmap) self.image_episode.show() elif episode.image_url is not None: # this must be before the get_image call, as it may call # immediately to image_episode_loaded (showing the image and # hiding the throber) self.image_episode.hide() self.throbber.show() # now do call the get_image self.get_image(episode.episode_id, episode.image_url.encode('utf-8')) # all description description = episode.description.replace("\n", "
") if episode.subtitle is None: msg = "

%s



%s" % ( episode.composed_title, description) else: msg = "

%s

%s


%s" % ( episode.composed_title, episode.subtitle, description) self.text_edit.setHtml(msg) # action button self.button.show() if episode.state == data.Status.downloaded: label = "Reproducir" func = self.main_window.play_episode enable = True remove = False elif episode.state == data.Status.downloading: label = "Cancelar descarga" func = self.main_window.cancel_download enable = True remove = False elif episode.state == data.Status.waiting: label = "Sacar de la cola" func = self.main_window.unqueue_download enable = True remove = True else: label = "Descargar" func = self.main_window.download_episode enable = bool(self.main_window.have_config()) remove = False def _exec(func, episode, remove): """Execute a function on the episode and update its info.""" func(episode) if not remove: self.update(episode) # set button text, disconnect if should, and connect new func self.button.setEnabled(enable) self.button.setText(label) if self.button.connected: self.button.clicked.disconnect() self.button.connected = True self.button.clicked.connect(lambda: _exec(func, episode, remove)) class BigPanel(QWidget): """The big panel for the main interface with user.""" def __init__(self, main_window): super(BigPanel, self).__init__() self.main_window = main_window layout = QHBoxLayout(self) # get this before, as it be used when creating other sutff episode_info = EpisodeInfo(main_window) self.episodes = EpisodesWidgetView(main_window, episode_info) # split on the right right_split = remembering.RememberingSplitter(Qt.Vertical, 'right') right_split.addWidget(episode_info) self.downloads_widget = DownloadsWidget(self.episodes) right_split.addWidget(self.downloads_widget) # main split main_split = remembering.RememberingSplitter(Qt.Horizontal, 'main') main_split.addWidget(self.episodes) main_split.addWidget(right_split) layout.addWidget(main_split) encuentro-5.0/encuentro/ui/main.py0000664000175000017500000004625713131470344020375 0ustar facundofacundo00000000000000# -*- coding: utf-8 -*- # Copyright 2013-2016 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """The main window.""" import logging import os import datetime as dt import defer from PyQt4.QtGui import ( QAction, QCheckBox, QKeySequence, QLabel, QLineEdit, QMessageBox, QShortcut, QSizePolicy, QStyle, QWidget, ) from encuentro import multiplatform, data, update from encuentro.config import config, signal from encuentro.data import Status from encuentro.network import ( BadCredentialsError, CancelledError, EncuentroError, all_downloaders, ) from encuentro.notify import notify from encuentro.ui import ( central_panel, preferences, remembering, systray, wizard, ) logger = logging.getLogger('encuentro.main') # tooltips for buttons enabled and disabled TTIP_PLAY_E = 'Reproducir el programa' TTIP_PLAY_D = "Reproducir - El episodio debe estar descargado para poder verlo." TTIP_DOWNLOAD_E = 'Descargar el programa de la web' TTIP_DOWNLOAD_D = ( "Descargar - No se puede descargar si ya está descargado o falta " "alguna configuración en el programa." ) ABOUT_TEXT = """
Simple programa que permite buscar, descargar y ver
contenido del canal Encuentro y otros.

Versión %s

Copyright 2010-2013 Facundo Batista

http://encuentro.taniquetil.com.ar
""" class MainUI(remembering.RememberingMainWindow): """Main UI.""" _programs_file = os.path.join(multiplatform.data_dir, 'encuentro.data') def __init__(self, version, app_quit, update_source): super(MainUI, self).__init__() self.app_quit = app_quit self.finished = False self.version = version self.update_source = update_source self.downloaders = {} self.setWindowTitle('Encuentro') self.programs_data = data.ProgramsData(self, self._programs_file) self._touch_config() # finish all gui stuff self.big_panel = central_panel.BigPanel(self) self.episodes_list = self.big_panel.episodes self.episodes_download = self.big_panel.downloads_widget self.setCentralWidget(self.big_panel) # the setting of menubar should be almost in the end, because it may # trigger the wizard, which needs big_panel and etc. self.action_play = self.action_download = None self.filter_line = self.filter_cbox = self.needsomething_alert = None self._menubar() systray.show(self) if config.get('autorefresh'): ue = update.UpdateEpisodes(self, update_source) ue.background() else: # refresh data if never done before or if last # update was 7 days ago last_refresh = config.get('autorefresh_last_time') if last_refresh is None or ( dt.datetime.now() - last_refresh > dt.timedelta(7)): ue = update.UpdateEpisodes(self, update_source) ue.background() self.show() self.episodes_download.load_pending() logger.debug("Main UI started ok") def _touch_config(self): """Do some config processing.""" # log the config, but without user and pass safecfg = config.sanitized_config() logger.debug("Configuration loaded: %s", safecfg) # we have a default for download dir if not config.get('downloaddir'): config['downloaddir'] = multiplatform.get_download_dir() # maybe clean some config if self.programs_data.reset_config_from_migration: config['user'] = '' config['password'] = '' config.pop('cols_width', None) config.pop('cols_order', None) config.pop('selected_row', None) def have_config(self): """Return if some config is needed.""" return config.get('user') and config.get('password') def have_metadata(self): """Return if metadata is needed.""" return bool(self.programs_data) def _menubar(self): """Set up the menu bar.""" menubar = self.menuBar() # applications menu menu_appl = menubar.addMenu('&Aplicación') icon = self.style().standardIcon(QStyle.SP_BrowserReload) action_reload = QAction(icon, '&Refrescar', self) action_reload.setShortcut('Ctrl+R') action_reload.setToolTip('Recarga la lista de programas') action_reload.triggered.connect(self.refresh_episodes) menu_appl.addAction(action_reload) icon = self.style().standardIcon(QStyle.SP_FileDialogDetailedView) action_preferences = QAction(icon, '&Preferencias', self) action_preferences.triggered.connect(self.open_preferences) action_preferences.setToolTip('Configurar distintos parámetros del programa') menu_appl.addAction(action_preferences) menu_appl.addSeparator() icon = self.style().standardIcon(QStyle.SP_MessageBoxInformation) _act = QAction(icon, '&Acerca de', self) _act.triggered.connect(self.open_about_dialog) _act.setToolTip('Muestra información de la aplicación') menu_appl.addAction(_act) icon = self.style().standardIcon(QStyle.SP_DialogCloseButton) _act = QAction(icon, '&Salir', self) _act.setShortcut('Ctrl+Q') _act.setToolTip('Sale de la aplicación') _act.triggered.connect(self.on_close) menu_appl.addAction(_act) # program menu menu_prog = menubar.addMenu('&Programa') icon = self.style().standardIcon(QStyle.SP_ArrowDown) self.action_download = QAction(icon, '&Descargar', self) self.action_download.setShortcut('Ctrl+D') self.action_download.setEnabled(False) self.action_download.setToolTip(TTIP_DOWNLOAD_D) self.action_download.triggered.connect(self.download_episode) menu_prog.addAction(self.action_download) icon = self.style().standardIcon(QStyle.SP_MediaPlay) self.action_play = QAction(icon, '&Reproducir', self) self.action_play.setEnabled(False) self.action_play.setToolTip(TTIP_PLAY_D) self.action_play.triggered.connect(self.on_play_action) menu_prog.addAction(self.action_play) # toolbar for buttons toolbar = self.addToolBar('main') toolbar.addAction(self.action_download) toolbar.addAction(self.action_play) toolbar.addSeparator() toolbar.addAction(action_reload) toolbar.addAction(action_preferences) # filter text and button, to the right spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) toolbar.addWidget(spacer) toolbar.addWidget(QLabel("Filtro: ")) self.filter_line = QLineEdit() self.filter_line.setMaximumWidth(150) self.filter_line.textChanged.connect(self.on_filter_changed) toolbar.addWidget(self.filter_line) self.filter_cbox = QCheckBox("Sólo descargados") self.filter_cbox.stateChanged.connect(self.on_filter_changed) toolbar.addWidget(self.filter_cbox) QShortcut(QKeySequence("Ctrl+F"), self, self.filter_line.setFocus) # if needed, a warning that stuff needs to be configured icon = self.style().standardIcon(QStyle.SP_MessageBoxWarning) m = "Necesita configurar algo; haga click aquí para abrir el wizard" self.needsomething_alert = QAction(icon, m, self) self.needsomething_alert.triggered.connect(self._start_wizard) toolbar.addAction(self.needsomething_alert) if not config.get('nowizard'): self._start_wizard() self._review_need_something_indicator() def _start_wizard(self, _=None): """Start the wizard if needed.""" if not self.have_config() or not self.have_metadata(): dlg = wizard.WizardDialog(self) dlg.exec_() self._review_need_something_indicator() def on_filter_changed(self, _): """The filter text has changed, apply it in the episodes list.""" text = self.filter_line.text() cbox = self.filter_cbox.checkState() self.episodes_list.set_filter(text, cbox) # after applying filter, nothing is selected, so check buttons # (easiest way to clean them all) self.check_download_play_buttons() def _review_need_something_indicator(self): """Hide/show/enable/disable different indicators if need sth.""" needsomething = bool(not self.have_config() or not self.have_metadata()) self.needsomething_alert.setVisible(needsomething) def shutdown(self): """Stop everything and quit. This shutdown con be called at any time, even on init, so we have extra precautions about which attributes we have. """ signal.emit('save_state') config.save() self.finished = True programs_data = getattr(self, 'programs_data', None) if programs_data is not None: programs_data.save() # shutdown all the downloaders for downloader in self.downloaders.itervalues(): downloader.shutdown() # bye bye self.app_quit() def on_close(self, _): """Close signal.""" if self._should_close(): self.shutdown() def closeEvent(self, event): """All is being closed.""" if self._should_close(): self.shutdown() else: event.ignore() def _should_close(self): """Still time to decide if want to close or not.""" logger.info("Attempt to close the program") pending = self.episodes_download.pending() if not pending: # all fine, save all and quit logger.info("Saving states and quitting") return True logger.debug("Still %d active downloads when trying to quit", pending) # stuff pending m = "Hay programas todavía en proceso de descarga!\n¿Seguro quiere salir del programa?" QMB = QMessageBox dlg = QMB("Guarda!", m, QMB.Question, QMB.Yes, QMB.No, QMB.NoButton) opt = dlg.exec_() if opt != QMB.Yes: logger.info("Quit cancelled") return False # quit anyway, put all downloading and pending episodes to none logger.info("Fixing episodes, saving state and exiting") for program in self.programs_data.values(): state = program.state if state == Status.waiting or state == Status.downloading: program.state = Status.none return True def show_message(self, err_type, text): """Show different messages to the user.""" if self.finished: logger.debug("Ignoring message: %r", text) return logger.debug("Showing a message: %r", text) # error text can be produced by windows, try to to sanitize it if isinstance(text, str): try: text = text.decode("utf8") except UnicodeDecodeError: try: text = text.decode("latin1") except UnicodeDecodeError: text = repr(text) QMB = QMessageBox dlg = QMB("Atención: " + err_type, text, QMB.Warning, QMB.Ok, QMB.NoButton, QMB.NoButton) dlg.exec_() def refresh_episodes(self, _=None): """Update and refresh episodes.""" ue = update.UpdateEpisodes(self, self.update_source) ue.interactive() def download_episode(self, _=None): """Download the episode(s).""" episode_ids = self.episodes_list.selected_items() for episode_id in episode_ids: episode = self.programs_data[episode_id] self.queue_download(episode) @defer.inline_callbacks def queue_download(self, episode): """User indicated to download something.""" logger.debug("Download requested of %s", episode) if episode.state != Status.none: logger.debug("Download denied, episode %s is not in downloadeable " "state.", episode.episode_id) return # queue self.episodes_download.append(episode) self.episodes_list.episode_info.update(episode) self.check_download_play_buttons() if self.episodes_download.downloading: return logger.debug("Downloads: starting") while self.episodes_download.pending(): episode = self.episodes_download.prepare() try: filename, episode = yield self._episode_download(episode) except CancelledError: logger.debug("Got a CancelledError!") self.episodes_download.end(error="Cancelado") except BadCredentialsError: logger.debug("Bad credentials error!") msg = "Error con las credenciales: hay que configurar usuario y clave correctos" self.show_message('BadCredentialsError', msg) self.episodes_download.end(error=msg) except EncuentroError as e: orig_exc = e.orig_exc msg = "%s(%s)" % (orig_exc, e) err_type = e.__class__.__name__ logger.exception("Custom Encuentro error: %s (%r)", e, orig_exc) notify(err_type, msg) self.episodes_download.end(error="Error: " + msg) except Exception as e: err_type = e.__class__.__name__ notify(err_type, str(e)) logger.exception("Unknown download error: %r (%r)", err_type, e) self.episodes_download.end(error="Error: {!r} ({!r})".format(err_type, e)) else: logger.debug("Episode downloaded: %s", episode) self.episodes_download.end() episode.filename = filename # check buttons self.check_download_play_buttons() # adjust the episode info only if it's still showing this one self.episodes_list.episode_info.update(episode, force_change=False) logger.debug("Downloads: finished") @defer.inline_callbacks def _episode_download(self, episode): """Effectively download an episode.""" logger.debug("Effectively downloading episode %s", episode.episode_id) self.episodes_download.start() # download! downloader_class = all_downloaders[episode.downtype] downloader = self.downloaders[episode.episode_id] = downloader_class() season = getattr(episode, 'season', None) # wasn't always there downloader.download(episode.channel, episode.section, season, episode.title, episode.url, self.episodes_download.progress) try: fname = yield downloader.deferred finally: self.downloaders.pop(episode.episode_id, None) episode_name = "%s - %s - %s" % (episode.channel, episode.section, episode.composed_title) notify("Descarga finalizada", episode_name) defer.return_value((fname, episode)) def open_preferences(self, _=None): """Open the preferences dialog.""" dlg = preferences.PreferencesDialog() dlg.exec_() # after dialog closes, config changed, so review indicators self._review_need_something_indicator() safecfg = config.sanitized_config() logger.debug("Configuration changed: %s", safecfg) def check_download_play_buttons(self): """Set both buttons state according to the selected episodes.""" episode_ids = self.episodes_list.selected_items() # 'play' button should be enabled if only one row is selected and # its state is 'downloaded' play_enabled = False if len(episode_ids) == 1: episode = self.programs_data[episode_ids[0]] if episode.state == Status.downloaded: play_enabled = True self.action_play.setEnabled(play_enabled) ttip = TTIP_PLAY_E if play_enabled else TTIP_PLAY_D self.action_play.setToolTip(ttip) # 'download' button should be enabled if at least one of the selected # rows is in 'none' state, and if config is ok download_enabled = False if self.have_config(): for episode_id in episode_ids: episode = self.programs_data[episode_id] if episode.state == Status.none: download_enabled = True break ttip = TTIP_DOWNLOAD_E if download_enabled else TTIP_DOWNLOAD_D self.action_download.setEnabled(download_enabled) self.action_download.setToolTip(ttip) def on_play_action(self, _=None): """Play the selected episode.""" episode_ids = self.episodes_list.selected_items() if len(episode_ids) != 1: raise ValueError("Wrong call to play_episode, with %d selections" % len(episode_ids)) episode_id = episode_ids[0] episode = self.programs_data[episode_id] self.play_episode(episode) def play_episode(self, episode): """Play an episode.""" downloaddir = config.get('downloaddir', '') filename = os.path.join(downloaddir, episode.filename) logger.info("Play requested of %s", episode) if os.path.exists(filename): # pass file:// url with absolute path fullpath = 'file://' + os.path.abspath(filename) logger.info("Playing %r", fullpath) multiplatform.open_file(fullpath) else: logger.warning("Aborted playing, file not found: %r", filename) msg = "No se encontró el archivo para reproducir: " + repr(filename) self.show_message('Error al reproducir', msg) episode.state = Status.none self.episodes_list.set_color(episode) def cancel_download(self, episode): """Cancel the downloading of an episode.""" logger.info("Cancelling download of %s", episode) self.episodes_download.cancel() downloader = self.downloaders.pop(episode.episode_id) downloader.cancel() def unqueue_download(self, episode): """Remove the episode from the download queue.""" logger.info("Unqueueing %s", episode) self.episodes_download.unqueue(episode) def open_about_dialog(self): """Show the about dialog.""" version = self.version if self.version else "(?)" title = "Encuentro v" + version text = ABOUT_TEXT % (version,) QMessageBox.about(self, title, text) encuentro-5.0/encuentro/ui/media/0000775000175000017500000000000013144662314020144 5ustar facundofacundo00000000000000encuentro-5.0/encuentro/ui/media/throbber.gif0000664000175000017500000000164612133130037022436 0ustar facundofacundo00000000000000GIF89a333LLLfff! NETSCAPE2.0!Created with GIMP! ,SI̵SHކ%:Vo s&F'7` Kjb-[+@x`1lF-G! ,SI/̵ H%:Vo 0s&F'G ` Kjb-[Px`1lF-G! ,SIO̵0H%:Vo As&F'W(`Kjb-[`x`1lF-G! ,SIo ̵AH%:Vo TQs&F'g0`Kjb-[px`1lF-G! ,SI̵SQH %:Vo as&F'w8`Kjb-[x`1lF-G! ,SI̵aH%:Vo qs&F'`Kjb-[#x`1lF-G! ,SI̵qH%:Vo s&F'`Kjb-[C x`1lF-G! ,SI̵H%:Vo Ts&F''`Kjb-[c0x`1lF-G;encuentro-5.0/encuentro/ui/throbber.py0000664000175000017500000000261412327320070021241 0ustar facundofacundo00000000000000# -*- coding: UTF-8 -*- # Copyright 2013-2014 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """A throbber.""" from PyQt4.QtGui import ( QLabel, QMovie, ) from PyQt4.QtCore import Qt from encuentro import multiplatform class Throbber(QLabel): """A throbber.""" def __init__(self): super(Throbber, self).__init__() self.setAlignment(Qt.AlignCenter) fname = multiplatform.get_path("encuentro/ui/media/throbber.gif") self._movie = QMovie(fname) self.setMovie(self._movie) def hide(self): """Overload to control the movie.""" self._movie.stop() super(Throbber, self).hide() def show(self): """Overload to control the movie.""" self._movie.start() super(Throbber, self).show() encuentro-5.0/encuentro/ui/preferences.py0000664000175000017500000001705612637746176021770 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2013-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """The preferences dialog.""" import os import sys import logging from PyQt4.QtGui import ( QCheckBox, QCompleter, QDialog, QDialogButtonBox, QDirModel, QFileDialog, QGridLayout, QLabel, QLineEdit, QPushButton, QTabWidget, QVBoxLayout, QWidget, ) from PyQt4.QtCore import Qt, QDir from encuentro.config import config logger = logging.getLogger('encuentro.preferences') URL_CONECTATE = ( "http://registro.educ.ar/cuentas/registro/index?servicio=conectate" ) class GeneralPreferences(QWidget): """The general preferences input.""" def __init__(self): super(GeneralPreferences, self).__init__() grid = QGridLayout(self) grid.setSpacing(20) grid.setColumnStretch(1, 10) # directory auto completer completer = QCompleter(self) dirs = QDirModel(self) dirs.setFilter(QDir.AllDirs | QDir.NoDotAndDotDot) completer.setModel(dirs) completer.setCaseSensitivity(Qt.CaseInsensitive) completer.setCompletionMode(QCompleter.PopupCompletion) l = QLabel("Ingresá el directorio donde descargar los videos...") l.setTextFormat(Qt.RichText) grid.addWidget(l, 0, 0, 1, 2) grid.addWidget(QLabel("Descargar en:"), 1, 0, 2, 1) prv = config.get('downloaddir', '') self.downloaddir_entry = QLineEdit(prv) self.downloaddir_entry.setCompleter(completer) self.downloaddir_entry.setPlaceholderText('Ingresá un directorio') grid.addWidget(self.downloaddir_entry, 1, 1, 2, 2) self.downloaddir_buttn = QPushButton("Elegir un directorio") self.downloaddir_buttn.clicked.connect(self._choose_dir) grid.addWidget(self.downloaddir_buttn, 2, 1, 3, 2) self.autoreload_checkbox = QCheckBox( "Recargar automáticamente la lista de episodios al iniciar") self.autoreload_checkbox.setToolTip( "Cada vez que arranca el programa refrescar la lista de episodios.") prv = config.get('autorefresh', False) self.autoreload_checkbox.setChecked(prv) grid.addWidget(self.autoreload_checkbox, 3, 0, 4, 2) self.shownotifs_checkbox = QCheckBox( "Mostrar una notificación cuando termina cada descarga") self.shownotifs_checkbox.setToolTip( "Hacer que el escritorio muestre una notificación cada vez que una descarga " "se complete.") prv = config.get('notification', True) self.shownotifs_checkbox.setChecked(prv) grid.addWidget(self.shownotifs_checkbox, 4, 0, 5, 2) self.cleanfnames_checkbox = QCheckBox( "Limpiar nombres para que se pueda guardar en cualquier lado") self.cleanfnames_checkbox.setToolTip( "Convertir caracteres extraños en títulos para que el archivo se pueda grabar en " "cualquier disco o pendrive.") prv = config.get('clean-filenames', False) self.cleanfnames_checkbox.setChecked(prv) grid.addWidget(self.cleanfnames_checkbox, 5, 0, 6, 2) def _choose_dir(self): """Choose a directory using a dialog.""" resp = QFileDialog.getExistingDirectory(self, '', os.path.expanduser("~")) if resp: self.downloaddir_entry.setText(resp) def get_config(self): """Return the config for this tab.""" d = {} d['downloaddir'] = self.downloaddir_entry.text() d['autorefresh'] = self.autoreload_checkbox.isChecked() d['notification'] = self.shownotifs_checkbox.isChecked() d['clean-filenames'] = self.cleanfnames_checkbox.isChecked() return d class ConectatePreferences(QWidget): """The preferences for Conectate backend.""" def __init__(self): super(ConectatePreferences, self).__init__() grid = QGridLayout(self) grid.setSpacing(20) grid.setColumnStretch(1, 10) l = QLabel("Ingresá tus datos del portal Conectate:") l.setTextFormat(Qt.RichText) grid.addWidget(l, 0, 0, 1, 2) grid.addWidget(QLabel("Usuario:"), 1, 0, 2, 1) prv = config.get('user', '') self.user_entry = QLineEdit(prv) self.user_entry.setPlaceholderText('Ingresá tu usuario de Conectate') grid.addWidget(self.user_entry, 1, 1, 2, 2) grid.addWidget(QLabel("Contraseña:"), 2, 0, 3, 1) prv = config.get('password', '') self.password_entry = QLineEdit(prv) self.password_entry.setEchoMode(QLineEdit.Password) self.password_entry.setPlaceholderText('Ingresá tu contraseña de Conectate') grid.addWidget(self.password_entry, 2, 1, 3, 2) self.password_mask = QCheckBox('Mostrar contraseña') self.password_mask.stateChanged.connect(self._toggle_password_mask) grid.addWidget(self.password_mask, 3, 1, 4, 2) l = QLabel('Si no tenés estos datos, ' 'registrate aquí'.format(URL_CONECTATE)) l.setAlignment(Qt.AlignRight | Qt.AlignVCenter) l.setTextFormat(Qt.RichText) l.setOpenExternalLinks(True) grid.addWidget(l, 4, 0, 5, 3) def _toggle_password_mask(self): """Toggle the password hiding.""" if self.password_mask.isChecked() is True: self.password_entry.setEchoMode(QLineEdit.Normal) else: self.password_entry.setEchoMode(QLineEdit.Password) def get_config(self): """Return the config for this tab.""" d = {} d['user'] = self.user_entry.text() d['password'] = self.password_entry.text() return d class PreferencesDialog(QDialog): """The dialog for preferences.""" def __init__(self): super(PreferencesDialog, self).__init__() vbox = QVBoxLayout(self) tabbed = QTabWidget() self.gp = GeneralPreferences() tabbed.addTab(self.gp, "General") self.cp = ConectatePreferences() tabbed.addTab(self.cp, "Conectate") vbox.addWidget(tabbed) bbox = QDialogButtonBox(QDialogButtonBox.Ok) bbox.accepted.connect(self.accept) bbox.accepted.connect(self._save) vbox.addWidget(bbox) def closeEvent(self, event): """Save and close.""" self._save() super(PreferencesDialog, self).closeEvent(event) def _save(self): """Just save.""" # get it from tabs config.update(self.gp.get_config()) config.update(self.cp.get_config()) config.save() if __name__ == '__main__': project_basedir = os.path.abspath(os.path.dirname(os.path.dirname( os.path.realpath(sys.argv[0])))) sys.path.insert(0, project_basedir) from PyQt4.QtGui import QApplication app = QApplication(sys.argv) frame = PreferencesDialog() frame.show() frame.exec_() frame.save_config() encuentro-5.0/encuentro/ui/remembering.py0000664000175000017500000001365112637746176021760 0ustar facundofacundo00000000000000# -*- coding: UTF-8 -*- # Copyright 2013-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """The remembering widgets.""" from PyQt4.QtCore import Qt from PyQt4.QtGui import ( QMainWindow, QSplitter, QTableView, QTreeWidget, ) from encuentro.config import config, signal SYSTEM = config.SYSTEM class RememberingMainWindow(QMainWindow): """A MainWindow that remembers size and position.""" def __init__(self): super(RememberingMainWindow, self).__init__() signal.register(self.save_state) self._name = self.__class__.__name__ self._initted = False def showEvent(self, event): """Know when it was shown, load config.""" if not self._initted: self._initted = True conf = config[SYSTEM].get(self._name, {}) prv_size = conf.get('size', (800, 600)) prv_pos = conf.get('pos', (300, 300)) self.resize(*prv_size) self.move(*prv_pos) super(RememberingMainWindow, self).showEvent(event) def save_state(self): """Save what to remember.""" qsize = self.size() size = qsize.width(), qsize.height() qpos = self.pos() pos = qpos.x(), qpos.y() to_save = dict(pos=pos, size=size) config[SYSTEM][self._name] = to_save class RememberingSplitter(QSplitter): """A Splitter that remembers position.""" def __init__(self, type_, name): super(RememberingSplitter, self).__init__(type_) signal.register(self.save_state) cname = self.__class__.__name__ self._name = '-'.join((cname, name)) self._initted = False def showEvent(self, event): """Know when it was shown, load config.""" if not self._initted: self._initted = True sizes = config[SYSTEM].get(self._name) if sizes is not None: self.setSizes(sizes) super(RememberingSplitter, self).showEvent(event) def save_state(self): """Save what to remember.""" sizes = self.sizes() config[SYSTEM][self._name] = sizes class RememberingTreeWidget(QTreeWidget): """A TreeWidget that remembers visual stuff.""" def __init__(self, name): super(RememberingTreeWidget, self).__init__() signal.register(self.save_state) cname = self.__class__.__name__ self._name = '-'.join((cname, name)) self._initted = False def showEvent(self, event): """Know when it was shown, load config.""" if not self._initted: self._initted = True info = config[SYSTEM].get(self._name) if info is not None: cols_w = info['cols_w'] for i, w in enumerate(cols_w): self.setColumnWidth(i, w) s_enabled = info['s_enabled'] self.setSortingEnabled(s_enabled) if s_enabled: s_column = info['s_column'] s_order = info['s_order'] ordr = Qt.AscendingOrder if s_order else Qt.DescendingOrder self.sortItems(s_column, ordr) super(RememberingTreeWidget, self).showEvent(event) def save_state(self): """Save what to remember.""" cols_w = [self.columnWidth(i) for i in range(self.columnCount())] s_enabled = self.isSortingEnabled() s_column = self.sortColumn() c = self.topLevelItemCount() if c < 2: # less than two records, no point in sorting s_order = True else: val_first = self.topLevelItem(0).text(s_column) val_last = self.topLevelItem(c - 1).text(s_column) s_order = val_first < val_last info = dict(cols_w=cols_w, s_enabled=s_enabled, s_column=s_column, s_order=s_order) config[SYSTEM][self._name] = info class RememberingTableView(QTableView): """A TableView that remembers visual stuff.""" def __init__(self, name): super(RememberingTableView, self).__init__() signal.register(self.save_state) cname = self.__class__.__name__ self._name = '-'.join((cname, name)) self._initted = False def showEvent(self, event): """Know when it was shown, load config.""" if not self._initted: self._initted = True info = config[SYSTEM].get(self._name) if info is not None: # cols width cols_w = info['cols_w'] for i, w in enumerate(cols_w): self.setColumnWidth(i, w) # sort sort_section = info.get('sort_section', 0) sort_order = info.get('sort_order', 0) self.sortByColumn(sort_section, sort_order) super(RememberingTableView, self).showEvent(event) def save_state(self): """Save what to remember.""" # sorting info, from the header header = self.horizontalHeader() sort_section = header.sortIndicatorSection() sort_order = header.sortIndicatorOrder() # rest of values and store col_count = self.model().columnCount(None) cols_w = [self.columnWidth(i) for i in range(col_count)] info = dict(cols_w=cols_w, sort_section=sort_section, sort_order=sort_order) config[SYSTEM][self._name] = info encuentro-5.0/encuentro/ui/systray.py0000664000175000017500000000613012637746176021174 0ustar facundofacundo00000000000000# Copyright 2013-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Show an icon in the systray.""" import json import logging import subprocess from encuentro import multiplatform from PyQt4.QtGui import QSystemTrayIcon, QIcon, QMenu logger = logging.getLogger("encuentro.systray") def _should_fix(): """Tell if we should fix the Unity panel systray settings. Return None if don't need, else return the current conf. """ cmd = "gsettings get com.canonical.Unity.Panel systray-whitelist".split() try: out = subprocess.check_output(cmd) except Exception as err: # don't have gsettings, nothing to fix etype = err.__class__.__name__ logger.debug("No gsettings, no systray conf to fix (got %r %s)", etype, err) return try: conf = map(str, json.loads(out.strip().replace("'", '"'))) except ValueError: # don't understand the output, can't really fix it :/ logger.warning("Don't understand gsettings output: %r", out) return logger.info("gsettings conf: %r", conf) if "all" in conf or "encuentro" in conf: # we're ok! return # need to fix return conf def _fix_unity_systray(): """Check settings.""" conf = _should_fix() if conf is None: return conf.append("encuentro") cmd = ["gsettings", "set", "com.canonical.Unity.Panel", "systray-whitelist", str(conf)] try: out = subprocess.check_output(cmd) except OSError as err: logger.warning("Error trying to set the new conf: %s", err) else: logger.warning("New config set (result: %r)", out) def show(main_window): """Show a system tray icon with a small icon.""" _fix_unity_systray() icon = QIcon(multiplatform.get_path("encuentro/logos/icon-192.png")) sti = QSystemTrayIcon(icon, main_window) if not sti.isSystemTrayAvailable(): logger.warning("System tray not available.") return def showhide(_): """Show or hide the main window.""" if main_window.isVisible(): main_window.hide() else: main_window.show() _menu = QMenu(main_window) _act = _menu.addAction("Mostrar/Ocultar") _act.triggered.connect(showhide) _act = _menu.addAction("Acerca de") _act.triggered.connect(main_window.open_about_dialog) _act = _menu.addAction("Salir") _act.triggered.connect(main_window.on_close) sti.setContextMenu(_menu) sti.show() encuentro-5.0/encuentro/ui/dialogs.py0000664000175000017500000000574212637746176021110 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2013 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """Several dialogs.""" from PyQt4.QtGui import ( QDialog, QDialogButtonBox, QLabel, QPlainTextEdit, QPushButton, QVBoxLayout, ) UPGRADE_TEXT = """ Esta nueva versión del programa Encuentro sólo funciona con contenido actualizado, lo cual le permitirá trabajar con programas del canal Encuentro y de otros nuevos canales, pero deberá configurarlo nuevamente y perderá la posibilidad de ver directactamente los videos ya descargados (los cuales permanecerán en su disco). Haga click en Continuar y podrá ver el Wizard que lo ayudará a configurar nuevamente el programa. """ class ForceUpgradeDialog(QDialog): """The dialog for a force upgrade.""" def __init__(self): super(ForceUpgradeDialog, self).__init__() vbox = QVBoxLayout(self) self.setWindowTitle("El contenido debe actualizarse") self.main_text = QLabel(UPGRADE_TEXT) vbox.addWidget(self.main_text) bbox = QDialogButtonBox() bbox.addButton(QPushButton("Salir del programa"), QDialogButtonBox.AcceptRole) bbox.accepted.connect(self.accept) bbox.addButton(QPushButton("Continuar"), QDialogButtonBox.RejectRole) bbox.rejected.connect(self.reject) vbox.addWidget(bbox) self.show() class UpdateDialog(QDialog): """The dialog for update.""" def __init__(self): super(UpdateDialog, self).__init__() vbox = QVBoxLayout(self) self.closed = False self.setModal(True) self.resize(500, 250) vbox.addWidget(QLabel("Actualización de la lista de episodios:")) self.text = QPlainTextEdit() self.text.setReadOnly(True) vbox.addWidget(self.text) bbox = QDialogButtonBox(QDialogButtonBox.Cancel) bbox.rejected.connect(self.reject) vbox.addWidget(bbox) def append(self, text): """Append some text in the dialog.""" self.text.appendPlainText(text.strip()) def closeEvent(self, event): """It was closed.""" self.closed = True if __name__ == '__main__': import sys from PyQt4.QtGui import QApplication app = QApplication(sys.argv) frame = UpdateDialog() frame.show() frame.exec_() encuentro-5.0/encuentro/ui/wizard.py0000664000175000017500000001456612637746176020772 0ustar facundofacundo00000000000000# -*- coding: utf8 -*- # Copyright 2013-2015 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro from __future__ import unicode_literals """The wizard that guides the user for the initial setup.""" import logging from PyQt4.QtGui import ( QCheckBox, QDialog, QDialogButtonBox, QLabel, QPushButton, QVBoxLayout, ) from encuentro.config import config logger = logging.getLogger('encuentro.wizard') TEXT_INIT = """ Bienvenido al visor de contenido del Canal Encuentro y otros. Para poder usar el programa debe primero configurarlo! """ TEXT_EPISODES = """ Primero tiene que actualizar la lista de episodios: puede actualizar la lista ahora desde esta misma ventana y en cualquier momento desde el menú del programa. """ TEXT_CONFIG = """ Para poder descargar los programas de los distintos backends tiene que configurar algunos con usuario y clave; puede configurar el sistema ahora desde esta misma ventana o en cualquier momento desde el menú del programa. """ TEXT_HAPPY_END = """ Felicitaciones, el programa está listo para usar :) """ TEXT_SAD_END = """ ¡Ya podes usar el programa! (aunque te falta actualizar y/o configurar algo) """ # The steps for the wizard # - the text to show to the user # - the method to decide if the step should be ignored ("self._ign_"...) # - the text for the action button # - the action when clicked (will compose method with "self._act_") STEPS = [ (TEXT_INIT, None, None, None), (TEXT_EPISODES, "episode", "Actualizar", "update"), (TEXT_CONFIG, "config", "Configurar", "configure"), (None, None, None, None), ] class WizardDialog(QDialog): """The dialog for update.""" def __init__(self, main_window): super(WizardDialog, self).__init__() self.mw = main_window vbox = QVBoxLayout(self) # label and checkbox self.main_text = QLabel("init text") vbox.addWidget(self.main_text) self.notthisagain = QCheckBox("No mostrar automáticamente esta ayuda") nowizard = config.get('nowizard', False) self.notthisagain.setCheckState(nowizard) self.notthisagain.stateChanged.connect(self._notthisagain_toggled) vbox.addWidget(self.notthisagain) # buttons bbox = QDialogButtonBox() self.navbut_actn = QPushButton("init text") bbox.addButton(self.navbut_actn, QDialogButtonBox.ActionRole) self.navbut_prev = QPushButton("Anterior") bbox.addButton(self.navbut_prev, QDialogButtonBox.ActionRole) self.navbut_next = QPushButton("Siguiente") bbox.addButton(self.navbut_next, QDialogButtonBox.ActionRole) vbox.addWidget(bbox) self.show() self.step = 0 self._move(0) def _notthisagain_toggled(self, state): """The "not this again" checkbutton togled state.""" logger.info("Configuring 'nowizard' to %s", state) config['nowizard'] = state def _move(self, delta_step): """The engine for the wizard steps.""" self.step += delta_step logger.debug("Entering into step %d", self.step) (text, ign_func, act_label, act_func) = STEPS[self.step] # if this step should be ignored, just leave if ign_func is not None: m = getattr(self, "_ign_" + ign_func) if m(): # keep going return self._move(delta_step) # adjust navigation buttons if self.step == 0: self.navbut_prev.setEnabled(False) self.navbut_next.setText("Siguiente") self.navbut_next.clicked.disconnect() self.navbut_next.clicked.connect(lambda: self._move(1)) self.notthisagain.show() elif self.step == len(STEPS) - 1: self.navbut_prev.setEnabled(True) self.navbut_prev.clicked.disconnect() self.navbut_prev.clicked.connect(lambda: self._move(-1)) self.navbut_next.setText("Terminar") self.navbut_next.clicked.disconnect() self.navbut_next.clicked.connect(self.accept) self.notthisagain.hide() else: self.navbut_prev.setEnabled(True) self.navbut_prev.clicked.disconnect() self.navbut_prev.clicked.connect(lambda: self._move(-1)) self.navbut_next.setText("Siguiente") self.navbut_next.clicked.disconnect() self.navbut_next.clicked.connect(lambda: self._move(1)) self.notthisagain.hide() # adjust main text and action button if self.step == len(STEPS) - 1: if self.mw.have_metadata() and self.mw.have_config(): self.main_text.setText(TEXT_HAPPY_END) else: self.main_text.setText(TEXT_SAD_END) else: self.main_text.setText(text) if act_label is None: self.navbut_actn.hide() else: self.navbut_actn.show() self.navbut_actn.setText(act_label) method_to_call = getattr(self, "_act_" + act_func) self.navbut_actn.clicked.disconnect() self.navbut_actn.clicked.connect(method_to_call) def _act_configure(self, _): """Open the config dialog.""" self.mw.open_preferences() def _act_update(self, *a): """Open the update dialog.""" self.mw.refresh_episodes() def _ign_episode(self): """Tell if the episode step should be ignored.""" return self.mw.have_metadata() def _ign_config(self): """Tell if the configure step should be ignored.""" return self.mw.have_config() if __name__ == '__main__': import sys from PyQt4.QtGui import QApplication app = QApplication(sys.argv) app.have_metadata = lambda: False app.have_config = lambda: False frame = WizardDialog(app) frame.show() frame.exec_() encuentro-5.0/AYUDA.txt0000644000175000017500000000325312133130037016040 0ustar facundofacundo00000000000000¡Bienvenido al Visualizador de contenidos del Canal Encuentro y otros! Este es un simple programa que permite buscar, descargar y ver contenido del Canal Encuentro y otros. Para más información de cómo instalar el programa y licencias, leer el archivo LEEME.txt. Cómo ejecutar el programa ------------------------- Una vez que está instalado se puede ejecutar el programa escribiendo... encuentro ...desde la linea de comandos, aunque también se puede ejecutarlo de la siguiente manera estando parado en el directorio donde se descomprimió el tarball: bin/encuentro Para ver no solo la salida estándar sino también el log del programa, ejecutar: bin/encuentro -v Cómo usarlo ----------- Lo primero que hay que hacer es actualizar la metadata de los programas. Para ello está el botón Actualizar en la barra superior, hacé click ahí, y luego de bajar la info tendrás centenares de programas para elegir. Para descargar cualquiera de ellos, tenés que seleccionarlo y hacer click en el botón "Descargar". Pero antes, como cada descarga es personal, hay que configurar el programa con el usuario y clave que saques en el sitio web del Canal Encuentro o del backend que corresponda (no todos necesitan autenticación) Entonces, en el programa hacé click en el botón Configurar de la barra de arriba. Ahí vas a ver algunas pestañas, con configuración general y específica para los distintos backends, incluso con la URL para ir a registrarte y obtener las credenciales). Listo! Ahora puedes descargar cualquier programa de cualquier canal! Una vez descargado, para verlos sólo necesitás seleccionarlo y hacer click en el botón de Play. ¡Que los disfrutes! encuentro-5.0/LEEME.txt0000644000175000017500000000547612542135444016050 0ustar facundofacundo00000000000000¡Bienvenido al Visualizador de contenidos del Canal Encuentro! Este es un simple programa que permite buscar, descargar y ver contenido del Canal Encuentro y otros. Para más información de cómo usar el programa, leer el archivo AYUDA.txt. Si querés conocer más sobre el proyecto, el mismo se gestiona desde aquí: https://launchpad.net/encuentro *Nota importante*: Si tenés una versión anterior a la 3.1, el programa no te va a funcionar. Tenés que actualizar sí o sí la versión. Esto es porque Encuentro migró sus contenidos al portal Conectate, por lo que las versiones viejas no te van a funcionar correctamente. La buena noticia es que ahora podrás descargar no sólo contenido de Encuentro, sino también de Paka Paka, Educ.ar, y otros. Cómo ejecutarlo directamente ---------------------------- Tanto si hacés un branch del proyecto, como si abrís el tarball, el programa puede ejecutarse fácilmente sin instalarlo, haciendo: bin/encuentro Cómo instalarlo --------------- Es bastante sencillo, sólo tenés que hacer: sudo python setup.py install Para que funcione correctamente, tenés que tener Python 2 instalado, y las siguientes dependencias (paquete y número mínimo de versión): python 2.6.6 python-requests 2.2.1 python-defer 1.0.6 python-qt4 4.9.1 python-xdg 0.15 python-bs4 4.1.0 python-notify 0.1.1 (este último, python-notify, no es realmente necesario, pero si está el programa notificará las descargas finalizadas) Cómo instalarlo en un virtualenv -------------------------------- Si no tenés la menor idea de qué es un virtualenv y el detalle anterior para instalar te sirve podés omitir este punto. Si querés instalarlo en un virtualenv para colaborar en el proyecto o por cualquier otra causa, tenés que seguir los siguientes pasos: Es necesario hacer un branch del proyecto, o descargar el tarball, tener instalado python-qt4 y python-notify. Al generar el virtualenv tenés que utilizar la opción '--system-site-packages'. Crear el virtualenv: virtualenv --system-site-packages path/to/encuentro_venv Activarlo: source path/to/encuentro_venv/bin/activate Instalar las dependencias: cd path/to/encuentro/code/trunk pip install -r requirements_py2.txt Ejecutar Encuentro: bin/encuentro Licencias --------- Este programa no distribuye contenido de Canal Encuentro, sino que permite un mejor uso personal de esos contenidos. Por favor, referirse al sitio web de Conectate (http://conectate.gov.ar/) para saber qué se puede y qué no se puede hacer con los contenidos de ese sitio. El código de este programa está licenciado bajo la GNU GPL v3, podés encontrar esta licencia en el archivo COPYING o acá: http://www.gnu.org/licenses/gpl.html encuentro-5.0/README.txt0000644000175000017500000000126012253337073016142 0ustar facundofacundo00000000000000Welcome to the Canal Encuentro visualization program! This is a simple program to search, download and see the content of the Canal Encuentro and others. This program is strongly oriented to Spanish speaking people, as the content of Canal Encuentro and the other channels is only in Spanish... for further information please check the LEEME.txt file. Notes regarding licenses: - The content of the channels is not distributed at all, but downloaded personally by the user, please check here to see the licenses about that content in http://conectate.gov.ar/ - The code is licensed under the GNU GPL v3, see file COPYING here or... http://www.gnu.org/licenses/gpl.html encuentro-5.0/bin/0000775000175000017500000000000013144662314015216 5ustar facundofacundo00000000000000encuentro-5.0/bin/encuentro0000755000175000017500000000441313131470344017142 0ustar facundofacundo00000000000000#!/usr/bin/env python # Copyright 2011-2017 Facundo Batista # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, 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 . # # For further info, check https://launchpad.net/encuentro """Script to run Encuentro.""" import argparse import logging import sys import os # this will be replaced at install time INSTALLED_BASE_DIR = "@ INSTALLED_BASE_DIR @" # get the replaced-at-install-time name if exists, or the project one if os.path.exists(INSTALLED_BASE_DIR): project_basedir = INSTALLED_BASE_DIR sys._INSTALLED_BASE_DIR = INSTALLED_BASE_DIR else: project_basedir = os.path.abspath(os.path.dirname(os.path.dirname( os.path.realpath(sys.argv[0])))) if project_basedir not in sys.path: sys.path.insert(0, project_basedir) sys.path.insert(1, os.path.join(project_basedir, 'qtreactor')) from encuentro import main, multiplatform, logger # parse cmd line params parser = argparse.ArgumentParser() parser.add_argument('--verbose', '-v', action='store_true', help="Set the log in verbose.") parser.add_argument('--source', '-s', help="Define the local source for metadata update files.") args = parser.parse_args() # set up logging verbose = bool(args.verbose) logger.set_up(verbose) log = logging.getLogger('encuentro.init') # first of all, show the versions print "Running Python %s on %r" % (sys.version_info, sys.platform) log.info("Running Python %s on %r", sys.version_info, sys.platform) version_file = multiplatform.get_path('version.txt') if os.path.exists(version_file): version = open(version_file).read().strip() print "Encuentro: v. %s" % (version,) else: version = None print "Encuentro: sin revno info" log.info("Encuentro version: %r", version) main.start(version, args.source) encuentro-5.0/PKG-INFO0000664000175000017500000000056013144662314015544 0ustar facundofacundo00000000000000Metadata-Version: 1.0 Name: encuentro Version: 5.0 Summary: Search, download and see the wonderful Encuentro content. Home-page: https://launchpad.net/encuentro Author: Facundo Batista Author-email: facundo@taniquetil.com.ar License: GPL-3 Description: Simple application that allows to search, download and see the content of the Encuentro channel. Platform: UNKNOWN encuentro-5.0/encuentro.desktop0000644000175000017500000000062211776557665020066 0ustar facundofacundo00000000000000[Desktop Entry] Version=1.0 Encoding=UTF-8 Type=Application Name=Encuentro GenericName=Encuentro Comment=Simple program to download and see the content from Canal Encuentro y otros Comment[es]=Simple programa que permite buscar, descargar y ver contenido del Canal Encuentro and others Icon=@ INSTALLED_ICON @ Exec=encuentro Terminal=false StartupNotify=false Categories=Application;Education;Network; encuentro-5.0/external/0000775000175000017500000000000013144662314016270 5ustar facundofacundo00000000000000encuentro-5.0/external/youtube-dl/0000775000175000017500000000000013144662314020361 5ustar facundofacundo00000000000000encuentro-5.0/external/youtube-dl/README.md0000664000175000017500000017115612637746176021672 0ustar facundofacundo00000000000000youtube-dl - download videos from youtube.com or other video platforms - [INSTALLATION](#installation) - [DESCRIPTION](#description) - [OPTIONS](#options) - [CONFIGURATION](#configuration) - [OUTPUT TEMPLATE](#output-template) - [FORMAT SELECTION](#format-selection) - [VIDEO SELECTION](#video-selection) - [FAQ](#faq) - [DEVELOPER INSTRUCTIONS](#developer-instructions) - [EMBEDDING YOUTUBE-DL](#embedding-youtube-dl) - [BUGS](#bugs) - [COPYRIGHT](#copyright) # INSTALLATION To install it right away for all UNIX users (Linux, OS X, etc.), type: sudo curl https://yt-dl.org/latest/youtube-dl -o /usr/local/bin/youtube-dl sudo chmod a+rx /usr/local/bin/youtube-dl If you do not have curl, you can alternatively use a recent wget: sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl sudo chmod a+rx /usr/local/bin/youtube-dl Windows users can [download a .exe file](https://yt-dl.org/latest/youtube-dl.exe) and place it in their home directory or any other location on their [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29). OS X users can install **youtube-dl** with [Homebrew](http://brew.sh/). brew install youtube-dl You can also use pip: sudo pip install youtube-dl Alternatively, refer to the [developer instructions](#developer-instructions) for how to check out and work with the git repository. For further options, including PGP signatures, see the [youtube-dl Download Page](https://rg3.github.io/youtube-dl/download.html). # DESCRIPTION **youtube-dl** is a small command-line program to download videos from YouTube.com and a few more sites. It requires the Python interpreter, version 2.6, 2.7, or 3.2+, and it is not platform specific. It should work on your Unix box, on Windows or on Mac OS X. It is released to the public domain, which means you can modify it, redistribute it or use it however you like. youtube-dl [OPTIONS] URL [URL...] # OPTIONS -h, --help Print this help text and exit --version Print program version and exit -U, --update Update this program to latest version. Make sure that you have sufficient permissions (run with sudo if needed) -i, --ignore-errors Continue on download errors, for example to skip unavailable videos in a playlist --abort-on-error Abort downloading of further videos (in the playlist or the command line) if an error occurs --dump-user-agent Display the current browser identification --list-extractors List all supported extractors --extractor-descriptions Output descriptions of all supported extractors --force-generic-extractor Force extraction to use the generic extractor --default-search PREFIX Use this prefix for unqualified URLs. For example "gvsearch2:" downloads two videos from google videos for youtube-dl "large apple". Use the value "auto" to let youtube-dl guess ("auto_warning" to emit a warning when guessing). "error" just throws an error. The default value "fixup_error" repairs broken URLs, but emits an error if this is not possible instead of searching. --ignore-config Do not read configuration files. When given in the global configuration file /etc /youtube-dl.conf: Do not read the user configuration in ~/.config/youtube- dl/config (%APPDATA%/youtube-dl/config.txt on Windows) --flat-playlist Do not extract the videos of a playlist, only list them. --no-color Do not emit color codes in output ## Network Options: --proxy URL Use the specified HTTP/HTTPS proxy. Pass in an empty string (--proxy "") for direct connection --socket-timeout SECONDS Time to wait before giving up, in seconds --source-address IP Client-side IP address to bind to (experimental) -4, --force-ipv4 Make all connections via IPv4 (experimental) -6, --force-ipv6 Make all connections via IPv6 (experimental) --cn-verification-proxy URL Use this proxy to verify the IP address for some Chinese sites. The default proxy specified by --proxy (or none, if the options is not present) is used for the actual downloading. (experimental) ## Video Selection: --playlist-start NUMBER Playlist video to start at (default is 1) --playlist-end NUMBER Playlist video to end at (default is last) --playlist-items ITEM_SPEC Playlist video items to download. Specify indices of the videos in the playlist separated by commas like: "--playlist-items 1,2,5,8" if you want to download videos indexed 1, 2, 5, 8 in the playlist. You can specify range: "--playlist-items 1-3,7,10-13", it will download the videos at index 1, 2, 3, 7, 10, 11, 12 and 13. --match-title REGEX Download only matching titles (regex or caseless sub-string) --reject-title REGEX Skip download for matching titles (regex or caseless sub-string) --max-downloads NUMBER Abort after downloading NUMBER files --min-filesize SIZE Do not download any videos smaller than SIZE (e.g. 50k or 44.6m) --max-filesize SIZE Do not download any videos larger than SIZE (e.g. 50k or 44.6m) --date DATE Download only videos uploaded in this date --datebefore DATE Download only videos uploaded on or before this date (i.e. inclusive) --dateafter DATE Download only videos uploaded on or after this date (i.e. inclusive) --min-views COUNT Do not download any videos with less than COUNT views --max-views COUNT Do not download any videos with more than COUNT views --match-filter FILTER Generic video filter (experimental). Specify any key (see help for -o for a list of available keys) to match if the key is present, !key to check if the key is not present,key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against a number, and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the operator.For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike functionality is not available at the given service), but who also have a description, use --match-filter "like_count > 100 & dislike_count \youtube-dl.conf`. For example, with the following configuration file youtube-dl will always extract the audio, not copy the mtime and use a proxy: ``` --extract-audio --no-mtime --proxy 127.0.0.1:3128 ``` You can use `--ignore-config` if you want to disable the configuration file for a particular youtube-dl run. ### Authentication with `.netrc` file You may also want to configure automatic credentials storage for extractors that support authentication (by providing login and password with `--username` and `--password`) in order not to pass credentials as command line arguments on every youtube-dl execution and prevent tracking plain text passwords in the shell command history. You can achieve this using a [`.netrc` file](http://stackoverflow.com/tags/.netrc/info) on per extractor basis. For that you will need to create a`.netrc` file in your `$HOME` and restrict permissions to read/write by you only: ``` touch $HOME/.netrc chmod a-rwx,u+rw $HOME/.netrc ``` After that you can add credentials for extractor in the following format, where *extractor* is the name of extractor in lowercase: ``` machine login password ``` For example: ``` machine youtube login myaccount@gmail.com password my_youtube_password machine twitch login my_twitch_account_name password my_twitch_password ``` To activate authentication with the `.netrc` file you should pass `--netrc` to youtube-dl or place it in the [configuration file](#configuration). On Windows you may also need to setup the `%HOME%` environment variable manually. # OUTPUT TEMPLATE The `-o` option allows users to indicate a template for the output file names. The basic usage is not to set any template arguments when downloading a single file, like in `youtube-dl -o funny_video.flv "http://some/video"`. However, it may contain special sequences that will be replaced when downloading each video. The special sequences have the format `%(NAME)s`. To clarify, that is a percent symbol followed by a name in parentheses, followed by a lowercase S. Allowed names are: - `id`: The sequence will be replaced by the video identifier. - `url`: The sequence will be replaced by the video URL. - `uploader`: The sequence will be replaced by the nickname of the person who uploaded the video. - `upload_date`: The sequence will be replaced by the upload date in YYYYMMDD format. - `title`: The sequence will be replaced by the video title. - `ext`: The sequence will be replaced by the appropriate extension (like flv or mp4). - `epoch`: The sequence will be replaced by the Unix epoch when creating the file. - `autonumber`: The sequence will be replaced by a five-digit number that will be increased with each download, starting at zero. - `playlist`: The sequence will be replaced by the name or the id of the playlist that contains the video. - `playlist_index`: The sequence will be replaced by the index of the video in the playlist padded with leading zeros according to the total length of the playlist. - `format_id`: The sequence will be replaced by the format code specified by `--format`. - `duration`: The sequence will be replaced by the length of the video in seconds. The current default template is `%(title)s-%(id)s.%(ext)s`. In some cases, you don't want special characters such as 中, spaces, or &, such as when transferring the downloaded filename to a Windows system or the filename through an 8bit-unsafe channel. In these cases, add the `--restrict-filenames` flag to get a shorter title: ```bash $ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc youtube-dl test video ''_ä↭𝕐.mp4 # All kinds of weird characters $ youtube-dl --get-filename -o "%(title)s.%(ext)s" BaW_jenozKc --restrict-filenames youtube-dl_test_video_.mp4 # A simple file name ``` # FORMAT SELECTION By default youtube-dl tries to download the best quality, but sometimes you may want to download in a different format. The simplest case is requesting a specific format, for example `-f 22`. You can get the list of available formats using `--list-formats`, you can also use a file extension (currently it supports aac, m4a, mp3, mp4, ogg, wav, webm) or the special names `best`, `bestvideo`, `bestaudio` and `worst`. If you want to download multiple videos and they don't have the same formats available, you can specify the order of preference using slashes, as in `-f 22/17/18`. You can also filter the video results by putting a condition in brackets, as in `-f "best[height=720]"` (or `-f "[filesize>10M]"`). This works for filesize, height, width, tbr, abr, vbr, asr, and fps and the comparisons <, <=, >, >=, =, != and for ext, acodec, vcodec, container, and protocol and the comparisons =, != . Formats for which the value is not known are excluded unless you put a question mark (?) after the operator. You can combine format filters, so `-f "[height <=? 720][tbr>500]"` selects up to 720p videos (or videos where the height is not known) with a bitrate of at least 500 KBit/s. Use commas to download multiple formats, such as `-f 136/137/mp4/bestvideo,140/m4a/bestaudio`. You can merge the video and audio of two formats into a single file using `-f +` (requires ffmpeg or avconv), for example `-f bestvideo+bestaudio`. Format selectors can also be grouped using parentheses, for example if you want to download the best mp4 and webm formats with a height lower than 480 you can use `-f '(mp4,webm)[height<480]'`. Since the end of April 2015 and version 2015.04.26 youtube-dl uses `-f bestvideo+bestaudio/best` as default format selection (see #5447, #5456). If ffmpeg or avconv are installed this results in downloading `bestvideo` and `bestaudio` separately and muxing them together into a single file giving the best overall quality available. Otherwise it falls back to `best` and results in downloading the best available quality served as a single file. `best` is also needed for videos that don't come from YouTube because they don't provide the audio and video in two different files. If you want to only download some dash formats (for example if you are not interested in getting videos with a resolution higher than 1080p), you can add `-f bestvideo[height<=?1080]+bestaudio/best` to your configuration file. Note that if you use youtube-dl to stream to `stdout` (and most likely to pipe it to your media player then), i.e. you explicitly specify output template as `-o -`, youtube-dl still uses `-f best` format selection in order to start content delivery immediately to your player and not to wait until `bestvideo` and `bestaudio` are downloaded and muxed. If you want to preserve the old format selection behavior (prior to youtube-dl 2015.04.26), i.e. you want to download the best available quality media served as a single file, you should explicitly specify your choice with `-f best`. You may want to add it to the [configuration file](#configuration) in order not to type it every time you run youtube-dl. # VIDEO SELECTION Videos can be filtered by their upload date using the options `--date`, `--datebefore` or `--dateafter`. They accept dates in two formats: - Absolute dates: Dates in the format `YYYYMMDD`. - Relative dates: Dates in the format `(now|today)[+-][0-9](day|week|month|year)(s)?` Examples: ```bash # Download only the videos uploaded in the last 6 months $ youtube-dl --dateafter now-6months # Download only the videos uploaded on January 1, 1970 $ youtube-dl --date 19700101 $ # Download only the videos uploaded in the 200x decade $ youtube-dl --dateafter 20000101 --datebefore 20091231 ``` # FAQ ### How do I update youtube-dl? If you've followed [our manual installation instructions](http://rg3.github.io/youtube-dl/download.html), you can simply run `youtube-dl -U` (or, on Linux, `sudo youtube-dl -U`). If you have used pip, a simple `sudo pip install -U youtube-dl` is sufficient to update. If you have installed youtube-dl using a package manager like *apt-get* or *yum*, use the standard system update mechanism to update. Note that distribution packages are often outdated. As a rule of thumb, youtube-dl releases at least once a month, and often weekly or even daily. Simply go to http://yt-dl.org/ to find out the current version. Unfortunately, there is nothing we youtube-dl developers can do if your distribution serves a really outdated version. You can (and should) complain to your distribution in their bugtracker or support forum. As a last resort, you can also uninstall the version installed by your package manager and follow our manual installation instructions. For that, remove the distribution's package, with a line like sudo apt-get remove -y youtube-dl Afterwards, simply follow [our manual installation instructions](http://rg3.github.io/youtube-dl/download.html): ``` sudo wget https://yt-dl.org/latest/youtube-dl -O /usr/local/bin/youtube-dl sudo chmod a+x /usr/local/bin/youtube-dl hash -r ``` Again, from then on you'll be able to update with `sudo youtube-dl -U`. ### I'm getting an error `Unable to extract OpenGraph title` on YouTube playlists YouTube changed their playlist format in March 2014 and later on, so you'll need at least youtube-dl 2014.07.25 to download all YouTube videos. If you have installed youtube-dl with a package manager, pip, setup.py or a tarball, please use that to update. Note that Ubuntu packages do not seem to get updated anymore. Since we are not affiliated with Ubuntu, there is little we can do. Feel free to [report bugs](https://bugs.launchpad.net/ubuntu/+source/youtube-dl/+filebug) to the [Ubuntu packaging guys](mailto:ubuntu-motu@lists.ubuntu.com?subject=outdated%20version%20of%20youtube-dl) - all they have to do is update the package to a somewhat recent version. See above for a way to update. ### Do I always have to pass `-citw`? By default, youtube-dl intends to have the best options (incidentally, if you have a convincing case that these should be different, [please file an issue where you explain that](https://yt-dl.org/bug)). Therefore, it is unnecessary and sometimes harmful to copy long option strings from webpages. In particular, the only option out of `-citw` that is regularly useful is `-i`. ### Can you please put the `-b` option back? Most people asking this question are not aware that youtube-dl now defaults to downloading the highest available quality as reported by YouTube, which will be 1080p or 720p in some cases, so you no longer need the `-b` option. For some specific videos, maybe YouTube does not report them to be available in a specific high quality format you're interested in. In that case, simply request it with the `-f` option and youtube-dl will try to download it. ### I get HTTP error 402 when trying to download a video. What's this? Apparently YouTube requires you to pass a CAPTCHA test if you download too much. We're [considering to provide a way to let you solve the CAPTCHA](https://github.com/rg3/youtube-dl/issues/154), but at the moment, your best course of action is pointing a webbrowser to the youtube URL, solving the CAPTCHA, and restart youtube-dl. ### Do I need any other programs? youtube-dl works fine on its own on most sites. However, if you want to convert video/audio, you'll need [avconv](https://libav.org/) or [ffmpeg](https://www.ffmpeg.org/). On some sites - most notably YouTube - videos can be retrieved in a higher quality format without sound. youtube-dl will detect whether avconv/ffmpeg is present and automatically pick the best option. Videos or video formats streamed via RTMP protocol can only be downloaded when [rtmpdump](https://rtmpdump.mplayerhq.hu/) is installed. Downloading MMS and RTSP videos requires either [mplayer](http://mplayerhq.hu/) or [mpv](https://mpv.io/) to be installed. ### I have downloaded a video but how can I play it? Once the video is fully downloaded, use any video player, such as [vlc](http://www.videolan.org) or [mplayer](http://www.mplayerhq.hu/). ### I extracted a video URL with `-g`, but it does not play on another machine / in my webbrowser. It depends a lot on the service. In many cases, requests for the video (to download/play it) must come from the same IP address and with the same cookies. Use the `--cookies` option to write the required cookies into a file, and advise your downloader to read cookies from that file. Some sites also require a common user agent to be used, use `--dump-user-agent` to see the one in use by youtube-dl. It may be beneficial to use IPv6; in some cases, the restrictions are only applied to IPv4. Some services (sometimes only for a subset of videos) do not restrict the video URL by IP address, cookie, or user-agent, but these are the exception rather than the rule. Please bear in mind that some URL protocols are **not** supported by browsers out of the box, including RTMP. If you are using `-g`, your own downloader must support these as well. If you want to play the video on a machine that is not running youtube-dl, you can relay the video content from the machine that runs youtube-dl. You can use `-o -` to let youtube-dl stream a video to stdout, or simply allow the player to download the files written by youtube-dl in turn. ### ERROR: no fmt_url_map or conn information found in video info YouTube has switched to a new video info format in July 2011 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl. ### ERROR: unable to download video YouTube requires an additional signature since September 2012 which is not supported by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl. ### Video URL contains an ampersand and I'm getting some strange output `[1] 2839` or `'v' is not recognized as an internal or external command` That's actually the output from your shell. Since ampersand is one of the special shell characters it's interpreted by the shell preventing you from passing the whole URL to youtube-dl. To disable your shell from interpreting the ampersands (or any other special characters) you have to either put the whole URL in quotes or escape them with a backslash (which approach will work depends on your shell). For example if your URL is https://www.youtube.com/watch?t=4&v=BaW_jenozKc you should end up with following command: ```youtube-dl 'https://www.youtube.com/watch?t=4&v=BaW_jenozKc'``` or ```youtube-dl https://www.youtube.com/watch?t=4\&v=BaW_jenozKc``` For Windows you have to use the double quotes: ```youtube-dl "https://www.youtube.com/watch?t=4&v=BaW_jenozKc"``` ### ExtractorError: Could not find JS function u'OF' In February 2015, the new YouTube player contained a character sequence in a string that was misinterpreted by old versions of youtube-dl. See [above](#how-do-i-update-youtube-dl) for how to update youtube-dl. ### HTTP Error 429: Too Many Requests or 402: Payment Required These two error codes indicate that the service is blocking your IP address because of overuse. Contact the service and ask them to unblock your IP address, or - if you have acquired a whitelisted IP address already - use the [`--proxy` or `--source-address` options](#network-options) to select another IP address. ### SyntaxError: Non-ASCII character The error File "youtube-dl", line 2 SyntaxError: Non-ASCII character '\x93' ... means you're using an outdated version of Python. Please update to Python 2.6 or 2.7. ### What is this binary file? Where has the code gone? Since June 2012 (#342) youtube-dl is packed as an executable zipfile, simply unzip it (might need renaming to `youtube-dl.zip` first on some systems) or clone the git repository, as laid out above. If you modify the code, you can run it by executing the `__main__.py` file. To recompile the executable, run `make youtube-dl`. ### The exe throws a *Runtime error from Visual C++* To run the exe you need to install first the [Microsoft Visual C++ 2008 Redistributable Package](http://www.microsoft.com/en-us/download/details.aspx?id=29). ### On Windows, how should I set up ffmpeg and youtube-dl? Where should I put the exe files? If you put youtube-dl and ffmpeg in the same directory that you're running the command from, it will work, but that's rather cumbersome. To make a different directory work - either for ffmpeg, or for youtube-dl, or for both - simply create the directory (say, `C:\bin`, or `C:\Users\\bin`), put all the executables directly in there, and then [set your PATH environment variable](https://www.java.com/en/download/help/path.xml) to include that directory. From then on, after restarting your shell, you will be able to access both youtube-dl and ffmpeg (and youtube-dl will be able to find ffmpeg) by simply typing `youtube-dl` or `ffmpeg`, no matter what directory you're in. ### How do I put downloads into a specific folder? Use the `-o` to specify an [output template](#output-template), for example `-o "/home/user/videos/%(title)s-%(id)s.%(ext)s"`. If you want this for all of your downloads, put the option into your [configuration file](#configuration). ### How do I download a video starting with a `-`? Either prepend `http://www.youtube.com/watch?v=` or separate the ID from the options with `--`: youtube-dl -- -wNyEUrxzFU youtube-dl "http://www.youtube.com/watch?v=-wNyEUrxzFU" ### How do I pass cookies to youtube-dl? Use the `--cookies` option, for example `--cookies /path/to/cookies/file.txt`. Note that the cookies file must be in Mozilla/Netscape format and the first line of the cookies file must be either `# HTTP Cookie File` or `# Netscape HTTP Cookie File`. Make sure you have correct [newline format](https://en.wikipedia.org/wiki/Newline) in the cookies file and convert newlines if necessary to correspond with your OS, namely `CRLF` (`\r\n`) for Windows, `LF` (`\n`) for Linux and `CR` (`\r`) for Mac OS. `HTTP Error 400: Bad Request` when using `--cookies` is a good sign of invalid newline format. Passing cookies to youtube-dl is a good way to workaround login when a particular extractor does not implement it explicitly. ### Can you add support for this anime video site, or site which shows current movies for free? As a matter of policy (as well as legality), youtube-dl does not include support for services that specialize in infringing copyright. As a rule of thumb, if you cannot easily find a video that the service is quite obviously allowed to distribute (i.e. that has been uploaded by the creator, the creator's distributor, or is published under a free license), the service is probably unfit for inclusion to youtube-dl. A note on the service that they don't host the infringing content, but just link to those who do, is evidence that the service should **not** be included into youtube-dl. The same goes for any DMCA note when the whole front page of the service is filled with videos they are not allowed to distribute. A "fair use" note is equally unconvincing if the service shows copyright-protected videos in full without authorization. Support requests for services that **do** purchase the rights to distribute their content are perfectly fine though. If in doubt, you can simply include a source that mentions the legitimate purchase of content. ### How can I speed up work on my issue? (Also known as: Help, my important issue not being solved!) The youtube-dl core developer team is quite small. While we do our best to solve as many issues as possible, sometimes that can take quite a while. To speed up your issue, here's what you can do: First of all, please do report the issue [at our issue tracker](https://yt-dl.org/bugs). That allows us to coordinate all efforts by users and developers, and serves as a unified point. Unfortunately, the youtube-dl project has grown too large to use personal email as an effective communication channel. Please read the [bug reporting instructions](#bugs) below. A lot of bugs lack all the necessary information. If you can, offer proxy, VPN, or shell access to the youtube-dl developers. If you are able to, test the issue from multiple computers in multiple countries to exclude local censorship or misconfiguration issues. If nobody is interested in solving your issue, you are welcome to take matters into your own hands and submit a pull request (or coerce/pay somebody else to do so). Feel free to bump the issue from time to time by writing a small comment ("Issue is still present in youtube-dl version ...from France, but fixed from Belgium"), but please not more than once a month. Please do not declare your issue as `important` or `urgent`. ### How can I detect whether a given URL is supported by youtube-dl? For one, have a look at the [list of supported sites](docs/supportedsites.md). Note that it can sometimes happen that the site changes its URL scheme (say, from http://example.com/video/1234567 to http://example.com/v/1234567 ) and youtube-dl reports an URL of a service in that list as unsupported. In that case, simply report a bug. It is *not* possible to detect whether a URL is supported or not. That's because youtube-dl contains a generic extractor which matches **all** URLs. You may be tempted to disable, exclude, or remove the generic extractor, but the generic extractor not only allows users to extract videos from lots of websites that embed a video from another service, but may also be used to extract video from a service that it's hosting itself. Therefore, we neither recommend nor support disabling, excluding, or removing the generic extractor. If you want to find out whether a given URL is supported, simply call youtube-dl with it. If you get no videos back, chances are the URL is either not referring to a video or unsupported. You can find out which by examining the output (if you run youtube-dl on the console) or catching an `UnsupportedError` exception if you run it from a Python program. # DEVELOPER INSTRUCTIONS Most users do not need to build youtube-dl and can [download the builds](http://rg3.github.io/youtube-dl/download.html) or get them from their distribution. To run youtube-dl as a developer, you don't need to build anything either. Simply execute python -m youtube_dl To run the test, simply invoke your favorite test runner, or execute a test file directly; any of the following work: python -m unittest discover python test/test_download.py nosetests If you want to create a build of youtube-dl yourself, you'll need * python * make * pandoc * zip * nosetests ### Adding support for a new site If you want to add support for a new site, you can follow this quick list (assuming your service is called `yourextractor`): 1. [Fork this repository](https://github.com/rg3/youtube-dl/fork) 2. Check out the source code with `git clone git@github.com:YOUR_GITHUB_USERNAME/youtube-dl.git` 3. Start a new git branch with `cd youtube-dl; git checkout -b yourextractor` 4. Start with this simple template and save it to `youtube_dl/extractor/yourextractor.py`: ```python # coding: utf-8 from __future__ import unicode_literals from .common import InfoExtractor class YourExtractorIE(InfoExtractor): _VALID_URL = r'https?://(?:www\.)?yourextractor\.com/watch/(?P[0-9]+)' _TEST = { 'url': 'http://yourextractor.com/watch/42', 'md5': 'TODO: md5 sum of the first 10241 bytes of the video file (use --test)', 'info_dict': { 'id': '42', 'ext': 'mp4', 'title': 'Video title goes here', 'thumbnail': 're:^https?://.*\.jpg$', # TODO more properties, either as: # * A value # * MD5 checksum; start the string with md5: # * A regular expression; start the string with re: # * Any Python type (for example int or float) } } def _real_extract(self, url): video_id = self._match_id(url) webpage = self._download_webpage(url, video_id) # TODO more code goes here, for example ... title = self._html_search_regex(r'

(.+?)

', webpage, 'title') return { 'id': video_id, 'title': title, 'description': self._og_search_description(webpage), 'uploader': self._search_regex(r']+id="uploader"[^>]*>([^<]+)<', webpage, 'uploader', fatal=False), # TODO more properties (see youtube_dl/extractor/common.py) } ``` 5. Add an import in [`youtube_dl/extractor/__init__.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/__init__.py). 6. Run `python test/test_download.py TestDownload.test_YourExtractor`. This *should fail* at first, but you can continually re-run it until you're done. If you decide to add more than one test, then rename ``_TEST`` to ``_TESTS`` and make it into a list of dictionaries. The tests will then be named `TestDownload.test_YourExtractor`, `TestDownload.test_YourExtractor_1`, `TestDownload.test_YourExtractor_2`, etc. 7. Have a look at [`youtube_dl/extractor/common.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py) for possible helper methods and a [detailed description of what your extractor should and may return](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/common.py#L62-L200). Add tests and code for as many as you want. 8. If you can, check the code with [flake8](https://pypi.python.org/pypi/flake8). 9. When the tests pass, [add](http://git-scm.com/docs/git-add) the new files and [commit](http://git-scm.com/docs/git-commit) them and [push](http://git-scm.com/docs/git-push) the result, like this: $ git add youtube_dl/extractor/__init__.py $ git add youtube_dl/extractor/yourextractor.py $ git commit -m '[yourextractor] Add new extractor' $ git push origin yourextractor 10. Finally, [create a pull request](https://help.github.com/articles/creating-a-pull-request). We'll then review and merge it. In any case, thank you very much for your contributions! # EMBEDDING YOUTUBE-DL youtube-dl makes the best effort to be a good command-line program, and thus should be callable from any programming language. If you encounter any problems parsing its output, feel free to [create a report](https://github.com/rg3/youtube-dl/issues/new). From a Python program, you can embed youtube-dl in a more powerful fashion, like this: ```python from __future__ import unicode_literals import youtube_dl ydl_opts = {} with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc']) ``` Most likely, you'll want to use various options. For a list of what can be done, have a look at [`youtube_dl/YoutubeDL.py`](https://github.com/rg3/youtube-dl/blob/master/youtube_dl/YoutubeDL.py#L121-L269). For a start, if you want to intercept youtube-dl's output, set a `logger` object. Here's a more complete example of a program that outputs only errors (and a short message after the download is finished), and downloads/converts the video to an mp3 file: ```python from __future__ import unicode_literals import youtube_dl class MyLogger(object): def debug(self, msg): pass def warning(self, msg): pass def error(self, msg): print(msg) def my_hook(d): if d['status'] == 'finished': print('Done downloading, now converting ...') ydl_opts = { 'format': 'bestaudio/best', 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192', }], 'logger': MyLogger(), 'progress_hooks': [my_hook], } with youtube_dl.YoutubeDL(ydl_opts) as ydl: ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc']) ``` # BUGS Bugs and suggestions should be reported at: . Unless you were prompted so or there is another pertinent reason (e.g. GitHub fails to accept the bug report), please do not send bug reports via personal email. For discussions, join us in the IRC channel [#youtube-dl](irc://chat.freenode.net/#youtube-dl) on freenode ([webchat](http://webchat.freenode.net/?randomnick=1&channels=youtube-dl)). **Please include the full output of youtube-dl when run with `-v`**, i.e. add `-v` flag to your command line, copy the **whole** output and post it in the issue body wrapped in \`\`\` for better formatting. It should look similar to this: ``` $ youtube-dl -v http://www.youtube.com/watch?v=BaW_jenozKcj [debug] System config: [] [debug] User config: [] [debug] Command-line args: [u'-v', u'http://www.youtube.com/watch?v=BaW_jenozKcj'] [debug] Encodings: locale cp1251, fs mbcs, out cp866, pref cp1251 [debug] youtube-dl version 2015.12.06 [debug] Git HEAD: 135392e [debug] Python version 2.6.6 - Windows-2003Server-5.2.3790-SP2 [debug] exe versions: ffmpeg N-75573-g1d0487f, ffprobe N-75573-g1d0487f, rtmpdump 2.4 [debug] Proxy map: {} ... ``` **Do not post screenshots of verbose log only plain text is acceptable.** The output (including the first lines) contains important debugging information. Issues without the full output are often not reproducible and therefore do not get solved in short order, if ever. Please re-read your issue once again to avoid a couple of common mistakes (you can and should use this as a checklist): ### Is the description of the issue itself sufficient? We often get issue reports that we cannot really decipher. While in most cases we eventually get the required information after asking back multiple times, this poses an unnecessary drain on our resources. Many contributors, including myself, are also not native speakers, so we may misread some parts. So please elaborate on what feature you are requesting, or what bug you want to be fixed. Make sure that it's obvious - What the problem is - How it could be fixed - How your proposed solution would look like If your report is shorter than two lines, it is almost certainly missing some of these, which makes it hard for us to respond to it. We're often too polite to close the issue outright, but the missing info makes misinterpretation likely. As a commiter myself, I often get frustrated by these issues, since the only possible way for me to move forward on them is to ask for clarification over and over. For bug reports, this means that your report should contain the *complete* output of youtube-dl when called with the `-v` flag. The error message you get for (most) bugs even says so, but you would not believe how many of our bug reports do not contain this information. If your server has multiple IPs or you suspect censorship, adding `--call-home` may be a good idea to get more diagnostics. If the error is `ERROR: Unable to extract ...` and you cannot reproduce it from multiple countries, add `--dump-pages` (warning: this will yield a rather large output, redirect it to the file `log.txt` by adding `>log.txt 2>&1` to your command-line) or upload the `.dump` files you get when you add `--write-pages` [somewhere](https://gist.github.com/). **Site support requests must contain an example URL**. An example URL is a URL you might want to download, like `http://www.youtube.com/watch?v=BaW_jenozKc`. There should be an obvious video present. Except under very special circumstances, the main page of a video service (e.g. `http://www.youtube.com/`) is *not* an example URL. ### Are you using the latest version? Before reporting any issue, type `youtube-dl -U`. This should report that you're up-to-date. About 20% of the reports we receive are already fixed, but people are using outdated versions. This goes for feature requests as well. ### Is the issue already documented? Make sure that someone has not already opened the issue you're trying to open. Search at the top of the window or browse the [GitHub Issues](https://github.com/rg3/youtube-dl/search?type=Issues) of this repository. If there is an issue, feel free to write something along the lines of "This affects me as well, with version 2015.01.01. Here is some more information on the issue: ...". While some issues may be old, a new post into them often spurs rapid activity. ### Why are existing options not enough? Before requesting a new feature, please have a quick peek at [the list of supported options](https://github.com/rg3/youtube-dl/blob/master/README.md#synopsis). Many feature requests are for features that actually exist already! Please, absolutely do show off your work in the issue report and detail how the existing similar options do *not* solve your problem. ### Is there enough context in your bug report? People want to solve problems, and often think they do us a favor by breaking down their larger problems (e.g. wanting to skip already downloaded files) to a specific request (e.g. requesting us to look whether the file exists before downloading the info page). However, what often happens is that they break down the problem into two steps: One simple, and one impossible (or extremely complicated one). We are then presented with a very complicated request when the original problem could be solved far easier, e.g. by recording the downloaded video IDs in a separate file. To avoid this, you must include the greater context where it is non-obvious. In particular, every feature request that does not consist of adding support for a new site should contain a use case scenario that explains in what situation the missing feature would be useful. ### Does the issue involve one problem, and one problem only? Some of our users seem to think there is a limit of issues they can or should open. There is no limit of issues they can or should open. While it may seem appealing to be able to dump all your issues into one ticket, that means that someone who solves one of your issues cannot mark the issue as closed. Typically, reporting a bunch of issues leads to the ticket lingering since nobody wants to attack that behemoth, until someone mercifully splits the issue into multiple ones. In particular, every site support request issue should only pertain to services at one site (generally under a common domain, but always using the same backend technology). Do not request support for vimeo user videos, Whitehouse podcasts, and Google Plus pages in the same issue. Also, make sure that you don't post bug reports alongside feature requests. As a rule of thumb, a feature request does not include outputs of youtube-dl that are not immediately related to the feature at hand. Do not post reports of a network error alongside the request for a new video service. ### Is anyone going to need the feature? Only post features that you (or an incapacitated friend you can personally talk to) require. Do not post features because they seem like a good idea. If they are really useful, they will be requested by someone who requires them. ### Is your question about youtube-dl? It may sound strange, but some bug reports we receive are completely unrelated to youtube-dl and relate to a different or even the reporter's own application. Please make sure that you are actually using youtube-dl. If you are using a UI for youtube-dl, report the bug to the maintainer of the actual application providing the UI. On the other hand, if your UI for youtube-dl fails in some way you believe is related to youtube-dl, by all means, go ahead and report the bug. # COPYRIGHT youtube-dl is released into the public domain by the copyright holders. This README file was originally written by [Daniel Bolton](https://github.com/dbbolton) and is likewise released into the public domain. encuentro-5.0/external/youtube-dl/youtube_dl/0000775000175000017500000000000013144662314022534 5ustar facundofacundo00000000000000encuentro-5.0/external/youtube-dl/youtube_dl/__init__.py0000664000175000017500000004065612637746176024677 0ustar facundofacundo00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import unicode_literals __license__ = 'Public Domain' import codecs import io import os import random import sys from .options import ( parseOpts, ) from .compat import ( compat_expanduser, compat_getpass, compat_print, compat_shlex_split, workaround_optparse_bug9161, ) from .utils import ( DateRange, decodeOption, DEFAULT_OUTTMPL, DownloadError, match_filter_func, MaxDownloadsReached, preferredencoding, read_batch_urls, SameFileError, setproctitle, std_headers, write_string, ) from .update import update_self from .downloader import ( FileDownloader, ) from .extractor import gen_extractors, list_extractors from .YoutubeDL import YoutubeDL def _real_main(argv=None): # Compatibility fixes for Windows if sys.platform == 'win32': # https://github.com/rg3/youtube-dl/issues/820 codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None) workaround_optparse_bug9161() setproctitle('youtube-dl') parser, opts, args = parseOpts(argv) # Set user agent if opts.user_agent is not None: std_headers['User-Agent'] = opts.user_agent # Set referer if opts.referer is not None: std_headers['Referer'] = opts.referer # Custom HTTP headers if opts.headers is not None: for h in opts.headers: if h.find(':', 1) < 0: parser.error('wrong header formatting, it should be key:value, not "%s"' % h) key, value = h.split(':', 2) if opts.verbose: write_string('[debug] Adding header from command line option %s:%s\n' % (key, value)) std_headers[key] = value # Dump user agent if opts.dump_user_agent: compat_print(std_headers['User-Agent']) sys.exit(0) # Batch file verification batch_urls = [] if opts.batchfile is not None: try: if opts.batchfile == '-': batchfd = sys.stdin else: batchfd = io.open(opts.batchfile, 'r', encoding='utf-8', errors='ignore') batch_urls = read_batch_urls(batchfd) if opts.verbose: write_string('[debug] Batch file urls: ' + repr(batch_urls) + '\n') except IOError: sys.exit('ERROR: batch file could not be read') all_urls = batch_urls + args all_urls = [url.strip() for url in all_urls] _enc = preferredencoding() all_urls = [url.decode(_enc, 'ignore') if isinstance(url, bytes) else url for url in all_urls] if opts.list_extractors: for ie in list_extractors(opts.age_limit): compat_print(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie._WORKING else '')) matchedUrls = [url for url in all_urls if ie.suitable(url)] for mu in matchedUrls: compat_print(' ' + mu) sys.exit(0) if opts.list_extractor_descriptions: for ie in list_extractors(opts.age_limit): if not ie._WORKING: continue desc = getattr(ie, 'IE_DESC', ie.IE_NAME) if desc is False: continue if hasattr(ie, 'SEARCH_KEY'): _SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow') _COUNTS = ('', '5', '10', 'all') desc += ' (Example: "%s%s:%s" )' % (ie.SEARCH_KEY, random.choice(_COUNTS), random.choice(_SEARCHES)) compat_print(desc) sys.exit(0) # Conflicting, missing and erroneous options if opts.usenetrc and (opts.username is not None or opts.password is not None): parser.error('using .netrc conflicts with giving username/password') if opts.password is not None and opts.username is None: parser.error('account username missing\n') if opts.outtmpl is not None and (opts.usetitle or opts.autonumber or opts.useid): parser.error('using output template conflicts with using title, video ID or auto number') if opts.usetitle and opts.useid: parser.error('using title conflicts with using video ID') if opts.username is not None and opts.password is None: opts.password = compat_getpass('Type account password and press [Return]: ') if opts.ratelimit is not None: numeric_limit = FileDownloader.parse_bytes(opts.ratelimit) if numeric_limit is None: parser.error('invalid rate limit specified') opts.ratelimit = numeric_limit if opts.min_filesize is not None: numeric_limit = FileDownloader.parse_bytes(opts.min_filesize) if numeric_limit is None: parser.error('invalid min_filesize specified') opts.min_filesize = numeric_limit if opts.max_filesize is not None: numeric_limit = FileDownloader.parse_bytes(opts.max_filesize) if numeric_limit is None: parser.error('invalid max_filesize specified') opts.max_filesize = numeric_limit if opts.retries is not None: if opts.retries in ('inf', 'infinite'): opts_retries = float('inf') else: try: opts_retries = int(opts.retries) except (TypeError, ValueError): parser.error('invalid retry count specified') if opts.buffersize is not None: numeric_buffersize = FileDownloader.parse_bytes(opts.buffersize) if numeric_buffersize is None: parser.error('invalid buffer size specified') opts.buffersize = numeric_buffersize if opts.playliststart <= 0: raise ValueError('Playlist start must be positive') if opts.playlistend not in (-1, None) and opts.playlistend < opts.playliststart: raise ValueError('Playlist end must be greater than playlist start') if opts.extractaudio: if opts.audioformat not in ['best', 'aac', 'mp3', 'm4a', 'opus', 'vorbis', 'wav']: parser.error('invalid audio format specified') if opts.audioquality: opts.audioquality = opts.audioquality.strip('k').strip('K') if not opts.audioquality.isdigit(): parser.error('invalid audio quality specified') if opts.recodevideo is not None: if opts.recodevideo not in ['mp4', 'flv', 'webm', 'ogg', 'mkv', 'avi']: parser.error('invalid video recode format specified') if opts.convertsubtitles is not None: if opts.convertsubtitles not in ['srt', 'vtt', 'ass']: parser.error('invalid subtitle format specified') if opts.date is not None: date = DateRange.day(opts.date) else: date = DateRange(opts.dateafter, opts.datebefore) # Do not download videos when there are audio-only formats if opts.extractaudio and not opts.keepvideo and opts.format is None: opts.format = 'bestaudio/best' # --all-sub automatically sets --write-sub if --write-auto-sub is not given # this was the old behaviour if only --all-sub was given. if opts.allsubtitles and not opts.writeautomaticsub: opts.writesubtitles = True outtmpl = ((opts.outtmpl is not None and opts.outtmpl) or (opts.format == '-1' and opts.usetitle and '%(title)s-%(id)s-%(format)s.%(ext)s') or (opts.format == '-1' and '%(id)s-%(format)s.%(ext)s') or (opts.usetitle and opts.autonumber and '%(autonumber)s-%(title)s-%(id)s.%(ext)s') or (opts.usetitle and '%(title)s-%(id)s.%(ext)s') or (opts.useid and '%(id)s.%(ext)s') or (opts.autonumber and '%(autonumber)s-%(id)s.%(ext)s') or DEFAULT_OUTTMPL) if not os.path.splitext(outtmpl)[1] and opts.extractaudio: parser.error('Cannot download a video and extract audio into the same' ' file! Use "{0}.%(ext)s" instead of "{0}" as the output' ' template'.format(outtmpl)) any_getting = opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration or opts.dumpjson or opts.dump_single_json any_printing = opts.print_json download_archive_fn = compat_expanduser(opts.download_archive) if opts.download_archive is not None else opts.download_archive # PostProcessors postprocessors = [] # Add the metadata pp first, the other pps will copy it if opts.metafromtitle: postprocessors.append({ 'key': 'MetadataFromTitle', 'titleformat': opts.metafromtitle }) if opts.addmetadata: postprocessors.append({'key': 'FFmpegMetadata'}) if opts.extractaudio: postprocessors.append({ 'key': 'FFmpegExtractAudio', 'preferredcodec': opts.audioformat, 'preferredquality': opts.audioquality, 'nopostoverwrites': opts.nopostoverwrites, }) if opts.recodevideo: postprocessors.append({ 'key': 'FFmpegVideoConvertor', 'preferedformat': opts.recodevideo, }) if opts.convertsubtitles: postprocessors.append({ 'key': 'FFmpegSubtitlesConvertor', 'format': opts.convertsubtitles, }) if opts.embedsubtitles: postprocessors.append({ 'key': 'FFmpegEmbedSubtitle', }) if opts.xattrs: postprocessors.append({'key': 'XAttrMetadata'}) if opts.embedthumbnail: already_have_thumbnail = opts.writethumbnail or opts.write_all_thumbnails postprocessors.append({ 'key': 'EmbedThumbnail', 'already_have_thumbnail': already_have_thumbnail }) if not already_have_thumbnail: opts.writethumbnail = True # Please keep ExecAfterDownload towards the bottom as it allows the user to modify the final file in any way. # So if the user is able to remove the file before your postprocessor runs it might cause a few problems. if opts.exec_cmd: postprocessors.append({ 'key': 'ExecAfterDownload', 'exec_cmd': opts.exec_cmd, }) if opts.xattr_set_filesize: try: import xattr xattr # Confuse flake8 except ImportError: parser.error('setting filesize xattr requested but python-xattr is not available') external_downloader_args = None if opts.external_downloader_args: external_downloader_args = compat_shlex_split(opts.external_downloader_args) postprocessor_args = None if opts.postprocessor_args: postprocessor_args = compat_shlex_split(opts.postprocessor_args) match_filter = ( None if opts.match_filter is None else match_filter_func(opts.match_filter)) ydl_opts = { 'usenetrc': opts.usenetrc, 'username': opts.username, 'password': opts.password, 'twofactor': opts.twofactor, 'videopassword': opts.videopassword, 'quiet': (opts.quiet or any_getting or any_printing), 'no_warnings': opts.no_warnings, 'forceurl': opts.geturl, 'forcetitle': opts.gettitle, 'forceid': opts.getid, 'forcethumbnail': opts.getthumbnail, 'forcedescription': opts.getdescription, 'forceduration': opts.getduration, 'forcefilename': opts.getfilename, 'forceformat': opts.getformat, 'forcejson': opts.dumpjson or opts.print_json, 'dump_single_json': opts.dump_single_json, 'simulate': opts.simulate or any_getting, 'skip_download': opts.skip_download, 'format': opts.format, 'listformats': opts.listformats, 'outtmpl': outtmpl, 'autonumber_size': opts.autonumber_size, 'restrictfilenames': opts.restrictfilenames, 'ignoreerrors': opts.ignoreerrors, 'force_generic_extractor': opts.force_generic_extractor, 'ratelimit': opts.ratelimit, 'nooverwrites': opts.nooverwrites, 'retries': opts_retries, 'buffersize': opts.buffersize, 'noresizebuffer': opts.noresizebuffer, 'continuedl': opts.continue_dl, 'noprogress': opts.noprogress, 'progress_with_newline': opts.progress_with_newline, 'playliststart': opts.playliststart, 'playlistend': opts.playlistend, 'playlistreverse': opts.playlist_reverse, 'noplaylist': opts.noplaylist, 'logtostderr': opts.outtmpl == '-', 'consoletitle': opts.consoletitle, 'nopart': opts.nopart, 'updatetime': opts.updatetime, 'writedescription': opts.writedescription, 'writeannotations': opts.writeannotations, 'writeinfojson': opts.writeinfojson, 'writethumbnail': opts.writethumbnail, 'write_all_thumbnails': opts.write_all_thumbnails, 'writesubtitles': opts.writesubtitles, 'writeautomaticsub': opts.writeautomaticsub, 'allsubtitles': opts.allsubtitles, 'listsubtitles': opts.listsubtitles, 'subtitlesformat': opts.subtitlesformat, 'subtitleslangs': opts.subtitleslangs, 'matchtitle': decodeOption(opts.matchtitle), 'rejecttitle': decodeOption(opts.rejecttitle), 'max_downloads': opts.max_downloads, 'prefer_free_formats': opts.prefer_free_formats, 'verbose': opts.verbose, 'dump_intermediate_pages': opts.dump_intermediate_pages, 'write_pages': opts.write_pages, 'test': opts.test, 'keepvideo': opts.keepvideo, 'min_filesize': opts.min_filesize, 'max_filesize': opts.max_filesize, 'min_views': opts.min_views, 'max_views': opts.max_views, 'daterange': date, 'cachedir': opts.cachedir, 'youtube_print_sig_code': opts.youtube_print_sig_code, 'age_limit': opts.age_limit, 'download_archive': download_archive_fn, 'cookiefile': opts.cookiefile, 'nocheckcertificate': opts.no_check_certificate, 'prefer_insecure': opts.prefer_insecure, 'proxy': opts.proxy, 'socket_timeout': opts.socket_timeout, 'bidi_workaround': opts.bidi_workaround, 'debug_printtraffic': opts.debug_printtraffic, 'prefer_ffmpeg': opts.prefer_ffmpeg, 'include_ads': opts.include_ads, 'default_search': opts.default_search, 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest, 'encoding': opts.encoding, 'extract_flat': opts.extract_flat, 'merge_output_format': opts.merge_output_format, 'postprocessors': postprocessors, 'fixup': opts.fixup, 'source_address': opts.source_address, 'call_home': opts.call_home, 'sleep_interval': opts.sleep_interval, 'external_downloader': opts.external_downloader, 'list_thumbnails': opts.list_thumbnails, 'playlist_items': opts.playlist_items, 'xattr_set_filesize': opts.xattr_set_filesize, 'match_filter': match_filter, 'no_color': opts.no_color, 'ffmpeg_location': opts.ffmpeg_location, 'hls_prefer_native': opts.hls_prefer_native, 'external_downloader_args': external_downloader_args, 'postprocessor_args': postprocessor_args, 'cn_verification_proxy': opts.cn_verification_proxy, } with YoutubeDL(ydl_opts) as ydl: # Update version if opts.update_self: update_self(ydl.to_screen, opts.verbose, ydl._opener) # Remove cache dir if opts.rm_cachedir: ydl.cache.remove() # Maybe do nothing if (len(all_urls) < 1) and (opts.load_info_filename is None): if opts.update_self or opts.rm_cachedir: sys.exit() ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv) parser.error( 'You must provide at least one URL.\n' 'Type youtube-dl --help to see a list of all options.') try: if opts.load_info_filename is not None: retcode = ydl.download_with_info_file(opts.load_info_filename) else: retcode = ydl.download(all_urls) except MaxDownloadsReached: ydl.to_screen('--max-download limit reached, aborting.') retcode = 101 sys.exit(retcode) def main(argv=None): try: _real_main(argv) except DownloadError: sys.exit(1) except SameFileError: sys.exit('ERROR: fixed output name but more than one file to download') except KeyboardInterrupt: sys.exit('\nERROR: Interrupted by user') __all__ = ['main', 'YoutubeDL', 'gen_extractors', 'list_extractors'] encuentro-5.0/external/youtube-dl/youtube_dl/cache.py0000664000175000017500000000564412637746176024201 0ustar facundofacundo00000000000000from __future__ import unicode_literals import errno import io import json import os import re import shutil import traceback from .compat import compat_expanduser, compat_getenv from .utils import write_json_file class Cache(object): def __init__(self, ydl): self._ydl = ydl def _get_root_dir(self): res = self._ydl.params.get('cachedir') if res is None: cache_root = compat_getenv('XDG_CACHE_HOME', '~/.cache') res = os.path.join(cache_root, 'youtube-dl') return compat_expanduser(res) def _get_cache_fn(self, section, key, dtype): assert re.match(r'^[a-zA-Z0-9_.-]+$', section), \ 'invalid section %r' % section assert re.match(r'^[a-zA-Z0-9_.-]+$', key), 'invalid key %r' % key return os.path.join( self._get_root_dir(), section, '%s.%s' % (key, dtype)) @property def enabled(self): return self._ydl.params.get('cachedir') is not False def store(self, section, key, data, dtype='json'): assert dtype in ('json',) if not self.enabled: return fn = self._get_cache_fn(section, key, dtype) try: try: os.makedirs(os.path.dirname(fn)) except OSError as ose: if ose.errno != errno.EEXIST: raise write_json_file(data, fn) except Exception: tb = traceback.format_exc() self._ydl.report_warning( 'Writing cache to %r failed: %s' % (fn, tb)) def load(self, section, key, dtype='json', default=None): assert dtype in ('json',) if not self.enabled: return default cache_fn = self._get_cache_fn(section, key, dtype) try: try: with io.open(cache_fn, 'r', encoding='utf-8') as cachef: return json.load(cachef) except ValueError: try: file_size = os.path.getsize(cache_fn) except (OSError, IOError) as oe: file_size = str(oe) self._ydl.report_warning( 'Cache retrieval from %s failed (%s)' % (cache_fn, file_size)) except IOError: pass # No cache available return default def remove(self): if not self.enabled: self._ydl.to_screen('Cache is disabled (Did you combine --no-cache-dir and --rm-cache-dir?)') return cachedir = self._get_root_dir() if not any((term in cachedir) for term in ('cache', 'tmp')): raise Exception('Not removing directory %s - this does not look like a cache dir' % cachedir) self._ydl.to_screen( 'Removing cache dir %s .' % cachedir, skip_eol=True) if os.path.exists(cachedir): self._ydl.to_screen('.', skip_eol=True) shutil.rmtree(cachedir) self._ydl.to_screen('.') encuentro-5.0/external/youtube-dl/youtube_dl/utils.py0000664000175000017500000023124712637746176024276 0ustar facundofacundo00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import unicode_literals import base64 import calendar import codecs import contextlib import ctypes import datetime import email.utils import errno import functools import gzip import itertools import io import json import locale import math import operator import os import pipes import platform import re import ssl import socket import struct import subprocess import sys import tempfile import traceback import xml.etree.ElementTree import zlib from .compat import ( compat_basestring, compat_chr, compat_etree_fromstring, compat_html_entities, compat_http_client, compat_kwargs, compat_parse_qs, compat_socket_create_connection, compat_str, compat_urllib_error, compat_urllib_parse, compat_urllib_parse_urlparse, compat_urllib_request, compat_urlparse, shlex_quote, ) # This is not clearly defined otherwise compiled_regex_type = type(re.compile('')) std_headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/20.0 (Chrome)', 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-us,en;q=0.5', } NO_DEFAULT = object() ENGLISH_MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] def preferredencoding(): """Get preferred encoding. Returns the best encoding scheme for the system, based on locale.getpreferredencoding() and some further tweaks. """ try: pref = locale.getpreferredencoding() 'TEST'.encode(pref) except Exception: pref = 'UTF-8' return pref def write_json_file(obj, fn): """ Encode obj as JSON and write it to fn, atomically if possible """ fn = encodeFilename(fn) if sys.version_info < (3, 0) and sys.platform != 'win32': encoding = get_filesystem_encoding() # os.path.basename returns a bytes object, but NamedTemporaryFile # will fail if the filename contains non ascii characters unless we # use a unicode object path_basename = lambda f: os.path.basename(fn).decode(encoding) # the same for os.path.dirname path_dirname = lambda f: os.path.dirname(fn).decode(encoding) else: path_basename = os.path.basename path_dirname = os.path.dirname args = { 'suffix': '.tmp', 'prefix': path_basename(fn) + '.', 'dir': path_dirname(fn), 'delete': False, } # In Python 2.x, json.dump expects a bytestream. # In Python 3.x, it writes to a character stream if sys.version_info < (3, 0): args['mode'] = 'wb' else: args.update({ 'mode': 'w', 'encoding': 'utf-8', }) tf = tempfile.NamedTemporaryFile(**compat_kwargs(args)) try: with tf: json.dump(obj, tf) if sys.platform == 'win32': # Need to remove existing file on Windows, else os.rename raises # WindowsError or FileExistsError. try: os.unlink(fn) except OSError: pass os.rename(tf.name, fn) except Exception: try: os.remove(tf.name) except OSError: pass raise if sys.version_info >= (2, 7): def find_xpath_attr(node, xpath, key, val=None): """ Find the xpath xpath[@key=val] """ assert re.match(r'^[a-zA-Z_-]+$', key) if val: assert re.match(r'^[a-zA-Z0-9@\s:._-]*$', val) expr = xpath + ('[@%s]' % key if val is None else "[@%s='%s']" % (key, val)) return node.find(expr) else: def find_xpath_attr(node, xpath, key, val=None): # Here comes the crazy part: In 2.6, if the xpath is a unicode, # .//node does not match if a node is a direct child of . ! if isinstance(xpath, compat_str): xpath = xpath.encode('ascii') for f in node.findall(xpath): if key not in f.attrib: continue if val is None or f.attrib.get(key) == val: return f return None # On python2.6 the xml.etree.ElementTree.Element methods don't support # the namespace parameter def xpath_with_ns(path, ns_map): components = [c.split(':') for c in path.split('/')] replaced = [] for c in components: if len(c) == 1: replaced.append(c[0]) else: ns, tag = c replaced.append('{%s}%s' % (ns_map[ns], tag)) return '/'.join(replaced) def xpath_element(node, xpath, name=None, fatal=False, default=NO_DEFAULT): def _find_xpath(xpath): if sys.version_info < (2, 7): # Crazy 2.6 xpath = xpath.encode('ascii') return node.find(xpath) if isinstance(xpath, (str, compat_str)): n = _find_xpath(xpath) else: for xp in xpath: n = _find_xpath(xp) if n is not None: break if n is None: if default is not NO_DEFAULT: return default elif fatal: name = xpath if name is None else name raise ExtractorError('Could not find XML element %s' % name) else: return None return n def xpath_text(node, xpath, name=None, fatal=False, default=NO_DEFAULT): n = xpath_element(node, xpath, name, fatal=fatal, default=default) if n is None or n == default: return n if n.text is None: if default is not NO_DEFAULT: return default elif fatal: name = xpath if name is None else name raise ExtractorError('Could not find XML element\'s text %s' % name) else: return None return n.text def xpath_attr(node, xpath, key, name=None, fatal=False, default=NO_DEFAULT): n = find_xpath_attr(node, xpath, key) if n is None: if default is not NO_DEFAULT: return default elif fatal: name = '%s[@%s]' % (xpath, key) if name is None else name raise ExtractorError('Could not find XML attribute %s' % name) else: return None return n.attrib[key] def get_element_by_id(id, html): """Return the content of the tag with the specified ID in the passed HTML document""" return get_element_by_attribute("id", id, html) def get_element_by_attribute(attribute, value, html): """Return the content of the tag with the specified attribute in the passed HTML document""" m = re.search(r'''(?xs) <([a-zA-Z0-9:._-]+) (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*? \s+%s=['"]?%s['"]? (?:\s+[a-zA-Z0-9:._-]+(?:=[a-zA-Z0-9:._-]+|="[^"]+"|='[^']+'))*? \s*> (?P.*?) ''' % (re.escape(attribute), re.escape(value)), html) if not m: return None res = m.group('content') if res.startswith('"') or res.startswith("'"): res = res[1:-1] return unescapeHTML(res) def clean_html(html): """Clean an HTML snippet into a readable string""" if html is None: # Convenience for sanitizing descriptions etc. return html # Newline vs
html = html.replace('\n', ' ') html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html) html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html) # Strip html tags html = re.sub('<.*?>', '', html) # Replace html entities html = unescapeHTML(html) return html.strip() def sanitize_open(filename, open_mode): """Try to open the given filename, and slightly tweak it if this fails. Attempts to open the given filename. If this fails, it tries to change the filename slightly, step by step, until it's either able to open it or it fails and raises a final exception, like the standard open() function. It returns the tuple (stream, definitive_file_name). """ try: if filename == '-': if sys.platform == 'win32': import msvcrt msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename) stream = open(encodeFilename(filename), open_mode) return (stream, filename) except (IOError, OSError) as err: if err.errno in (errno.EACCES,): raise # In case of error, try to remove win32 forbidden chars alt_filename = sanitize_path(filename) if alt_filename == filename: raise else: # An exception here should be caught in the caller stream = open(encodeFilename(alt_filename), open_mode) return (stream, alt_filename) def timeconvert(timestr): """Convert RFC 2822 defined time string into system timestamp""" timestamp = None timetuple = email.utils.parsedate_tz(timestr) if timetuple is not None: timestamp = email.utils.mktime_tz(timetuple) return timestamp def sanitize_filename(s, restricted=False, is_id=False): """Sanitizes a string so it could be used as part of a filename. If restricted is set, use a stricter subset of allowed characters. Set is_id if this is not an arbitrary string, but an ID that should be kept if possible """ def replace_insane(char): if char == '?' or ord(char) < 32 or ord(char) == 127: return '' elif char == '"': return '' if restricted else '\'' elif char == ':': return '_-' if restricted else ' -' elif char in '\\/|*<>': return '_' if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()): return '_' if restricted and ord(char) > 127: return '_' return char # Handle timestamps s = re.sub(r'[0-9]+(?::[0-9]+)+', lambda m: m.group(0).replace(':', '_'), s) result = ''.join(map(replace_insane, s)) if not is_id: while '__' in result: result = result.replace('__', '_') result = result.strip('_') # Common case of "Foreign band name - English song title" if restricted and result.startswith('-_'): result = result[2:] if result.startswith('-'): result = '_' + result[len('-'):] result = result.lstrip('.') if not result: result = '_' return result def sanitize_path(s): """Sanitizes and normalizes path on Windows""" if sys.platform != 'win32': return s drive_or_unc, _ = os.path.splitdrive(s) if sys.version_info < (2, 7) and not drive_or_unc: drive_or_unc, _ = os.path.splitunc(s) norm_path = os.path.normpath(remove_start(s, drive_or_unc)).split(os.path.sep) if drive_or_unc: norm_path.pop(0) sanitized_path = [ path_part if path_part in ['.', '..'] else re.sub('(?:[/<>:"\\|\\\\?\\*]|[\s.]$)', '#', path_part) for path_part in norm_path] if drive_or_unc: sanitized_path.insert(0, drive_or_unc + os.path.sep) return os.path.join(*sanitized_path) # Prepend protocol-less URLs with `http:` scheme in order to mitigate the number of # unwanted failures due to missing protocol def sanitized_Request(url, *args, **kwargs): return compat_urllib_request.Request( 'http:%s' % url if url.startswith('//') else url, *args, **kwargs) def orderedSet(iterable): """ Remove all duplicates from the input iterable """ res = [] for el in iterable: if el not in res: res.append(el) return res def _htmlentity_transform(entity): """Transforms an HTML entity to a character.""" # Known non-numeric HTML entity if entity in compat_html_entities.name2codepoint: return compat_chr(compat_html_entities.name2codepoint[entity]) mobj = re.match(r'#(x[0-9a-fA-F]+|[0-9]+)', entity) if mobj is not None: numstr = mobj.group(1) if numstr.startswith('x'): base = 16 numstr = '0%s' % numstr else: base = 10 # See https://github.com/rg3/youtube-dl/issues/7518 try: return compat_chr(int(numstr, base)) except ValueError: pass # Unknown entity in name, return its literal representation return '&%s;' % entity def unescapeHTML(s): if s is None: return None assert type(s) == compat_str return re.sub( r'&([^;]+);', lambda m: _htmlentity_transform(m.group(1)), s) def get_subprocess_encoding(): if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5: # For subprocess calls, encode with locale encoding # Refer to http://stackoverflow.com/a/9951851/35070 encoding = preferredencoding() else: encoding = sys.getfilesystemencoding() if encoding is None: encoding = 'utf-8' return encoding def encodeFilename(s, for_subprocess=False): """ @param s The name of the file """ assert type(s) == compat_str # Python 3 has a Unicode API if sys.version_info >= (3, 0): return s # Pass '' directly to use Unicode APIs on Windows 2000 and up # (Detecting Windows NT 4 is tricky because 'major >= 4' would # match Windows 9x series as well. Besides, NT 4 is obsolete.) if not for_subprocess and sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5: return s return s.encode(get_subprocess_encoding(), 'ignore') def decodeFilename(b, for_subprocess=False): if sys.version_info >= (3, 0): return b if not isinstance(b, bytes): return b return b.decode(get_subprocess_encoding(), 'ignore') def encodeArgument(s): if not isinstance(s, compat_str): # Legacy code that uses byte strings # Uncomment the following line after fixing all post processors # assert False, 'Internal error: %r should be of type %r, is %r' % (s, compat_str, type(s)) s = s.decode('ascii') return encodeFilename(s, True) def decodeArgument(b): return decodeFilename(b, True) def decodeOption(optval): if optval is None: return optval if isinstance(optval, bytes): optval = optval.decode(preferredencoding()) assert isinstance(optval, compat_str) return optval def formatSeconds(secs): if secs > 3600: return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60) elif secs > 60: return '%d:%02d' % (secs // 60, secs % 60) else: return '%d' % secs def make_HTTPS_handler(params, **kwargs): opts_no_check_certificate = params.get('nocheckcertificate', False) if hasattr(ssl, 'create_default_context'): # Python >= 3.4 or 2.7.9 context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) if opts_no_check_certificate: context.check_hostname = False context.verify_mode = ssl.CERT_NONE try: return YoutubeDLHTTPSHandler(params, context=context, **kwargs) except TypeError: # Python 2.7.8 # (create_default_context present but HTTPSHandler has no context=) pass if sys.version_info < (3, 2): return YoutubeDLHTTPSHandler(params, **kwargs) else: # Python < 3.4 context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) context.verify_mode = (ssl.CERT_NONE if opts_no_check_certificate else ssl.CERT_REQUIRED) context.set_default_verify_paths() return YoutubeDLHTTPSHandler(params, context=context, **kwargs) def bug_reports_message(): if ytdl_is_updateable(): update_cmd = 'type youtube-dl -U to update' else: update_cmd = 'see https://yt-dl.org/update on how to update' msg = '; please report this issue on https://yt-dl.org/bug .' msg += ' Make sure you are using the latest version; %s.' % update_cmd msg += ' Be sure to call youtube-dl with the --verbose flag and include its complete output.' return msg class ExtractorError(Exception): """Error during info extraction.""" def __init__(self, msg, tb=None, expected=False, cause=None, video_id=None): """ tb, if given, is the original traceback (so that it can be printed out). If expected is set, this is a normal error message and most likely not a bug in youtube-dl. """ if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError): expected = True if video_id is not None: msg = video_id + ': ' + msg if cause: msg += ' (caused by %r)' % cause if not expected: msg += bug_reports_message() super(ExtractorError, self).__init__(msg) self.traceback = tb self.exc_info = sys.exc_info() # preserve original exception self.cause = cause self.video_id = video_id def format_traceback(self): if self.traceback is None: return None return ''.join(traceback.format_tb(self.traceback)) class UnsupportedError(ExtractorError): def __init__(self, url): super(UnsupportedError, self).__init__( 'Unsupported URL: %s' % url, expected=True) self.url = url class RegexNotFoundError(ExtractorError): """Error when a regex didn't match""" pass class DownloadError(Exception): """Download Error exception. This exception may be thrown by FileDownloader objects if they are not configured to continue on errors. They will contain the appropriate error message. """ def __init__(self, msg, exc_info=None): """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """ super(DownloadError, self).__init__(msg) self.exc_info = exc_info class SameFileError(Exception): """Same File exception. This exception will be thrown by FileDownloader objects if they detect multiple files would have to be downloaded to the same file on disk. """ pass class PostProcessingError(Exception): """Post Processing exception. This exception may be raised by PostProcessor's .run() method to indicate an error in the postprocessing task. """ def __init__(self, msg): self.msg = msg class MaxDownloadsReached(Exception): """ --max-downloads limit has been reached. """ pass class UnavailableVideoError(Exception): """Unavailable Format exception. This exception will be thrown when a video is requested in a format that is not available for that video. """ pass class ContentTooShortError(Exception): """Content Too Short exception. This exception may be raised by FileDownloader objects when a file they download is too small for what the server announced first, indicating the connection was probably interrupted. """ def __init__(self, downloaded, expected): # Both in bytes self.downloaded = downloaded self.expected = expected def _create_http_connection(ydl_handler, http_class, is_https, *args, **kwargs): # Working around python 2 bug (see http://bugs.python.org/issue17849) by limiting # expected HTTP responses to meet HTTP/1.0 or later (see also # https://github.com/rg3/youtube-dl/issues/6727) if sys.version_info < (3, 0): kwargs[b'strict'] = True hc = http_class(*args, **kwargs) source_address = ydl_handler._params.get('source_address') if source_address is not None: sa = (source_address, 0) if hasattr(hc, 'source_address'): # Python 2.7+ hc.source_address = sa else: # Python 2.6 def _hc_connect(self, *args, **kwargs): sock = compat_socket_create_connection( (self.host, self.port), self.timeout, sa) if is_https: self.sock = ssl.wrap_socket( sock, self.key_file, self.cert_file, ssl_version=ssl.PROTOCOL_TLSv1) else: self.sock = sock hc.connect = functools.partial(_hc_connect, hc) return hc def handle_youtubedl_headers(headers): filtered_headers = headers if 'Youtubedl-no-compression' in filtered_headers: filtered_headers = dict((k, v) for k, v in filtered_headers.items() if k.lower() != 'accept-encoding') del filtered_headers['Youtubedl-no-compression'] return filtered_headers class YoutubeDLHandler(compat_urllib_request.HTTPHandler): """Handler for HTTP requests and responses. This class, when installed with an OpenerDirector, automatically adds the standard headers to every HTTP request and handles gzipped and deflated responses from web servers. If compression is to be avoided in a particular request, the original request in the program code only has to include the HTTP header "Youtubedl-no-compression", which will be removed before making the real request. Part of this code was copied from: http://techknack.net/python-urllib2-handlers/ Andrew Rowls, the author of that code, agreed to release it to the public domain. """ def __init__(self, params, *args, **kwargs): compat_urllib_request.HTTPHandler.__init__(self, *args, **kwargs) self._params = params def http_open(self, req): return self.do_open(functools.partial( _create_http_connection, self, compat_http_client.HTTPConnection, False), req) @staticmethod def deflate(data): try: return zlib.decompress(data, -zlib.MAX_WBITS) except zlib.error: return zlib.decompress(data) @staticmethod def addinfourl_wrapper(stream, headers, url, code): if hasattr(compat_urllib_request.addinfourl, 'getcode'): return compat_urllib_request.addinfourl(stream, headers, url, code) ret = compat_urllib_request.addinfourl(stream, headers, url) ret.code = code return ret def http_request(self, req): # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not # always respected by websites, some tend to give out URLs with non percent-encoded # non-ASCII characters (see telemb.py, ard.py [#3412]) # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991) # To work around aforementioned issue we will replace request's original URL with # percent-encoded one # Since redirects are also affected (e.g. http://www.southpark.de/alle-episoden/s18e09) # the code of this workaround has been moved here from YoutubeDL.urlopen() url = req.get_full_url() url_escaped = escape_url(url) # Substitute URL if any change after escaping if url != url_escaped: req_type = HEADRequest if req.get_method() == 'HEAD' else compat_urllib_request.Request new_req = req_type( url_escaped, data=req.data, headers=req.headers, origin_req_host=req.origin_req_host, unverifiable=req.unverifiable) new_req.timeout = req.timeout req = new_req for h, v in std_headers.items(): # Capitalize is needed because of Python bug 2275: http://bugs.python.org/issue2275 # The dict keys are capitalized because of this bug by urllib if h.capitalize() not in req.headers: req.add_header(h, v) req.headers = handle_youtubedl_headers(req.headers) if sys.version_info < (2, 7) and '#' in req.get_full_url(): # Python 2.6 is brain-dead when it comes to fragments req._Request__original = req._Request__original.partition('#')[0] req._Request__r_type = req._Request__r_type.partition('#')[0] return req def http_response(self, req, resp): old_resp = resp # gzip if resp.headers.get('Content-encoding', '') == 'gzip': content = resp.read() gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb') try: uncompressed = io.BytesIO(gz.read()) except IOError as original_ioerror: # There may be junk add the end of the file # See http://stackoverflow.com/q/4928560/35070 for details for i in range(1, 1024): try: gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb') uncompressed = io.BytesIO(gz.read()) except IOError: continue break else: raise original_ioerror resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code) resp.msg = old_resp.msg # deflate if resp.headers.get('Content-encoding', '') == 'deflate': gz = io.BytesIO(self.deflate(resp.read())) resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code) resp.msg = old_resp.msg # Percent-encode redirect URL of Location HTTP header to satisfy RFC 3986 (see # https://github.com/rg3/youtube-dl/issues/6457). if 300 <= resp.code < 400: location = resp.headers.get('Location') if location: # As of RFC 2616 default charset is iso-8859-1 that is respected by python 3 if sys.version_info >= (3, 0): location = location.encode('iso-8859-1').decode('utf-8') location_escaped = escape_url(location) if location != location_escaped: del resp.headers['Location'] resp.headers['Location'] = location_escaped return resp https_request = http_request https_response = http_response class YoutubeDLHTTPSHandler(compat_urllib_request.HTTPSHandler): def __init__(self, params, https_conn_class=None, *args, **kwargs): compat_urllib_request.HTTPSHandler.__init__(self, *args, **kwargs) self._https_conn_class = https_conn_class or compat_http_client.HTTPSConnection self._params = params def https_open(self, req): kwargs = {} if hasattr(self, '_context'): # python > 2.6 kwargs['context'] = self._context if hasattr(self, '_check_hostname'): # python 3.x kwargs['check_hostname'] = self._check_hostname return self.do_open(functools.partial( _create_http_connection, self, self._https_conn_class, True), req, **kwargs) class YoutubeDLCookieProcessor(compat_urllib_request.HTTPCookieProcessor): def __init__(self, cookiejar=None): compat_urllib_request.HTTPCookieProcessor.__init__(self, cookiejar) def http_response(self, request, response): # Python 2 will choke on next HTTP request in row if there are non-ASCII # characters in Set-Cookie HTTP header of last response (see # https://github.com/rg3/youtube-dl/issues/6769). # In order to at least prevent crashing we will percent encode Set-Cookie # header before HTTPCookieProcessor starts processing it. # if sys.version_info < (3, 0) and response.headers: # for set_cookie_header in ('Set-Cookie', 'Set-Cookie2'): # set_cookie = response.headers.get(set_cookie_header) # if set_cookie: # set_cookie_escaped = compat_urllib_parse.quote(set_cookie, b"%/;:@&=+$,!~*'()?#[] ") # if set_cookie != set_cookie_escaped: # del response.headers[set_cookie_header] # response.headers[set_cookie_header] = set_cookie_escaped return compat_urllib_request.HTTPCookieProcessor.http_response(self, request, response) https_request = compat_urllib_request.HTTPCookieProcessor.http_request https_response = http_response def parse_iso8601(date_str, delimiter='T', timezone=None): """ Return a UNIX timestamp from the given date """ if date_str is None: return None date_str = re.sub(r'\.[0-9]+', '', date_str) if timezone is None: m = re.search( r'(?:Z$| ?(?P\+|-)(?P[0-9]{2}):?(?P[0-9]{2})$)', date_str) if not m: timezone = datetime.timedelta() else: date_str = date_str[:-len(m.group(0))] if not m.group('sign'): timezone = datetime.timedelta() else: sign = 1 if m.group('sign') == '+' else -1 timezone = datetime.timedelta( hours=sign * int(m.group('hours')), minutes=sign * int(m.group('minutes'))) try: date_format = '%Y-%m-%d{0}%H:%M:%S'.format(delimiter) dt = datetime.datetime.strptime(date_str, date_format) - timezone return calendar.timegm(dt.timetuple()) except ValueError: pass def unified_strdate(date_str, day_first=True): """Return a string with the date in the format YYYYMMDD""" if date_str is None: return None upload_date = None # Replace commas date_str = date_str.replace(',', ' ') # %z (UTC offset) is only supported in python>=3.2 if not re.match(r'^[0-9]{1,2}-[0-9]{1,2}-[0-9]{4}$', date_str): date_str = re.sub(r' ?(\+|-)[0-9]{2}:?[0-9]{2}$', '', date_str) # Remove AM/PM + timezone date_str = re.sub(r'(?i)\s*(?:AM|PM)(?:\s+[A-Z]+)?', '', date_str) format_expressions = [ '%d %B %Y', '%d %b %Y', '%B %d %Y', '%b %d %Y', '%b %dst %Y %I:%M%p', '%b %dnd %Y %I:%M%p', '%b %dth %Y %I:%M%p', '%Y %m %d', '%Y-%m-%d', '%Y/%m/%d', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S.%f', '%d.%m.%Y %H:%M', '%d.%m.%Y %H.%M', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%S.%f0Z', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S.%f', '%Y-%m-%dT%H:%M', ] if day_first: format_expressions.extend([ '%d-%m-%Y', '%d.%m.%Y', '%d/%m/%Y', '%d/%m/%y', '%d/%m/%Y %H:%M:%S', ]) else: format_expressions.extend([ '%m-%d-%Y', '%m.%d.%Y', '%m/%d/%Y', '%m/%d/%y', '%m/%d/%Y %H:%M:%S', ]) for expression in format_expressions: try: upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d') except ValueError: pass if upload_date is None: timetuple = email.utils.parsedate_tz(date_str) if timetuple: upload_date = datetime.datetime(*timetuple[:6]).strftime('%Y%m%d') if upload_date is not None: return compat_str(upload_date) def determine_ext(url, default_ext='unknown_video'): if url is None: return default_ext guess = url.partition('?')[0].rpartition('.')[2] if re.match(r'^[A-Za-z0-9]+$', guess): return guess elif guess.rstrip('/') in ( 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'aac', 'flv', 'f4v', 'f4a', 'f4b', 'webm', 'ogg', 'ogv', 'oga', 'ogx', 'spx', 'opus', 'mkv', 'mka', 'mk3d', 'avi', 'divx', 'mov', 'asf', 'wmv', 'wma', '3gp', '3g2', 'mp3', 'flac', 'ape', 'wav', 'f4f', 'f4m', 'm3u8', 'smil'): return guess.rstrip('/') else: return default_ext def subtitles_filename(filename, sub_lang, sub_format): return filename.rsplit('.', 1)[0] + '.' + sub_lang + '.' + sub_format def date_from_str(date_str): """ Return a datetime object from a string in the format YYYYMMDD or (now|today)[+-][0-9](day|week|month|year)(s)?""" today = datetime.date.today() if date_str in ('now', 'today'): return today if date_str == 'yesterday': return today - datetime.timedelta(days=1) match = re.match('(now|today)(?P[+-])(?P