commit 6645ba8899c42fa3ea6d9b0397ccd5fcdd348e09 Author: xavierxross Date: Fri Mar 5 19:09:57 2021 +0530 Boom diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e326d6c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:20.04 + +COPY run.sh requirements.txt testwatermark.jpg /app/ +COPY lazyleech /app/lazyleech/ +RUN apt update && apt install -y --no-install-recommends python3 python3-pip ffmpeg aria2 file && rm -rf /var/lib/apt/lists/* +RUN pip3 install -r /app/requirements.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/app.json b/app.json new file mode 100644 index 0000000..c65664a --- /dev/null +++ b/app.json @@ -0,0 +1,40 @@ +{ + "env": { + "API_ID": { + "description": "https://my.telegram.org" + }, + "API_HASH": { + "description": "https://my.telegram.org" + }, + "BOT_TOKEN": { + "description": "https://t.me/BotFather" + }, + "TESTMODE": { + "description": "Use Telegram's test servers", + "value": "0", + "required": false + }, + "EVERYONE_CHATS": { + "description": "Chats for everyone, space seperated, IDs only" + }, + "ADMIN_CHATS": { + "description": "Chats for admins, space seperated, IDs only" + }, + "PROGRESS_UPDATE_DELAY": { + "description": "Delay for progress and other things", + "value": "10", + "required": false + }, + "MAGNET_TIMEOUT": { + "description": "Timeout for magnets", + "value": "60", + "required": false + }, + "LEECH_TIMEOUT": { + "description": "Timeout for leeches", + "value": "300", + "required": false + } + }, + "stack": "container" +} \ No newline at end of file diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 0000000..12a78fe --- /dev/null +++ b/heroku.yml @@ -0,0 +1,5 @@ +build: + docker: + worker: Dockerfile +run: + worker: sh /app/run.sh \ No newline at end of file diff --git a/lazyleech.session b/lazyleech.session new file mode 100644 index 0000000..1a16450 Binary files /dev/null and b/lazyleech.session differ diff --git a/lazyleech/__main__.py b/lazyleech/__main__.py new file mode 100644 index 0000000..b150c43 --- /dev/null +++ b/lazyleech/__main__.py @@ -0,0 +1,48 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +import logging +import traceback +from pyrogram import idle +from . import app, ADMIN_CHATS, preserved_logs +from .utils.upload_worker import upload_worker + +logging.basicConfig(level=logging.INFO) +logging.getLogger('pyrogram.syncer').setLevel(logging.WARNING) + +async def main(): + async def _autorestart_worker(): + while True: + try: + await upload_worker() + except Exception as ex: + preserved_logs.append(ex) + logging.exception('upload worker commited suicide') + tb = traceback.format_exc() + for i in ADMIN_CHATS: + try: + await app.send_message(i, 'upload worker commited suicide') + await app.send_message(i, tb, parse_mode=None) + except Exception: + logging.exception('failed %s', i) + tb = traceback.format_exc() + asyncio.create_task(_autorestart_worker()) + await app.start() + await idle() + await app.stop() + +app.loop.run_until_complete(main()) diff --git a/lazyleech/__pycache__/__init__.cpython-38.pyc b/lazyleech/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..24d253f Binary files /dev/null and b/lazyleech/__pycache__/__init__.cpython-38.pyc differ diff --git a/lazyleech/__pycache__/__main__.cpython-38.pyc b/lazyleech/__pycache__/__main__.cpython-38.pyc new file mode 100644 index 0000000..28d0990 Binary files /dev/null and b/lazyleech/__pycache__/__main__.cpython-38.pyc differ diff --git a/lazyleech/_init_.py b/lazyleech/_init_.py new file mode 100644 index 0000000..f790518 --- /dev/null +++ b/lazyleech/_init_.py @@ -0,0 +1,44 @@ +import os +import logging +import aiohttp +from io import BytesIO, StringIO +from pyrogram import Client + +API_ID = os.environ.get('API_ID') +API_HASH = os.environ.get('API_HASH') +BOT_TOKEN = os.environ.get('BOT_TOKEN') +TESTMODE = os.environ.get('TESTMODE') +TESTMODE = TESTMODE and TESTMODE != '0' + +EVERYONE_CHATS = os.environ.get('EVERYONE_CHATS') +EVERYONE_CHATS = list(map(int, EVERYONE_CHATS.split(' '))) if EVERYONE_CHATS else [] +ADMIN_CHATS = os.environ.get('ADMIN_CHATS') +ADMIN_CHATS = list(map(int, ADMIN_CHATS.split(' '))) if ADMIN_CHATS else [] +ALL_CHATS = EVERYONE_CHATS + ADMIN_CHATS + +PROGRESS_UPDATE_DELAY = int(os.environ.get('PROGRESS_UPDATE_DELAY', 10)) +MAGNET_TIMEOUT = int(os.environ.get('LEECH_TIMEOUT', 60)) +LEECH_TIMEOUT = int(os.environ.get('LEECH_TIMEOUT', 300)) + +logging.basicConfig(level=logging.INFO) +app = Client('lazyleech', API_ID, API_HASH, plugins={'root': os.path.join(__package__, 'plugins')}, bot_token=BOT_TOKEN, test_mode=TESTMODE, parse_mode='html', sleep_threshold=30) +session = aiohttp.ClientSession() +help_dict = dict() +preserved_logs = [] + +class SendAsZipFlag: + pass + +class ForceDocumentFlag: + pass + +def memory_file(name=None, contents=None, *, bytes=True): + if isinstance(contents, str) and bytes: + contents = contents.encode() + file = BytesIO() if bytes else StringIO() + if name: + file.name = name + if contents: + file.write(contents) + file.seek(0) + return file \ No newline at end of file diff --git a/lazyleech/plugins/__pycache__/autodetect.cpython-38.pyc b/lazyleech/plugins/__pycache__/autodetect.cpython-38.pyc new file mode 100644 index 0000000..17c8e63 Binary files /dev/null and b/lazyleech/plugins/__pycache__/autodetect.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/help.cpython-38.pyc b/lazyleech/plugins/__pycache__/help.cpython-38.pyc new file mode 100644 index 0000000..94a36df Binary files /dev/null and b/lazyleech/plugins/__pycache__/help.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/leech.cpython-38.pyc b/lazyleech/plugins/__pycache__/leech.cpython-38.pyc new file mode 100644 index 0000000..c32b9e7 Binary files /dev/null and b/lazyleech/plugins/__pycache__/leech.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/nyaa.cpython-38.pyc b/lazyleech/plugins/__pycache__/nyaa.cpython-38.pyc new file mode 100644 index 0000000..9fb0098 Binary files /dev/null and b/lazyleech/plugins/__pycache__/nyaa.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/ping.cpython-38.pyc b/lazyleech/plugins/__pycache__/ping.cpython-38.pyc new file mode 100644 index 0000000..df92662 Binary files /dev/null and b/lazyleech/plugins/__pycache__/ping.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/pyexec.cpython-38.pyc b/lazyleech/plugins/__pycache__/pyexec.cpython-38.pyc new file mode 100644 index 0000000..dac9963 Binary files /dev/null and b/lazyleech/plugins/__pycache__/pyexec.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/thumbnail.cpython-38.pyc b/lazyleech/plugins/__pycache__/thumbnail.cpython-38.pyc new file mode 100644 index 0000000..a508aef Binary files /dev/null and b/lazyleech/plugins/__pycache__/thumbnail.cpython-38.pyc differ diff --git a/lazyleech/plugins/__pycache__/watermark.cpython-38.pyc b/lazyleech/plugins/__pycache__/watermark.cpython-38.pyc new file mode 100644 index 0000000..9e3bfa3 Binary files /dev/null and b/lazyleech/plugins/__pycache__/watermark.cpython-38.pyc differ diff --git a/lazyleech/plugins/autodetect.py b/lazyleech/plugins/autodetect.py new file mode 100644 index 0000000..9b07fcf --- /dev/null +++ b/lazyleech/plugins/autodetect.py @@ -0,0 +1,98 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import re +import time +import asyncio +import tempfile +from urllib.parse import urlsplit +from pyrogram import Client, filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from .. import ALL_CHATS, SendAsZipFlag, ForceDocumentFlag +from ..utils.misc import get_file_mimetype +from ..utils import custom_filters +from .leech import initiate_torrent, initiate_magnet + +NYAA_REGEX = re.compile(r'^(?:https?://)?(?P(?:www\.|sukebei\.)?nyaa\.si|nyaa\.squid\.workers\.dev)/(?:view|download)/(?P\d+)(?:[\./]torrent)?$') +auto_detects = dict() +@Client.on_message(filters.chat(ALL_CHATS), group=1) +async def autodetect(client, message): + text = message.text + document = message.document + link = None + is_torrent = False + if document: + if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): + os.makedirs(str(message.from_user.id), exist_ok=True) + fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') + os.fdopen(fd).close() + await message.download(link) + mimetype = await get_file_mimetype(link) + is_torrent = True + if mimetype != 'application/x-bittorrent': + os.remove(link) + link = None + is_torrent = False + if not link and text: + match = NYAA_REGEX.match(text) + if match: + link = f'https://{match.group("base")}/download/{match.group("sauce")}.torrent' + is_torrent = True + else: + splitted = urlsplit(text) + if splitted.scheme == 'magnet' and splitted.query: + link = text + if link: + reply = await message.reply_text(f'{"Torrent" if is_torrent else "Magnet"} detected. Select upload method', reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton('Individual Files', 'autodetect_individual'), InlineKeyboardButton('Zip', 'autodetect_zip'), InlineKeyboardButton('Force Document', 'autodetect_file')], + [InlineKeyboardButton('Delete', 'autodetect_delete')] + ])) + auto_detects[(reply.chat.id, reply.message_id)] = link, message.from_user.id, (initiate_torrent if is_torrent else initiate_magnet) + +answered = set() +answer_lock = asyncio.Lock() +@Client.on_callback_query(custom_filters.callback_data(['autodetect_individual', 'autodetect_zip', 'autodetect_file', 'autodetect_delete']) & custom_filters.callback_chat(ALL_CHATS)) +async def autodetect_callback(client, callback_query): + message = callback_query.message + identifier = (message.chat.id, message.message_id) + result = auto_detects.get(identifier) + if not result: + await callback_query.answer('I can\'t get your message, please try again.', show_alert=True, cache_time=3600) + return + link, user_id, init_func = result + if callback_query.from_user.id != user_id: + await callback_query.answer('...no', cache_time=3600) + return + async with answer_lock: + if identifier in answered: + await callback_query.answer('...no') + return + answered.add(identifier) + asyncio.create_task(message.delete()) + data = callback_query.data + start_leech = data in ('autodetect_individual', 'autodetect_zip', 'autodetect_file') + if start_leech: + if getattr(message.reply_to_message, 'empty', True): + await callback_query.answer('Don\'t delete your message!', show_alert=True) + return + if data == 'autodetect_zip': + flags = (SendAsZipFlag,) + elif data == 'autodetect_file': + flags = (ForceDocumentFlag,) + else: + flags = () + await asyncio.gather(callback_query.answer(), init_func(client, message.reply_to_message, link, flags)) diff --git a/lazyleech/plugins/help.py b/lazyleech/plugins/help.py new file mode 100644 index 0000000..f42c15c --- /dev/null +++ b/lazyleech/plugins/help.py @@ -0,0 +1,108 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import asyncio +from pyrogram import Client, filters +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from .. import ALL_CHATS, help_dict +from ..utils import custom_filters + +@Client.on_message(filters.command('help') & filters.chat(ALL_CHATS)) +async def help_cmd(client, message): + module = message.text.split(' ', 1) + module.pop(0) + try: + module = module[0].lower().strip() + except IndexError: + module = None + for internal_name in help_dict: + external_name, text = help_dict[internal_name] + external_name = external_name.lower().strip() + internal_name = internal_name.lower().strip() + if module in (internal_name, external_name): + buttons = [ + [InlineKeyboardButton('Back', 'help_back')] + ] + break + else: + module = None + text = 'Select the module you want help with' + buttons = [] + to_append = [] + for internal_name in help_dict: + external_name, _ = help_dict[internal_name] + to_append.append(InlineKeyboardButton(external_name.strip(), f'help_m{internal_name}')) + if len(to_append) > 2: + buttons.append(to_append) + to_append = [] + if to_append: + buttons.append(to_append) + reply = await message.reply_text(text, reply_markup=InlineKeyboardMarkup(buttons)) + callback_info[(reply.chat.id, reply.message_id)] = message.from_user.id, module + +callback_lock = asyncio.Lock() +callback_info = dict() +@Client.on_callback_query(custom_filters.callback_data('help_back') & custom_filters.callback_chat(ALL_CHATS)) +async def help_back(client, callback_query): + message = callback_query.message + message_identifier = (message.chat.id, message.message_id) + if message_identifier not in callback_info: + await callback_query.answer('This help message is too old that I don\'t have info on it.', show_alert=True, cache_time=3600) + return + async with callback_lock: + info = callback_info.get((message.chat.id, message.message_id)) + user_id, location = info + if user_id != callback_query.from_user.id: + await callback_query.answer('...no', cache_time=3600) + return + if location is not None: + buttons = [] + to_append = [] + for internal_name in help_dict: + external_name, _ = help_dict[internal_name] + to_append.append(InlineKeyboardButton(external_name.strip(), f'help_m{internal_name}')) + if len(to_append) > 2: + buttons.append(to_append) + to_append = [] + if to_append: + buttons.append(to_append) + await message.edit_text('Select the module you want help with.', reply_markup=InlineKeyboardMarkup(buttons)) + callback_info[message_identifier] = user_id, None + await callback_query.answer() + +@Client.on_callback_query(filters.regex('help_m.+') & custom_filters.callback_chat(ALL_CHATS)) +async def help_m(client, callback_query): + message = callback_query.message + message_identifier = (message.chat.id, message.message_id) + if message_identifier not in callback_info: + await callback_query.answer('This help message is too old that I don\'t have info on it.', show_alert=True, cache_time=3600) + return + async with callback_lock: + info = callback_info.get((message.chat.id, message.message_id)) + user_id, location = info + if user_id != callback_query.from_user.id: + await callback_query.answer('...no', cache_time=3600) + return + module = callback_query.data[6:] + if module not in help_dict: + await callback_query.answer('What module?') + return + if module != location: + await message.edit_text(help_dict[module][1], reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton('Back', 'help_back')] + ])) + callback_info[message_identifier] = user_id, module + await callback_query.answer() diff --git a/lazyleech/plugins/leech.py b/lazyleech/plugins/leech.py new file mode 100644 index 0000000..7ddba9a --- /dev/null +++ b/lazyleech/plugins/leech.py @@ -0,0 +1,392 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import time +import html +import asyncio +import tempfile +from urllib.parse import urlparse, urlunparse, unquote as urldecode +from pyrogram import Client, filters +from pyrogram.parser import html as pyrogram_html +from .. import ADMIN_CHATS, ALL_CHATS, PROGRESS_UPDATE_DELAY, session, help_dict, LEECH_TIMEOUT, MAGNET_TIMEOUT, SendAsZipFlag, ForceDocumentFlag +from ..utils.aria2 import aria2_add_torrent, aria2_tell_status, aria2_remove, aria2_add_magnet, Aria2Error, aria2_tell_active, is_gid_owner, aria2_add_directdl +from ..utils.misc import format_bytes, get_file_mimetype, return_progress_string, calculate_eta, allow_admin_cancel +from ..utils.upload_worker import upload_queue, upload_statuses, progress_callback_data, upload_waits, stop_uploads + +@Client.on_message(filters.command(['torrent', 'ziptorrent', 'filetorrent']) & filters.chat(ALL_CHATS)) +async def torrent_cmd(client, message): + text = (message.text or message.caption).split(None, 1) + command = text.pop(0).lower() + if 'zip' in command: + flags = (SendAsZipFlag,) + elif 'file' in command: + flags = (ForceDocumentFlag,) + else: + flags = () + link = None + reply = message.reply_to_message + document = message.document + if document: + if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): + os.makedirs(str(message.from_user.id), exist_ok=True) + fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') + os.fdopen(fd).close() + await message.download(link) + mimetype = await get_file_mimetype(link) + if mimetype != 'application/x-bittorrent': + os.remove(link) + link = None + if not link: + if text: + link = text[0].strip() + elif not getattr(reply, 'empty', True): + document = reply.document + link = reply.text + if document: + if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): + os.makedirs(str(message.from_user.id), exist_ok=True) + fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') + os.fdopen(fd).close() + await reply.download(link) + mimetype = await get_file_mimetype(link) + if mimetype != 'application/x-bittorrent': + os.remove(link) + link = reply.text or reply.caption + if not link: + await message.reply_text('''Usage: +- /torrent <Torrent URL or File> +- /torrent (as reply to a Torrent URL or file) + +- /ziptorrent <Torrent URL or File> +- /ziptorrent (as reply to a Torrent URL or File) + +- /filetorrent <Torrent URL or File> - Sends videos as files +- /filetorrent (as reply to a Torrent URL or file) - Sends videos as files''') + return + await initiate_torrent(client, message, link, flags) + await message.stop_propagation() + +async def initiate_torrent(client, message, link, flags): + user_id = message.from_user.id + reply = await message.reply_text('Adding torrent...') + try: + gid = await aria2_add_torrent(session, user_id, link, LEECH_TIMEOUT) + except Aria2Error as ex: + await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) + return + finally: + if os.path.isfile(link): + os.remove(link) + await handle_leech(client, message, gid, reply, user_id, flags) + +@Client.on_message(filters.command(['magnet', 'zipmagnet', 'filemagnet']) & filters.chat(ALL_CHATS)) +async def magnet_cmd(client, message): + text = (message.text or message.caption).split(None, 1) + command = text.pop(0).lower() + if 'zip' in command: + flags = (SendAsZipFlag,) + elif 'file' in command: + flags = (ForceDocumentFlag,) + else: + flags = () + link = None + reply = message.reply_to_message + if text: + link = text[0].strip() + elif not getattr(reply, 'empty', True): + link = reply.text or reply.caption + if not link: + await message.reply_text('''Usage: +- /magnet <Magnet URL> +- /magnet (as reply to a Magnet URL) + +- /zipmagnet <Magnet URL> +- /zipmagnet (as reply to a Magnet URL) + +- /filemagnet <Magnet URL> - Sends videos as files +- /filemagnet (as reply to a Magnet URL) - Sends videos as files''') + return + await initiate_magnet(client, message, link, flags) + +async def initiate_magnet(client, message, link, flags): + user_id = message.from_user.id + reply = await message.reply_text('Adding magnet...') + try: + gid = await asyncio.wait_for(aria2_add_magnet(session, user_id, link, LEECH_TIMEOUT), MAGNET_TIMEOUT) + except Aria2Error as ex: + await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) + except asyncio.TimeoutError: + await asyncio.gather(message.reply_text('Magnet timed out'), reply.delete()) + else: + await handle_leech(client, message, gid, reply, user_id, flags) + +@Client.on_message(filters.command(['directdl', 'direct', 'zipdirectdl', 'zipdirect', 'filedirectdl', 'filedirect']) & filters.chat(ALL_CHATS)) +async def directdl_cmd(client, message): + text = message.text.split(None, 1) + command = text.pop(0).lower() + if 'zip' in command: + flags = (SendAsZipFlag,) + elif 'file' in command: + flags = (ForceDocumentFlag,) + else: + flags = () + link = filename = None + reply = message.reply_to_message + if text: + link = text[0].strip() + elif not getattr(reply, 'empty', True): + link = reply.text + if not link: + await message.reply_text('''Usage: +- /directdl <Direct URL> | optional custom file name +- /directdl (as reply to a Direct URL) | optional custom file name +- /direct <Direct URL> | optional custom file name +- /direct (as reply to a Direct URL) | optional custom file name + +- /zipdirectdl <Direct URL> | optional custom file name +- /zipdirectdl (as reply to a Direct URL) | optional custom file name +- /zipdirect <Direct URL> | optional custom file name +- /zipdirect (as reply to a Direct URL) | optional custom file name + +- /filedirectdl <Direct URL> | optional custom file name - Sends videos as files +- /filedirectdl (as reply to a Direct URL) | optional custom file name - Sends videos as files +- /filedirect <Direct URL> | optional custom file name - Sends videos as files +- /filedirect (as reply to a Direct URL) | optional custom file name - Sends videos as files''') + return + split = link.split('|', 1) + if len(split) > 1: + filename = os.path.basename(split[1].strip()) + link = split[0].strip() + parsed = list(urlparse(link, 'https')) + if parsed[0] == 'magnet': + if SendAsZipFlag in flags: + prefix = 'zip' + elif ForceDocumentFlag in flags: + prefix = 'file' + else: + prefix = '' + await message.reply_text(f'Use /{prefix}magnet instead') + return + if not parsed[0]: + parsed[0] = 'https' + if parsed[0] not in ('http', 'https'): + await message.reply_text('Invalid scheme') + return + link = urlunparse(parsed) + await initiate_directdl(client, message, link, filename, flags) + +async def initiate_directdl(client, message, link, filename, flags): + user_id = message.from_user.id + reply = await message.reply_text('Adding url...') + try: + gid = await asyncio.wait_for(aria2_add_directdl(session, user_id, link, filename, LEECH_TIMEOUT), MAGNET_TIMEOUT) + except Aria2Error as ex: + await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) + except asyncio.TimeoutError: + await asyncio.gather(message.reply_text('Connection timed out'), reply.delete()) + else: + await handle_leech(client, message, gid, reply, user_id, flags) + +leech_statuses = dict() +async def handle_leech(client, message, gid, reply, user_id, flags): + prevtext = None + torrent_info = await aria2_tell_status(session, gid) + last_edit = 0 + start_time = time.time() + message_identifier = (reply.chat.id, reply.message_id) + leech_statuses[message_identifier] = gid + download_speed = None + while torrent_info['status'] in ('active', 'waiting', 'paused'): + if torrent_info.get('seeder') == 'true': + break + status = torrent_info['status'].capitalize() + total_length = int(torrent_info['totalLength']) + completed_length = int(torrent_info['completedLength']) + download_speed = format_bytes(torrent_info['downloadSpeed']) + '/s' + if total_length: + formatted_total_length = format_bytes(total_length) + else: + formatted_total_length = 'Unknown' + formatted_completed_length = format_bytes(completed_length) + seeders = torrent_info.get('numSeeders') + peers = torrent_info.get('connections') + if torrent_info.get('bittorrent'): + tor_name = torrent_info['bittorrent']['info']['name'] + else: + tor_name = os.path.basename(torrent_info['files'][0]['path']) + if not tor_name: + tor_name = urldecode(os.path.basename(urlparse(torrent_info['files'][0]['uris'][0]['uri']).path)) + text = f'''{html.escape(tor_name)} +{html.escape(return_progress_string(completed_length, total_length))} + +GID: {gid} +Status: {status} +Total Size: {formatted_total_length} +Downloaded Size: {formatted_completed_length} +Download Speed: {download_speed} +ETA: {calculate_eta(completed_length, total_length, start_time)}''' + if seeders is not None: + text += f'\nSeeders: {seeders}' + if peers is not None: + text += f'\n{"Peers" if seeders is not None else "Connections"}: {peers}' + if (time.time() - last_edit) > PROGRESS_UPDATE_DELAY and text != prevtext: + await reply.edit_text(text) + prevtext = text + last_edit = time.time() + torrent_info = await aria2_tell_status(session, gid) + if torrent_info['status'] == 'error': + error_code = torrent_info['errorCode'] + error_message = torrent_info['errorMessage'] + text = f'Aria2 Error Occured!\n{error_code}: {html.escape(error_message)}' + if error_code == '7' and not error_message and torrent_info['downloadSpeed'] == '0': + text += '\n\nThis error may have been caused due to the torrent being too slow' + await asyncio.gather( + message.reply_text(text), + reply.delete() + ) + elif torrent_info['status'] == 'removed': + await asyncio.gather( + message.reply_text('Your download has been manually cancelled.'), + reply.delete() + ) + else: + leech_statuses.pop(message_identifier) + task = None + if upload_queue._unfinished_tasks: + task = asyncio.create_task(reply.edit_text('Download successful, waiting for queue...')) + upload_queue.put_nowait((client, message, reply, torrent_info, user_id, flags)) + try: + await aria2_remove(session, gid) + except Aria2Error as ex: + if not (ex.error_code == 1 and ex.error_message == f'Active Download not found for GID#{gid}'): + raise + finally: + if task: + await task + +@Client.on_message(filters.command('list') & filters.chat(ALL_CHATS)) +async def list_leeches(client, message): + user_id = message.from_user.id + text = '' + quote = None + parser = pyrogram_html.HTML(client) + for i in await aria2_tell_active(session): + if i.get('bittorrent'): + info = i['bittorrent'].get('info') + if not info: + continue + tor_name = info['name'] + else: + tor_name = os.path.basename(i['files'][0]['path']) + if not tor_name: + tor_name = urldecode(os.path.basename(urlparse(i['files'][0]['uris'][0]['uri']).path)) + a = f'''{html.escape(tor_name)} +{i['gid']}\n\n''' + futtext = text + a + if len((await parser.parse(futtext))['message']) > 4096: + await message.reply_text(text, quote=quote) + quote = False + futtext = a + text = futtext + if not text: + text = 'No leeches found.' + await message.reply_text(text, quote=quote) + +@Client.on_message(filters.command('cancel') & filters.chat(ALL_CHATS)) +async def cancel_leech(client, message): + user_id = message.from_user.id + gid = None + text = message.text.split(' ', 1) + text.pop(0) + reply = message.reply_to_message + if text: + gid = text[0].strip() + elif not getattr(reply, 'empty', True): + reply_identifier = (reply.chat.id, reply.message_id) + task = upload_statuses.get(reply_identifier) + if task: + task, starter_id = task + if user_id != starter_id and not await allow_admin_cancel(message.chat.id, user_id): + await message.reply_text('You did not start this leech.') + else: + task.cancel() + return + result = progress_callback_data.get(reply_identifier) + if result: + if user_id != result[3] and not await allow_admin_cancel(message.chat.id, user_id): + await message.reply_text('You did not start this leech.') + else: + stop_uploads.add(reply_identifier) + await message.reply_text('Cancelled!') + return + starter_id = upload_waits.get(reply_identifier) + if starter_id: + if user_id != starter_id[0] and not await allow_admin_cancel(message.chat.id, user_id): + await message.reply_text('You did not start this leech.') + else: + stop_uploads.add(reply_identifier) + await message.reply_text('Cancelled!') + return + gid = leech_statuses.get(reply_identifier) + if not gid: + await message.reply_text('''Usage: +/cancel <GID> +/cancel (as reply to status message)''') + return + if not is_gid_owner(user_id, gid) and not await allow_admin_cancel(message.chat.id, user_id): + await message.reply_text('You did not start this leech.') + return + await aria2_remove(session, gid) + +help_dict['leech'] = ('Leech', +'''/torrent <Torrent URL or File> +/torrent (as reply to a Torrent URL or file) + +/ziptorrent <Torrent URL or File> +/ziptorrent (as reply to a Torrent URL or File) + +/filetorrent <Torrent URL or File> - Sends videos as files +/filetorrent (as reply to a Torrent URL or File) - Sends videos as files + +/magnet <Magnet URL> +/magnet (as reply to a Magnet URL) + +/zipmagnet <Magnet URL> +/zipmagnet (as reply to a Magnet URL) + +/filemagnet <Magnet URL> - Sends videos as files +/filemagnet (as reply to a Magnet URL) - Sends videos as files + +/directdl <Direct URL> | optional custom file name +/directdl (as reply to a Direct URL) | optional custom file name +/direct <Direct URL> | optional custom file name +/direct (as reply to a Direct URL) | optional custom file name + +/zipdirectdl <Direct URL> | optional custom file name +/zipdirectdl (as reply to a Direct URL) | optional custom file name +/zipdirect <Direct URL> | optional custom file name +/zipdirect (as reply to a Direct URL) | optional custom file name + +/filedirectdl <Direct URL> | optional custom file name - Sends videos as files +/filedirectdl (as reply to a Direct URL) | optional custom file name - Sends videos as files +/filedirect <Direct URL> | optional custom file name - Sends videos as files +/filedirect (as reply to a Direct URL) | optional custom file name - Sends videos as files + +/cancel <GID> +/cancel (as reply to status message) + +/list - Lists all current leeches''') diff --git a/lazyleech/plugins/nyaa.py b/lazyleech/plugins/nyaa.py new file mode 100644 index 0000000..069dcd7 --- /dev/null +++ b/lazyleech/plugins/nyaa.py @@ -0,0 +1,156 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import time +import html +import asyncio +import feedparser +from urllib.parse import quote as urlencode, urlsplit +from pyrogram import Client, filters +from pyrogram.parser import html as pyrogram_html +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from .. import ALL_CHATS, session, help_dict +from ..utils import custom_filters + +search_lock = asyncio.Lock() +search_info = {False: dict(), True: dict()} +async def return_search(query, page=1, sukebei=False): + page -= 1 + query = query.lower().strip() + used_search_info = search_info[sukebei] + async with search_lock: + results, get_time = used_search_info.get(query, (None, 0)) + if (time.time() - get_time) > 3600: + results = [] + async with session.get(f'https://{"sukebei." if sukebei else ""}nyaa.si/?page=rss&q={urlencode(query)}') as resp: + d = feedparser.parse(await resp.text()) + text = '' + a = 0 + parser = pyrogram_html.HTML(None) + for i in sorted(d['entries'], key=lambda i: int(i['nyaa_seeders']), reverse=True): + if i['nyaa_size'].startswith('0'): + continue + if not int(i['nyaa_seeders']): + break + link = i['link'] + splitted = urlsplit(link) + if splitted.scheme == 'magnet' and splitted.query: + link = f'{link}' + newtext = f'''{a + 1}. {html.escape(i["title"])} +Link: {link} +Size: {i["nyaa_size"]} +Seeders: {i["nyaa_seeders"]} +Leechers: {i["nyaa_leechers"]} +Category: {i["nyaa_category"]}\n\n''' + futtext = text + newtext + if (a and not a % 10) or len((await parser.parse(futtext))['message']) > 4096: + results.append(text) + futtext = newtext + text = futtext + a += 1 + results.append(text) + ttl = time.time() + used_search_info[query] = results, ttl + try: + return results[page], len(results), ttl + except IndexError: + return '', len(results), ttl + +message_info = dict() +ignore = set() +@Client.on_message(filters.command(['ts', 'nyaa', 'nyaasi'])) +async def nyaa_search(client, message): + text = message.text.split(' ') + text.pop(0) + query = ' '.join(text) + await init_search(client, message, query, False) + +@Client.on_message(filters.command(['sts', 'sukebei'])) +async def nyaa_search_sukebei(client, message): + text = message.text.split(' ') + text.pop(0) + query = ' '.join(text) + await init_search(client, message, query, True) + +async def init_search(client, message, query, sukebei): + result, pages, ttl = await return_search(query, sukebei=sukebei) + if not result: + await message.reply_text('No results found') + else: + buttons = [InlineKeyboardButton(f'1/{pages}', 'nyaa_nop'), InlineKeyboardButton('Next', 'nyaa_next')] + if pages == 1: + buttons.pop() + reply = await message.reply_text(result, reply_markup=InlineKeyboardMarkup([ + buttons + ])) + message_info[(reply.chat.id, reply.message_id)] = message.from_user.id, ttl, query, 1, pages, sukebei + +@Client.on_callback_query(custom_filters.callback_data('nyaa_nop')) +async def nyaa_nop(client, callback_query): + await callback_query.answer(cache_time=3600) + +callback_lock = asyncio.Lock() +@Client.on_callback_query(custom_filters.callback_data(['nyaa_back', 'nyaa_next'])) +async def nyaa_callback(client, callback_query): + message = callback_query.message + message_identifier = (message.chat.id, message.message_id) + data = callback_query.data + async with callback_lock: + if message_identifier in ignore: + await callback_query.answer() + return + user_id, ttl, query, current_page, pages, sukebei = message_info.get(message_identifier, (None, 0, None, 0, 0, None)) + og_current_page = current_page + if data == 'nyaa_back': + current_page -= 1 + elif data == 'nyaa_next': + current_page += 1 + if current_page < 1: + current_page = 1 + elif current_page > pages: + current_page = pages + ttl_ended = (time.time() - ttl) > 3600 + if ttl_ended: + text = getattr(message.text, 'html', 'Search expired') + else: + if callback_query.from_user.id != user_id: + await callback_query.answer('...no', cache_time=3600) + return + text, pages, ttl = await return_search(query, current_page, sukebei) + buttons = [InlineKeyboardButton('Back', 'nyaa_back'), InlineKeyboardButton(f'{current_page}/{pages}', 'nyaa_nop'), InlineKeyboardButton('Next', 'nyaa_next')] + if ttl_ended: + buttons = [InlineKeyboardButton('Search Expired', 'nyaa_nop')] + else: + if current_page == 1: + buttons.pop(0) + if current_page == pages: + buttons.pop() + if ttl_ended or current_page != og_current_page: + await callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup([ + buttons + ])) + message_info[message_identifier] = user_id, ttl, query, current_page, pages, sukebei + if ttl_ended: + ignore.add(message_identifier) + await callback_query.answer() + +help_dict['nyaa'] = ('Nyaa.si', +'''/ts [search query] +/nyaa [search query] +/nyaasi [search query] + +/sts [search query] +/sukebei [search query]''') diff --git a/lazyleech/plugins/ping.py b/lazyleech/plugins/ping.py new file mode 100644 index 0000000..545ee90 --- /dev/null +++ b/lazyleech/plugins/ping.py @@ -0,0 +1,22 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from pyrogram import Client, filters +from .. import ALL_CHATS + +@Client.on_message(filters.command('ping') & filters.chat(ALL_CHATS)) +async def ping_pong(client, message): + await message.reply_text('Pong') diff --git a/lazyleech/plugins/pyexec.py b/lazyleech/plugins/pyexec.py new file mode 100644 index 0000000..f7bb84e --- /dev/null +++ b/lazyleech/plugins/pyexec.py @@ -0,0 +1,92 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +# https://greentreesnakes.readthedocs.io/ +import ast +import sys +import html +import inspect +import traceback +from pyrogram import Client, filters +from .. import ADMIN_CHATS, memory_file + +@Client.on_message(filters.command('exec') & filters.chat(ADMIN_CHATS)) +async def run_code(client, message): + class UniqueExecReturnIdentifier: + pass + code = message.text[5:].strip() + if not code: + await message.reply_text('code 100') + return + tree = ast.parse(code) + obody = tree.body + body = obody.copy() + body.append(ast.Return(ast.Name('_ueri', ast.Load()))) + def _gf(body): + # args: m, message, c, client, _ueri + func = ast.AsyncFunctionDef('ex', ast.arguments([], [ast.arg(i, None, None) for i in ['m', 'message', 'c', 'client', '_ueri']], None, [], [], None, []), body, [], None, None) + ast.fix_missing_locations(func) + mod = ast.parse('') + mod.body = [func] + fl = locals().copy() + exec(compile(mod, '', 'exec'), globals(), fl) + return fl['ex'] + try: + exx = _gf(body) + except SyntaxError as ex: + if ex.msg != "'return' with value in async generator": + raise + exx = _gf(obody) + escaped_code = html.escape(code) + async_obj = exx(message, message, client, client, UniqueExecReturnIdentifier) + reply = await message.reply_text('Type[py]\n{}\nState[Executing]'.format(escaped_code)) + stdout = sys.stdout + stderr = sys.stderr + wrapped_stdout = memory_file(bytes=False) + wrapped_stdout.buffer = memory_file() + wrapped_stderr = memory_file(bytes=False) + wrapped_stderr.buffer = memory_file() + sys.stdout = wrapped_stdout + sys.stderr = wrapped_stderr + try: + if inspect.isasyncgen(async_obj): + returned = [i async for i in async_obj] + else: + returned = [await async_obj] + if returned == [UniqueExecReturnIdentifier]: + returned = [] + except Exception: + await message.reply_text(traceback.format_exc(), parse_mode=None) + return + finally: + sys.stdout = stdout + sys.stderr = stderr + wrapped_stdout.seek(0) + wrapped_stderr.seek(0) + wrapped_stdout.buffer.seek(0) + wrapped_stderr.buffer.seek(0) + r = [] + outtxt = wrapped_stderr.read() + wrapped_stderr.buffer.read().decode() + if outtxt.strip().strip('\n').strip(): + r.append(outtxt) + errtxt = wrapped_stdout.read() + wrapped_stdout.buffer.read().decode() + if errtxt.strip().strip('\n').strip(): + r.append(errtxt) + r.extend(returned) + r = [html.escape(str(i).strip('\n')) for i in r] + r = '\n'.join([f'{i}' for i in r]) + r = r.strip() or 'undefined' + await reply.edit_text('Type[py]\n{}\nState[Executed]\nOutput \\\n{}'.format(escaped_code, r)) diff --git a/lazyleech/plugins/thumbnail.py b/lazyleech/plugins/thumbnail.py new file mode 100644 index 0000000..7db0dbe --- /dev/null +++ b/lazyleech/plugins/thumbnail.py @@ -0,0 +1,77 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import tempfile +from pyrogram import Client, filters +from .. import ALL_CHATS, help_dict +from ..utils.misc import convert_to_jpg, get_file_mimetype, watermark_photo + +@Client.on_message(filters.command(['thumbnail', 'savethumbnail', 'setthumbnail']) & filters.chat(ALL_CHATS)) +async def savethumbnail(client, message): + reply = message.reply_to_message + document = message.document + photo = message.photo + thumbset = False + user_id = message.from_user.id + thumbnail_path = os.path.join(str(user_id), 'thumbnail.jpg') + os.makedirs(str(user_id), exist_ok=True) + if document or photo: + if photo or (document.file_size < 10485760 and os.path.splitext(document.file_name)[1] and (not document.mime_type or document.mime_type.startswith('image/'))): + with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: + await message.download(tempthumb.name) + mimetype = await get_file_mimetype(tempthumb.name) + if mimetype.startswith('image/'): + await convert_to_jpg(tempthumb.name, thumbnail_path) + thumbset = True + if not getattr(reply, 'empty', True) and not thumbset: + document = reply.document + photo = reply.photo + if document or photo: + if photo or (document.file_size < 10485760 and os.path.splitext(document.file_name)[1] and (not document.mime_type or document.mime_type.startswith('image/'))): + with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: + await reply.download(tempthumb.name) + mimetype = await get_file_mimetype(tempthumb.name) + if mimetype.startswith('image/'): + await convert_to_jpg(tempthumb.name, thumbnail_path) + thumbset = True + if thumbset: + watermark = os.path.join(str(user_id), 'watermark.jpg') + watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') + if os.path.isfile(watermark): + await watermark_photo(thumbnail_path, watermark, watermarked_thumbnail) + await message.reply_text('Thumbnail set') + else: + await message.reply_text('Cannot find thumbnail') + +@Client.on_message(filters.command(['clearthumbnail', 'rmthumbnail', 'delthumbnail', 'removethumbnail', 'deletethumbnail']) & filters.chat(ALL_CHATS)) +async def rmthumbnail(client, message): + for path in ('thumbnail', 'watermarked_thumbnail'): + path = os.path.join(str(message.from_user.id), f'{path}.jpg') + if os.path.isfile(path): + os.remove(path) + await message.reply_text('Thumbnail cleared') + +help_dict['thumbnail'] = ('Thumbnail', +'''/thumbnail <as reply to image or as a caption> +/setthumbnail <as reply to image or as a caption> +/savethumbnail <as reply to image or as a caption> + +/clearthumbnail +/rmthumbnail +/removethumbnail +/delthumbnail +/deletethumbnail''') diff --git a/lazyleech/plugins/watermark.py b/lazyleech/plugins/watermark.py new file mode 100644 index 0000000..d2027ce --- /dev/null +++ b/lazyleech/plugins/watermark.py @@ -0,0 +1,103 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import tempfile +from pyrogram import Client, filters +from .. import ALL_CHATS, help_dict +from ..utils.misc import get_file_mimetype, watermark_photo + +@Client.on_message(filters.command(['watermark', 'savewatermark', 'setwatermark']) & filters.chat(ALL_CHATS)) +async def savewatermark(client, message): + reply = message.reply_to_message + document = message.document + photo = message.photo + thumbset = False + user_id = message.from_user.id + watermark_path = os.path.join(str(user_id), 'watermark.jpg') + os.makedirs(str(user_id), exist_ok=True) + if document or photo: + if photo or (document.file_size < 10485760 and os.path.splitext(document.file_name)[1] and (not document.mime_type or document.mime_type.startswith('image/'))): + with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: + await message.download(tempthumb.name) + mimetype = await get_file_mimetype(tempthumb.name) + if mimetype.startswith('image/'): + thumbset = True + with open(watermark_path, 'wb') as watermark_file: + while True: + chunk = tempthumb.read(10) + if not chunk: + break + watermark_file.write(chunk) + if not getattr(reply, 'empty', True) and not thumbset: + document = reply.document + photo = reply.photo + if document or photo: + if photo or (document.file_size < 10485760 and os.path.splitext(document.file_name)[1] and (not document.mime_type or document.mime_type.startswith('image/'))): + with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: + await reply.download(tempthumb.name) + mimetype = await get_file_mimetype(tempthumb.name) + if mimetype.startswith('image/'): + thumbset = True + with open(watermark_path, 'wb') as watermark_file: + while True: + chunk = tempthumb.read(10) + if not chunk: + break + watermark_file.write(chunk) + if thumbset: + thumbnail = os.path.join(str(user_id), 'thumbnail.jpg') + watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') + if os.path.isfile(thumbnail): + await watermark_photo(thumbnail, watermark_path, watermarked_thumbnail) + await message.reply_text('Watermark set') + else: + await message.reply_text('Cannot find watermark') + +@Client.on_message(filters.command(['clearwatermark', 'rmwatermark', 'delwatermark', 'removewatermark', 'deletewatermark']) & filters.chat(ALL_CHATS)) +async def rmwatermark(client, message): + for path in ('watermark', 'watermarked_thumbnail'): + path = os.path.join(str(message.from_user.id), f'{path}.jpg') + if os.path.isfile(path): + os.remove(path) + await message.reply_text('Watermark cleared') + +@Client.on_message(filters.command('testwatermark') & filters.chat(ALL_CHATS)) +async def testwatermark(client, message): + watermark = os.path.join(str(message.from_user.id), 'watermark.jpg') + if not os.path.isfile(watermark): + await message.reply_text('Cannot find watermark') + return + watermarked_thumbnail = os.path.join(str(message.from_user.id), 'watermarked_thumbnail.jpg') + with tempfile.NamedTemporaryFile(suffix='.jpg') as file: + to_upload = watermarked_thumbnail + if not os.path.isfile(to_upload): + await watermark_photo('testwatermark.jpg', watermark, file.name) + to_upload = file.name + await message.reply_photo(to_upload) + +help_dict['watermark'] = ('Watermark', +'''/watermark <as reply to image or as a caption> +/setwatermark <as reply to image or as a caption> +/savewatermark <as reply to image or as a caption> + +/clearwatermark +/rmwatermark +/removewatermark +/delwatermark +/deletewatermark + +/testwatermark''') diff --git a/lazyleech/utils/__init__.py b/lazyleech/utils/__init__.py new file mode 100644 index 0000000..dac2ddb --- /dev/null +++ b/lazyleech/utils/__init__.py @@ -0,0 +1,57 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from pyrogram import filters +from pyrogram.errors import RPCError +from . import aria2 +from . import misc +from . import upload_worker +from . import custom_filters + +# see i pulled a little sneaky on ya +TSM = ''.join([chr(i) for i in [83, 111, 117, 114, 99, 101, 32, 109, 101, 115, 115, 97, 103, 101]]) +try: + import random + import importlib + r = random.Random(b"i'm ho" b"rny!" b"!!") + k0 = r.randint(23 * 3, 210 * 2) + k1 = ord(r.randbytes(1)) + m = importlib.import_module(''.join([chr(i + k0) for i in [-220, -231, -206, -207, + -220, -227, -227, -229, -224]])) + SM = getattr(m, ''.join([chr(int(i / k1)) for i in [9462, 9006, 9690, 9348, 7638, 7866, 10830, + 8778, 7866, 9462, 9462, 7410, 8094, 7866]])) +except (AttributeError, ModuleNotFoundError): + SM = f'{TSM} is {"".join([chr(i) for i in [109, 105, 115, 115, 105, 110, 103]])}' +else: + if not isinstance(SM, str): + SM = '%s is n' % TSM +'ot str' +try: + from .. import app + a = locals()['ppa'[::-1]] +except ImportError: + import sys + print('dednilb gnieb pots dna seye ruoy esu esaelP'[::-1], file=sys.stderr) + sys.exit(1) + +@a.on_message(filters.command('so' 'ur' 'ce')) +async def g_s(_, message): + '''does g_s things''' + try: + await message.reply_text( + SM.strip() or (TSM + ' is ' + 'ytpme'[::-1]), + disable_web_page_preview=True) + except RPCError: + pass diff --git a/lazyleech/utils/__pycache__/__init__.cpython-38.pyc b/lazyleech/utils/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..15007d7 Binary files /dev/null and b/lazyleech/utils/__pycache__/__init__.cpython-38.pyc differ diff --git a/lazyleech/utils/__pycache__/aria2.cpython-38.pyc b/lazyleech/utils/__pycache__/aria2.cpython-38.pyc new file mode 100644 index 0000000..f3fb5f2 Binary files /dev/null and b/lazyleech/utils/__pycache__/aria2.cpython-38.pyc differ diff --git a/lazyleech/utils/__pycache__/custom_filters.cpython-38.pyc b/lazyleech/utils/__pycache__/custom_filters.cpython-38.pyc new file mode 100644 index 0000000..90a3336 Binary files /dev/null and b/lazyleech/utils/__pycache__/custom_filters.cpython-38.pyc differ diff --git a/lazyleech/utils/__pycache__/misc.cpython-38.pyc b/lazyleech/utils/__pycache__/misc.cpython-38.pyc new file mode 100644 index 0000000..77fb027 Binary files /dev/null and b/lazyleech/utils/__pycache__/misc.cpython-38.pyc differ diff --git a/lazyleech/utils/__pycache__/upload_worker.cpython-38.pyc b/lazyleech/utils/__pycache__/upload_worker.cpython-38.pyc new file mode 100644 index 0000000..c49e217 Binary files /dev/null and b/lazyleech/utils/__pycache__/upload_worker.cpython-38.pyc differ diff --git a/lazyleech/utils/aria2.py b/lazyleech/utils/aria2.py new file mode 100644 index 0000000..bc87da1 --- /dev/null +++ b/lazyleech/utils/aria2.py @@ -0,0 +1,132 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import json +import time +import base64 +import random +import asyncio +import tempfile + +HEX_CHARACTERS = 'abcdef' +HEXNUMERIC_CHARACTERS = HEX_CHARACTERS + '0123456789' + +class Aria2Error(Exception): + def __init__(self, message): + self.error_code = message.get('code') + self.error_message = message.get('message') + return super().__init__(str(message)) + +def _raise_or_return(data): + if 'error' in data: + raise Aria2Error(data['error']) + return data['result'] + +async def aria2_request(session, method, params=[]): + data = {'jsonrpc': '2.0', 'id': str(time.time()), 'method': method, 'params': params} + async with session.post('http://127.0.0.1:6800/jsonrpc', data=json.dumps(data)) as resp: + return await resp.json(encoding='utf-8') + +async def aria2_tell_active(session): + return _raise_or_return(await aria2_request(session, 'aria2.tellActive')) + +async def aria2_tell_status(session, gid): + return _raise_or_return(await aria2_request(session, 'aria2.tellStatus', [gid])) + +async def aria2_change_option(session, gid, options): + return _raise_or_return(await aria2_request(session, 'aria2.changeOption', [gid, options])) + +async def aria2_remove(session, gid): + return _raise_or_return(await aria2_request(session, 'aria2.remove', [gid])) + +async def generate_gid(session, user_id): + def _generate_gid(): + gid = str(user_id) + gid += random.choice(HEX_CHARACTERS) + while len(gid) < 16: + gid += random.choice(HEXNUMERIC_CHARACTERS) + return gid + while True: + gid = _generate_gid() + try: + await aria2_tell_status(session, gid) + except Aria2Error as ex: + if not (ex.error_code == 1 and ex.error_message == f'GID {gid} is not found'): + raise + return gid + +def is_gid_owner(user_id, gid): + return gid.split(str(user_id), 1)[-1][0] in HEX_CHARACTERS + +async def aria2_add_torrent(session, user_id, link, timeout=0): + if os.path.isfile(link): + with open(link, 'rb') as file: + torrent = file.read() + else: + async with session.get(link) as resp: + torrent = await resp.read() + torrent = base64.b64encode(torrent).decode() + dir = os.path.join( + os.getcwd(), + str(user_id), + str(time.time()) + ) + return _raise_or_return(await aria2_request(session, 'aria2.addTorrent', [torrent, [], { + 'gid': await generate_gid(session, user_id), + 'dir': dir, + 'seed-time': 0, + 'bt-stop-timeout': str(timeout) + }])) + +async def aria2_add_magnet(session, user_id, link, timeout=0): + with tempfile.TemporaryDirectory() as tempdir: + gid = _raise_or_return(await aria2_request(session, 'aria2.addUri', [[link], { + 'dir': tempdir, + 'bt-save-metadata': 'true', + 'bt-metadata-only': 'true', + 'follow-torrent': 'false' + }])) + try: + info = await aria2_tell_status(session, gid) + while info['status'] == 'active': + await asyncio.sleep(0.5) + info = await aria2_tell_status(session, gid) + filename = os.path.join(tempdir, info['infoHash'] + '.torrent') + return await aria2_add_torrent(session, user_id, filename, timeout) + finally: + try: + await aria2_remove(session, gid) + except Aria2Error as ex: + if not (ex.error_code == 1 and ex.error_message == f'Active Download not found for GID#{gid}'): + raise + +async def aria2_add_directdl(session, user_id, link, filename=None, timeout=60): + dir = os.path.join( + os.getcwd(), + str(user_id), + str(time.time()) + ) + options = { + 'gid': await generate_gid(session, user_id), + 'dir': dir, + 'timeout': str(timeout), + 'follow-torrent': 'false', + 'header': 'User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0' + } + if filename: + options['out'] = filename + return _raise_or_return(await aria2_request(session, 'aria2.addUri', [[link], options])) diff --git a/lazyleech/utils/custom_filters.py b/lazyleech/utils/custom_filters.py new file mode 100644 index 0000000..6f3ff1c --- /dev/null +++ b/lazyleech/utils/custom_filters.py @@ -0,0 +1,39 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from pyrogram import filters + +def callback_data(data): + def func(flt, client, callback_query): + return callback_query.data in flt.data + + data = data if isinstance(data, list) else [data] + return filters.create( + func, + 'CustomCallbackDataFilter', + data=data + ) + +def callback_chat(chats): + def func(flt, client, callback_query): + return callback_query.message.chat.id in flt.chats + + chats = chats if isinstance(chats, list) else [chats] + return filters.create( + func, + 'CustomCallbackChatsFilter', + chats=chats + ) diff --git a/lazyleech/utils/misc.py b/lazyleech/utils/misc.py new file mode 100644 index 0000000..f8dbbde --- /dev/null +++ b/lazyleech/utils/misc.py @@ -0,0 +1,144 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import time +import json +import shlex +import asyncio +import tempfile +import mimetypes +from decimal import Decimal +from datetime import timedelta +from pyrogram.errors.exceptions.bad_request_400 import UserNotParticipant +from .. import app, ADMIN_CHATS + +# https://stackoverflow.com/a/49361727 +def format_bytes(size): + size = int(size) + # 2**10 = 1024 + power = 1024 + n = 0 + power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} + while size > power: + size /= power + n += 1 + return f"{size:.2f} {power_labels[n]+'B'}" + +async def get_file_mimetype(filename): + mimetype = mimetypes.guess_type(filename)[0] + if not mimetype: + proc = await asyncio.create_subprocess_exec('file', '--brief', '--mime-type', filename, stdout=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate() + mimetype = stdout.decode().strip() + return mimetype or '' + +async def split_files(filename, destination_dir, no_ffmpeg=False): + ext = os.path.splitext(filename)[1] + if not no_ffmpeg and (await get_file_mimetype(filename)).startswith('video/'): + video_info = (await get_video_info(filename))['format'] + if 'duration' in video_info: + times = 1 + ss = Decimal('0.0') + duration = Decimal(video_info['duration']) + files = [] + while duration - ss > 1: + filepath = os.path.join(destination_dir, os.path.splitext(os.path.basename(filename))[0][-(248-len(ext)):] + ('-' if ext else '.') + 'part' + str(times) + ext) + proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', filename, '-ss', str(ss), '-c', 'copy', '-fs', '1900000000', filepath) + await proc.communicate() + video_info = (await get_video_info(filepath)).get('format') + if not video_info: + break + if 'duration' not in video_info: + break + files.append(filepath) + times += 1 + ss += Decimal(video_info['duration']) + return files + args = ['split', '--verbose', '--numeric-suffixes=1', '--bytes=2097152000', '--suffix-length=2'] + if ext: + args.append(f'--additional-suffix={ext}') + args.append(filename) + args.append(os.path.join(destination_dir, os.path.basename(filename)[-(248-len(ext)):] + ('-' if ext else '.') + 'part')) + proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate() + return shlex.split(' '.join([i[14:] for i in stdout.decode().strip().split('\n')])) + +video_duration_cache = dict() +video_duration_lock = asyncio.Lock() +async def get_video_info(filename): + proc = await asyncio.create_subprocess_exec('ffprobe', '-print_format', 'json', '-show_format', '-show_streams', filename, stdout=asyncio.subprocess.PIPE) + stdout, _ = await proc.communicate() + js = json.loads(stdout) + if js.get('format'): + if 'duration' not in js['format']: + async with video_duration_lock: + if filename not in video_duration_cache: + with tempfile.NamedTemporaryFile(suffix='.mkv') as tempf: + proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', filename, '-c', 'copy', tempf.name) + await proc.communicate() + video_duration_cache[filename] = (await get_video_info(tempf.name))['format']['duration'] + js['format']['duration'] = video_duration_cache[filename] + return js + +async def generate_thumbnail(videopath, photopath): + video_info = await get_video_info(videopath) + for duration in (10, 5, 0): + if duration < float(video_info['format']['duration']): + proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', videopath, '-ss', str(duration), '-frames:v', '1', photopath) + await proc.communicate() + break + +async def convert_to_jpg(original, end): + proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', original, end) + await proc.communicate() + +# https://stackoverflow.com/a/34325723 +def return_progress_string(current, total): + if total: + filled_length = int(30 * current // total) + else: + filled_length = 0 + return '[' + '=' * filled_length + ' ' * (30 - filled_length) + ']' + +# https://stackoverflow.com/a/852718 +# https://stackoverflow.com/a/775095 +def calculate_eta(current, total, start_time): + if not current or not total: + return '00:00:00' + end_time = time.time() + elapsed_time = end_time - start_time + seconds = (elapsed_time * (total / current)) - elapsed_time + thing = ''.join(str(timedelta(seconds=seconds)).split('.')[:-1]).split(', ') + thing[-1] = thing[-1].rjust(8, '0') + return ', '.join(thing) + +# https://stackoverflow.com/a/10920872 +async def watermark_photo(main, overlay, out): + proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', main, '-i', overlay, '-filter_complex', 'overlay=(main_w-overlay_w)/2:(main_h-overlay_h)', out) + await proc.communicate() + +async def allow_admin_cancel(chat_id, user_id): + if chat_id in ADMIN_CHATS: + return True + for i in ADMIN_CHATS: + try: + await app.get_chat_member(i, user_id) + except UserNotParticipant: + pass + else: + return True + return False diff --git a/lazyleech/utils/upload_worker.py b/lazyleech/utils/upload_worker.py new file mode 100644 index 0000000..3713717 --- /dev/null +++ b/lazyleech/utils/upload_worker.py @@ -0,0 +1,264 @@ +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import os +import html +import time +import shutil +import logging +import asyncio +import zipfile +import tempfile +import traceback +from natsort import natsorted +from pyrogram.parser import html as pyrogram_html +from .. import PROGRESS_UPDATE_DELAY, ADMIN_CHATS, preserved_logs, TESTMODE, SendAsZipFlag, ForceDocumentFlag +from .misc import split_files, get_file_mimetype, format_bytes, get_video_info, generate_thumbnail, return_progress_string, calculate_eta, watermark_photo + +upload_queue = asyncio.Queue() +upload_statuses = dict() +upload_tamper_lock = asyncio.Lock() +async def upload_worker(): + while True: + client, message, reply, torrent_info, user_id, flags = await upload_queue.get() + try: + message_identifier = (reply.chat.id, reply.message_id) + if SendAsZipFlag not in flags: + asyncio.create_task(reply.edit_text('Download successful, uploading files...')) + task = asyncio.create_task(_upload_worker(client, message, reply, torrent_info, user_id, flags)) + upload_statuses[message_identifier] = task, user_id + await task + except asyncio.CancelledError: + text = 'Your leech has been cancelled.' + await asyncio.gather(reply.edit_text(text), message.reply_text(text)) + except Exception as ex: + preserved_logs.append((message, torrent_info, ex)) + logging.exception('%s %s', message, torrent_info) + await message.reply_text(traceback.format_exc(), parse_mode=None) + for admin_chat in ADMIN_CHATS: + await client.send_message(admin_chat, traceback.format_exc(), parse_mode=None) + finally: + upload_queue.task_done() + worker_identifier = (reply.chat.id, reply.message_id) + to_delete = [] + async with upload_tamper_lock: + for key in upload_waits: + _, iworker_identifier = upload_waits[key] + if iworker_identifier == worker_identifier: + upload_waits.pop(key) + to_delete.append(key[1]) + task = None + if to_delete: + task = asyncio.create_task(client.delete_messages(reply.chat.id, to_delete)) + upload_statuses.pop(message_identifier) + if not TESTMODE: + shutil.rmtree(torrent_info['dir']) + if task: + await task + +upload_waits = dict() +async def _upload_worker(client, message, reply, torrent_info, user_id, flags): + files = dict() + sent_files = [] + with tempfile.TemporaryDirectory(dir=str(user_id)) as zip_tempdir: + if SendAsZipFlag in flags: + if torrent_info.get('bittorrent'): + filename = torrent_info['bittorrent']['info']['name'] + else: + filename = os.path.basename(torrent_info['files'][0]['path']) + filename = filename[-251:] + '.zip' + filepath = os.path.join(zip_tempdir, filename) + def _zip_files(): + with zipfile.ZipFile(filepath, 'x') as zipf: + for file in torrent_info['files']: + zipf.write(file['path'], file['path'].replace(os.path.join(torrent_info['dir'], ''), '', 1)) + await asyncio.gather(reply.edit_text('Download successful, zipping files...'), client.loop.run_in_executor(None, _zip_files)) + asyncio.create_task(reply.edit_text('Download successful, uploading files...')) + files[filepath] = filename + else: + for file in torrent_info['files']: + filepath = file['path'] + filename = filepath.replace(os.path.join(torrent_info['dir'], ''), '', 1) + files[filepath] = filename + for filepath in natsorted(files): + sent_files.extend(await _upload_file(client, message, reply, files[filepath], filepath, ForceDocumentFlag in flags)) + text = 'Files:\n' + parser = pyrogram_html.HTML(client) + quote = None + first_index = None + all_amount = 1 + for filename, filelink in sent_files: + if filelink: + atext = f'- {html.escape(filename)}' + else: + atext = f'- {html.escape(filename)} (empty)' + atext += '\n' + futtext = text + atext + if all_amount > 100 or len((await parser.parse(futtext))['message']) > 4096: + thing = await message.reply_text(text, quote=quote, disable_web_page_preview=True) + if first_index is None: + first_index = thing + quote = False + futtext = atext + all_amount = 1 + await asyncio.sleep(PROGRESS_UPDATE_DELAY) + all_amount += 1 + text = futtext + if not sent_files: + text = 'Files: None' + thing = await message.reply_text(text, quote=quote, disable_web_page_preview=True) + if first_index is None: + first_index = thing + asyncio.create_task(reply.edit_text(f'Download successful, files uploaded.\nFiles: {first_index.link}', disable_web_page_preview=True)) + +async def _upload_file(client, message, reply, filename, filepath, force_document): + if not os.path.getsize(filepath): + return [(os.path.basename(filename), None)] + worker_identifier = (reply.chat.id, reply.message_id) + user_id = message.from_user.id + user_thumbnail = os.path.join(str(user_id), 'thumbnail.jpg') + user_watermark = os.path.join(str(user_id), 'watermark.jpg') + user_watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') + file_has_big = os.path.getsize(filepath) > 2097152000 + upload_wait = await reply.reply_text(f'Upload of {html.escape(filename)} will start in {PROGRESS_UPDATE_DELAY}s') + upload_identifier = (upload_wait.chat.id, upload_wait.message_id) + async with upload_tamper_lock: + upload_waits[upload_identifier] = user_id, worker_identifier + to_upload = [] + sent_files = [] + split_task = None + try: + with tempfile.TemporaryDirectory(dir=str(user_id)) as tempdir: + if file_has_big: + async def _split_files(): + splitted = await split_files(filepath, tempdir, force_document) + for a, split in enumerate(splitted, 1): + to_upload.append((split, filename + f' (part {a})')) + split_task = asyncio.create_task(_split_files()) + else: + to_upload.append((filepath, filename)) + for _ in range(PROGRESS_UPDATE_DELAY): + if upload_identifier in stop_uploads: + return sent_files + await asyncio.sleep(1) + if upload_identifier in stop_uploads: + return sent_files + if split_task and not split_task.done(): + await upload_wait.edit_text(f'Splitting {html.escape(filename)}...') + while not split_task.done(): + if upload_identifier in stop_uploads: + return sent_files + await asyncio.sleep(1) + if upload_identifier in stop_uploads: + return sent_files + for a, (filepath, filename) in enumerate(to_upload): + while True: + if a: + async with upload_tamper_lock: + upload_waits.pop(upload_identifier) + upload_wait = await reply.reply_text(f'Upload of {html.escape(filename)} will start in {PROGRESS_UPDATE_DELAY}s') + upload_identifier = (upload_wait.chat.id, upload_wait.message_id) + upload_waits[upload_identifier] = user_id, worker_identifier + for _ in range(PROGRESS_UPDATE_DELAY): + if upload_identifier in stop_uploads: + return sent_files + await asyncio.sleep(1) + if upload_identifier in stop_uploads: + return sent_files + thumbnail = None + for i in (user_thumbnail, user_watermarked_thumbnail): + thumbnail = i if os.path.isfile(i) else thumbnail + mimetype = await get_file_mimetype(filepath) + progress_args = (client, upload_wait, filename, user_id) + try: + if not force_document and mimetype.startswith('video/'): + duration = 0 + video_json = await get_video_info(filepath) + video_format = video_json.get('format') + if video_format and 'duration' in video_format: + duration = round(float(video_format['duration'])) + for stream in video_json.get('streams', ()): + if stream['codec_type'] == 'video': + width = stream.get('width') + height = stream.get('height') + if width and height: + if not thumbnail: + thumbnail = os.path.join(tempdir, '0.jpg') + await generate_thumbnail(filepath, thumbnail) + if os.path.isfile(thumbnail) and os.path.isfile(user_watermark): + othumbnail = thumbnail + thumbnail = os.path.join(tempdir, '1.jpg') + await watermark_photo(othumbnail, user_watermark, thumbnail) + if not os.path.isfile(thumbnail): + thumbnail = othumbnail + if not os.path.isfile(thumbnail): + thumbnail = None + break + else: + width = height = 0 + resp = await reply.reply_video(filepath, thumb=thumbnail, caption=filename, + duration=duration, width=width, height=height, + parse_mode=None, progress=progress_callback, + progress_args=progress_args) + else: + resp = await reply.reply_document(filepath, thumb=thumbnail, caption=filename, + parse_mode=None, progress=progress_callback, + progress_args=progress_args) + except Exception: + await message.reply_text(traceback.format_exc(), parse_mode=None) + continue + if resp: + sent_files.append((os.path.basename(filename), resp.link)) + break + return sent_files + return sent_files + finally: + if split_task: + split_task.cancel() + asyncio.create_task(upload_wait.delete()) + async with upload_tamper_lock: + upload_waits.pop(upload_identifier) + +progress_callback_data = dict() +stop_uploads = set() +async def progress_callback(current, total, client, reply, filename, user_id): + message_identifier = (reply.chat.id, reply.message_id) + last_edit_time, prevtext, start_time, user_id = progress_callback_data.get(message_identifier, (0, None, time.time(), user_id)) + if message_identifier in stop_uploads or current == total: + asyncio.create_task(reply.delete()) + try: + progress_callback_data.pop(message_identifier) + except KeyError: + pass + if message_identifier in stop_uploads: + client.stop_transmission() + elif (time.time() - last_edit_time) > PROGRESS_UPDATE_DELAY: + if last_edit_time: + upload_speed = format_bytes((total - current) / (time.time() - start_time)) + else: + upload_speed = '0 B' + text = f'''Uploading {html.escape(filename)}... +{html.escape(return_progress_string(current, total))} + +Total Size: {format_bytes(total)} +Uploaded Size: {format_bytes(current)} +Upload Speed: {upload_speed}/s +ETA: {calculate_eta(current, total, start_time)}''' + if prevtext != text: + await reply.edit_text(text) + prevtext = text + last_edit_time = time.time() + progress_callback_data[message_identifier] = last_edit_time, prevtext, start_time, user_id diff --git a/pre-commit b/pre-commit new file mode 100644 index 0000000..6b81eee --- /dev/null +++ b/pre-commit @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import glob + +LICENSE_HEADER = ''' +# lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram +# Copyright (c) 2021 lazyleech developers +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero 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 Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +'''.strip() + +missing_header = False +for file in glob.iglob('lazyleech/**/*.py', recursive=True): + with open(file, 'r') as fileobj: + file_header = fileobj.read(len(LICENSE_HEADER)) + if file_header != LICENSE_HEADER: + print(file, 'is missing AGPL license header') + missing_header = True +if missing_header: + exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e4ff90f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +feedparser +pyrogram +tgcrypto +natsort +aiohttp diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..6d07094 --- /dev/null +++ b/run.sh @@ -0,0 +1,6 @@ +#!/bin/sh +touch aria2.log lazyleech.log +tail -f aria2.log & +tail -f lazyleech.log & +aria2c --enable-rpc=true -j5 -x5 > aria2.log 2>&1 & +python3 -m lazyleech > lazyleech.log 2>&1 diff --git a/testwatermark.jpg b/testwatermark.jpg new file mode 100644 index 0000000..669a04c Binary files /dev/null and b/testwatermark.jpg differ