├── Dockerfile ├── LICENSE ├── README.md ├── app.json ├── heroku.yml ├── lazyleech ├── __init__.py ├── __main__.py ├── plugins │ ├── autodetect.py │ ├── help.py │ ├── leech.py │ ├── mediainfo.py │ ├── nyaa.py │ ├── nyaa_auto_download.py │ ├── ping.py │ ├── pyexec.py │ ├── thumbnail.py │ ├── watermark.py │ └── ytdl.py └── utils │ ├── __init__.py │ ├── aiohttp_helper.py │ ├── aria2.py │ ├── custom_filters.py │ ├── misc.py │ └── upload_worker.py ├── pre-commit ├── requirements.txt ├── run.sh ├── testwatermark.jpg └── ytdl ├── __init__.py └── downloads └── __init__.py /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | COPY run.sh requirements.txt testwatermark.jpg /app/ 4 | COPY lazyleech /app/lazyleech/ 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | RUN apt -y update && \ 7 | apt install -y --no-install-recommends python3 git python3-pip ffmpeg mediainfo aria2 file p7zip-full && \ 8 | rm -rf /var/lib/apt/lists/* \ 9 | && echo "Etc/UTC" > /etc/timezone \ 10 | && pip3 install -r /app/requirements.txt 11 | COPY . . 12 | CMD ["bash","run.sh"] 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LazyLeech 2 | 3 |

4 | “ Heroku Supported Telegram Torrent Leeching Bot by Some Weebs ” 5 | 6 | ## More features on [personal-tweak](https://github.com/lostb053/lazyleech/tree/personal-tweak) branch 7 | 8 |

9 | 10 | # Table of Content 11 | - [WHAT IS THIS REPO ABOUT ?](#what-is-this-repo-about) 12 | - [FEATURES](#features) 13 | - [BOT COMMANDS](#bot-commands) 14 | - [TEST THE BOT (DEMO)](https://t.me/joinchat/HC7YmklXMSRPH3N2) 15 | - [CREDITS](#credits-) 16 | - [POINTS TO BE NOTED](#points-to-be-noted) 17 | 18 | 19 | # What is this repo about? 20 | This is a telegram bot writen with pyrogram for leeching files on the internet to Telegram. 21 | 22 | [Bot Demo](https://t.me/joinchat/HC7YmklXMSRPH3N2) 23 | 24 | # Features 25 | - Leeching direct download links | Torrent | Magnets. 26 | - Thumbnail and Watermark Support. 27 | - Auto Download from nyaa.si (choose your uploader wisely, try not to add `https://nyaa.si/?page=rss` as your NYAA_RSS_LINK) 28 | - Torrent/Magnet Auto Detect Support. 29 | - Nyaa.si Search Support. 30 | - Upload as zip, streamable, document. 31 | - Can List All your Ongoing Leeches. 32 | - Advanced ytdl 33 | - Docker support. 34 | 35 | ## Bot Commands 36 | 37 | **Leech Module** 38 | ``` 39 | torrent or as reply to a Torrent URL or file 40 | ziptorrent or as reply to a Torrent URL or File 41 | filetorrent or as reply to a Torrent URL or File - Sends videos as files 42 | magnet or as reply to a Magnet URL 43 | zipmagnet or as reply to a Magnet URL 44 | filemagnet or as reply to a Magnet URL - Sends videos as files 45 | directdl or as reply to a Direct URL | optional custom file name 46 | direct or as reply to a Direct URL | optional custom file name 47 | zipdirectdl or as reply to a Direct URL | optional custom file name 48 | zipdirect or as reply to a Direct URL | optional custom file name 49 | filedirectdl or as reply to a Direct URL | optional custom file name - Sends videos as files 50 | filedirect or as reply to a Direct URL | optional custom file name - Sends videos as files 51 | cancel - or as reply to status message 52 | list - Lists your Ongoing Leeches. 53 | ``` 54 | 55 | **Other Modules** 56 | ``` 57 | help - to get organised help message 58 | 59 | ts - [search query] 60 | nyaa - [search query] 61 | nyaasi - [search query] 62 | sts - [search query] 63 | sukebei - [search query] 64 | 65 | thumbnail 66 | setthumbnail 67 | savethumbnail 68 | clearthumbnail 69 | rmthumbnail 70 | removethumbnail 71 | delthumbnail 72 | deletethumbnail 73 | 74 | watermark 75 | setwatermark 76 | savewatermark 77 | clearwatermark 78 | rmwatermark 79 | removewatermark 80 | delwatermark 81 | deletewatermark 82 | testwatermark 83 | ``` 84 | 85 | ## Credits 📍 86 | 87 | [@TheKneesocks](https://t.me/TheKneesocks)
88 | [@DeletedUser420](https://t.me/DeletedUser420)
89 | [WeebTime](https://github.com/WeebTime) on github for his [repo](https://github.com/WeebTime/Torrent-Bot-Lazyleech) 90 | 91 | ## Points To Be Noted 92 | 93 | - This repo is fork of [Anime Leeching Group](https://t.me/joinchat/BWHQ6lb_FmSP3pxfyYolfg) Bot Leafa-chan. 94 | - I dont own this repo, I have just Uploaded this code on github. 95 | - This Repo is meant for small groups. 96 | - Heroku Supported. 97 | - This Repo is licenced under [AGPL](https://github.com/ShinchanNohara1/Torrent-Bot-Lazyleech/blob/Master/LICENSE) that means you have to share this repo if anyone ask. 98 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Telegram Torrent Leecher", 3 | "description": "A Telegram Torrent Leecher and ytdl bot based on Pyrogram!", 4 | "keywords": [ 5 | "telegram" 6 | ], 7 | "repository": "https://github.com/lostb053/lazyleech", 8 | "stack": "container", 9 | "env":{ 10 | "API_ID":{ 11 | "description":"Get this value from https://my.telegram.org" 12 | }, 13 | "API_HASH":{ 14 | "description":"Get this value from https://my.telegram.org" 15 | }, 16 | "ADMIN_CHATS":{ 17 | "description":"Chats where this bot will respond and leech" 18 | }, 19 | "DB_URL":{ 20 | "description":"Mongodb url from https://cloud.mongodb.com/, guide: https://del.dog/mongodb_guide, if want to add auto download feature", 21 | "required":false 22 | }, 23 | "NYAA_RSS_LINKS":{ 24 | "description":"Add multiple nyaa links you wish to auto-download animes from, else leave it and will auto-download animes from AWS in all admin chats [Useless if no DB_URL added]", 25 | "required":false 26 | }, 27 | "RSS_RECHECK_INTERVAL":{ 28 | "description":"Add time in minutes, key is self-descriptive, set recheck interval else default is 5 minutes [Useless if no DB_URL added]", 29 | "required":false 30 | }, 31 | "BOT_TOKEN":{ 32 | "description":"Get this from https://t.me/botfather and enable Inline Mode" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /heroku.yml: -------------------------------------------------------------------------------- 1 | build: 2 | docker: 3 | worker: Dockerfile 4 | run: 5 | worker: bash run.sh 6 | -------------------------------------------------------------------------------- /lazyleech/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import aiohttp 4 | from io import BytesIO, StringIO 5 | from pyrogram import Client 6 | 7 | API_ID = os.environ.get('API_ID') 8 | API_HASH = os.environ.get('API_HASH') 9 | BOT_TOKEN = os.environ.get('BOT_TOKEN') 10 | TESTMODE = os.environ.get('TESTMODE') 11 | TESTMODE = TESTMODE and TESTMODE != '0' 12 | 13 | EVERYONE_CHATS = os.environ.get('EVERYONE_CHATS') 14 | EVERYONE_CHATS = list(map(int, EVERYONE_CHATS.split(' '))) if EVERYONE_CHATS else [-1001378211961] 15 | ADMIN_CHATS = os.environ.get('ADMIN_CHATS') 16 | ADMIN_CHATS = list(map(int, ADMIN_CHATS.split(' '))) if ADMIN_CHATS else [441422215] 17 | ALL_CHATS = EVERYONE_CHATS + ADMIN_CHATS 18 | # LICHER_* variables are for @animebatchstash and similar, not required 19 | LICHER_CHAT = os.environ.get('LICHER_CHAT', '') 20 | try: 21 | LICHER_CHAT = int(LICHER_CHAT) 22 | except ValueError: 23 | pass 24 | LICHER_STICKER = os.environ.get('LICHER_STICKER') 25 | LICHER_FOOTER = os.environ.get('LICHER_FOOTER', '').encode().decode('unicode_escape') 26 | LICHER_PARSE_EPISODE = os.environ.get('LICHER_PARSE_EPISODE') 27 | LICHER_PARSE_EPISODE = LICHER_PARSE_EPISODE and LICHER_PARSE_EPISODE != '0' 28 | 29 | PROGRESS_UPDATE_DELAY = int(os.environ.get('PROGRESS_UPDATE_DELAY', 5)) 30 | MAGNET_TIMEOUT = int(os.environ.get('LEECH_TIMEOUT', 60)) 31 | LEECH_TIMEOUT = int(os.environ.get('LEECH_TIMEOUT', 300)) 32 | ARIA2_SECRET = os.environ.get('ARIA2_SECRET', '') 33 | IGNORE_PADDING_FILE = os.environ.get('IGNORE_PADDING_FILE', '1') 34 | IGNORE_PADDING_FILE = IGNORE_PADDING_FILE and IGNORE_PADDING_FILE != '0' 35 | 36 | logging.basicConfig(level=logging.INFO) 37 | 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) 38 | session = aiohttp.ClientSession() 39 | help_dict = dict() 40 | preserved_logs = [] 41 | 42 | class SendAsZipFlag: 43 | pass 44 | 45 | class ForceDocumentFlag: 46 | pass 47 | 48 | def memory_file(name=None, contents=None, *, bytes=True): 49 | if isinstance(contents, str) and bytes: 50 | contents = contents.encode() 51 | file = BytesIO() if bytes else StringIO() 52 | if name: 53 | file.name = name 54 | if contents: 55 | file.write(contents) 56 | file.seek(0) 57 | return file 58 | -------------------------------------------------------------------------------- /lazyleech/__main__.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import asyncio 18 | import os 19 | import logging 20 | import traceback 21 | from pyrogram import idle 22 | from . import app, ADMIN_CHATS, preserved_logs 23 | from .utils.upload_worker import upload_worker 24 | 25 | async def main(): 26 | async def _autorestart_worker(): 27 | while True: 28 | try: 29 | await upload_worker() 30 | except Exception as ex: 31 | preserved_logs.append(ex) 32 | logging.exception('upload worker commited suicide') 33 | tb = traceback.format_exc() 34 | for i in ADMIN_CHATS: 35 | try: 36 | await app.send_message(i, 'upload worker commited suicide') 37 | await app.send_message(i, tb, parse_mode=None) 38 | except Exception: 39 | logging.exception('failed %s', i) 40 | tb = traceback.format_exc() 41 | asyncio.create_task(_autorestart_worker()) 42 | await app.start() 43 | await idle() 44 | await app.stop() 45 | if os.environ.get('DB_URL'): 46 | from plugins.nyaa_auto_download import _close_db 47 | _close_db() 48 | 49 | app.loop.run_until_complete(main()) 50 | -------------------------------------------------------------------------------- /lazyleech/plugins/autodetect.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import re 19 | import time 20 | import asyncio 21 | import tempfile 22 | from urllib.parse import urlsplit 23 | from pyrogram import Client, filters 24 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 25 | from .. import ALL_CHATS, SendAsZipFlag, ForceDocumentFlag 26 | from ..utils.misc import get_file_mimetype 27 | from ..utils import custom_filters 28 | from .leech import initiate_torrent, initiate_magnet 29 | 30 | NYAA_REGEX = re.compile(r'^(?:https?://)?(?P(?:www\.|sukebei\.)?nyaa\.si|nyaa\.squid\.workers\.dev)/(?:view|download)/(?P\d+)(?:[\./]torrent)?$') 31 | auto_detects = dict() 32 | @Client.on_message(filters.chat(ALL_CHATS), group=1) 33 | async def autodetect(client, message): 34 | text = message.text 35 | document = message.document 36 | link = None 37 | is_torrent = False 38 | if document: 39 | if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): 40 | os.makedirs(str(message.from_user.id), exist_ok=True) 41 | fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') 42 | os.fdopen(fd).close() 43 | await message.download(link) 44 | mimetype = await get_file_mimetype(link) 45 | is_torrent = True 46 | if mimetype != 'application/x-bittorrent': 47 | os.remove(link) 48 | link = None 49 | is_torrent = False 50 | if not link and text: 51 | match = NYAA_REGEX.match(text) 52 | if match: 53 | link = f'https://{match.group("base")}/download/{match.group("sauce")}.torrent' 54 | is_torrent = True 55 | else: 56 | splitted = urlsplit(text) 57 | if splitted.scheme == 'magnet' and splitted.query: 58 | link = text 59 | if link: 60 | reply = await message.reply_text(f'{"Torrent" if is_torrent else "Magnet"} detected. Select upload method', reply_markup=InlineKeyboardMarkup([ 61 | [InlineKeyboardButton('Individual Files', 'autodetect_individual'), InlineKeyboardButton('Zip', 'autodetect_zip'), InlineKeyboardButton('Force Document', 'autodetect_file')], 62 | [InlineKeyboardButton('Delete', 'autodetect_delete')] 63 | ])) 64 | auto_detects[(reply.chat.id, reply.message_id)] = link, message.from_user.id, (initiate_torrent if is_torrent else initiate_magnet) 65 | 66 | answered = set() 67 | answer_lock = asyncio.Lock() 68 | @Client.on_callback_query(custom_filters.callback_data(['autodetect_individual', 'autodetect_zip', 'autodetect_file', 'autodetect_delete']) & custom_filters.callback_chat(ALL_CHATS)) 69 | async def autodetect_callback(client, callback_query): 70 | message = callback_query.message 71 | identifier = (message.chat.id, message.message_id) 72 | result = auto_detects.get(identifier) 73 | if not result: 74 | await callback_query.answer('I can\'t get your message, please try again.', show_alert=True, cache_time=3600) 75 | return 76 | link, user_id, init_func = result 77 | if callback_query.from_user.id != user_id: 78 | await callback_query.answer('...no', cache_time=3600) 79 | return 80 | async with answer_lock: 81 | if identifier in answered: 82 | await callback_query.answer('...no') 83 | return 84 | answered.add(identifier) 85 | asyncio.create_task(message.delete()) 86 | data = callback_query.data 87 | start_leech = data in ('autodetect_individual', 'autodetect_zip', 'autodetect_file') 88 | if start_leech: 89 | if getattr(message.reply_to_message, 'empty', True): 90 | await callback_query.answer('Don\'t delete your message!', show_alert=True) 91 | return 92 | if data == 'autodetect_zip': 93 | flags = (SendAsZipFlag,) 94 | elif data == 'autodetect_file': 95 | flags = (ForceDocumentFlag,) 96 | else: 97 | flags = () 98 | await asyncio.gather(callback_query.answer(), init_func(client, message.reply_to_message, link, flags)) 99 | -------------------------------------------------------------------------------- /lazyleech/plugins/help.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import asyncio 18 | from pyrogram import Client, filters 19 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 20 | from .. import ALL_CHATS, help_dict 21 | from ..utils import custom_filters 22 | 23 | @Client.on_message(filters.command('help') & filters.chat(ALL_CHATS)) 24 | async def help_cmd(client, message): 25 | module = message.text.split(' ', 1) 26 | module.pop(0) 27 | try: 28 | module = module[0].lower().strip() 29 | except IndexError: 30 | module = None 31 | for internal_name in help_dict: 32 | external_name, text = help_dict[internal_name] 33 | external_name = external_name.lower().strip() 34 | internal_name = internal_name.lower().strip() 35 | if module in (internal_name, external_name): 36 | buttons = [ 37 | [InlineKeyboardButton('Back', 'help_back')] 38 | ] 39 | break 40 | else: 41 | module = None 42 | text = 'Select the module you want help with' 43 | buttons = [] 44 | to_append = [] 45 | for internal_name in help_dict: 46 | external_name, _ = help_dict[internal_name] 47 | to_append.append(InlineKeyboardButton(external_name.strip(), f'help_m{internal_name}')) 48 | if len(to_append) > 2: 49 | buttons.append(to_append) 50 | to_append = [] 51 | if to_append: 52 | buttons.append(to_append) 53 | reply = await message.reply_text(text, reply_markup=InlineKeyboardMarkup(buttons)) 54 | callback_info[(reply.chat.id, reply.message_id)] = message.from_user.id, module 55 | 56 | callback_lock = asyncio.Lock() 57 | callback_info = dict() 58 | @Client.on_callback_query(custom_filters.callback_data('help_back') & custom_filters.callback_chat(ALL_CHATS)) 59 | async def help_back(client, callback_query): 60 | message = callback_query.message 61 | message_identifier = (message.chat.id, message.message_id) 62 | if message_identifier not in callback_info: 63 | await callback_query.answer('This help message is too old that I don\'t have info on it.', show_alert=True, cache_time=3600) 64 | return 65 | async with callback_lock: 66 | info = callback_info.get((message.chat.id, message.message_id)) 67 | user_id, location = info 68 | if user_id != callback_query.from_user.id: 69 | await callback_query.answer('...no', cache_time=3600) 70 | return 71 | if location is not None: 72 | buttons = [] 73 | to_append = [] 74 | for internal_name in help_dict: 75 | external_name, _ = help_dict[internal_name] 76 | to_append.append(InlineKeyboardButton(external_name.strip(), f'help_m{internal_name}')) 77 | if len(to_append) > 2: 78 | buttons.append(to_append) 79 | to_append = [] 80 | if to_append: 81 | buttons.append(to_append) 82 | await message.edit_text('Select the module you want help with.', reply_markup=InlineKeyboardMarkup(buttons)) 83 | callback_info[message_identifier] = user_id, None 84 | await callback_query.answer() 85 | 86 | @Client.on_callback_query(filters.regex('help_m.+') & custom_filters.callback_chat(ALL_CHATS)) 87 | async def help_m(client, callback_query): 88 | message = callback_query.message 89 | message_identifier = (message.chat.id, message.message_id) 90 | if message_identifier not in callback_info: 91 | await callback_query.answer('This help message is too old that I don\'t have info on it.', show_alert=True, cache_time=3600) 92 | return 93 | async with callback_lock: 94 | info = callback_info.get((message.chat.id, message.message_id)) 95 | user_id, location = info 96 | if user_id != callback_query.from_user.id: 97 | await callback_query.answer('...no', cache_time=3600) 98 | return 99 | module = callback_query.data[6:] 100 | if module not in help_dict: 101 | await callback_query.answer('What module?') 102 | return 103 | if module != location: 104 | await message.edit_text(help_dict[module][1], reply_markup=InlineKeyboardMarkup([ 105 | [InlineKeyboardButton('Back', 'help_back')] 106 | ])) 107 | callback_info[message_identifier] = user_id, module 108 | await callback_query.answer() 109 | -------------------------------------------------------------------------------- /lazyleech/plugins/leech.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import time 19 | import html 20 | import asyncio 21 | import tempfile 22 | from urllib.parse import urlparse, urlunparse, unquote as urldecode 23 | from pyrogram import Client, filters 24 | from pyrogram.parser import html as pyrogram_html 25 | from .. import ADMIN_CHATS, ALL_CHATS, PROGRESS_UPDATE_DELAY, session, help_dict, LEECH_TIMEOUT, MAGNET_TIMEOUT, SendAsZipFlag, ForceDocumentFlag 26 | 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 27 | from ..utils.misc import format_bytes, get_file_mimetype, return_progress_string, calculate_eta, allow_admin_cancel 28 | from ..utils.upload_worker import upload_queue, upload_statuses, progress_callback_data, upload_waits, stop_uploads 29 | 30 | @Client.on_message(filters.command(['torrent', 'ziptorrent', 'filetorrent']) & filters.chat(ALL_CHATS)) 31 | async def torrent_cmd(client, message): 32 | text = (message.text or message.caption).split(None, 1) 33 | command = text.pop(0).lower() 34 | if 'zip' in command: 35 | flags = (SendAsZipFlag,) 36 | elif 'file' in command: 37 | flags = (ForceDocumentFlag,) 38 | else: 39 | flags = () 40 | link = None 41 | reply = message.reply_to_message 42 | document = message.document 43 | if document: 44 | if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): 45 | os.makedirs(str(message.from_user.id), exist_ok=True) 46 | fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') 47 | os.fdopen(fd).close() 48 | await message.download(link) 49 | mimetype = await get_file_mimetype(link) 50 | if mimetype != 'application/x-bittorrent': 51 | os.remove(link) 52 | link = None 53 | if not link: 54 | if text: 55 | link = text[0].strip() 56 | elif not getattr(reply, 'empty', True): 57 | document = reply.document 58 | link = reply.text 59 | if document: 60 | if document.file_size < 1048576 and document.file_name.endswith('.torrent') and (not document.mime_type or document.mime_type == 'application/x-bittorrent'): 61 | os.makedirs(str(message.from_user.id), exist_ok=True) 62 | fd, link = tempfile.mkstemp(dir=str(message.from_user.id), suffix='.torrent') 63 | os.fdopen(fd).close() 64 | await reply.download(link) 65 | mimetype = await get_file_mimetype(link) 66 | if mimetype != 'application/x-bittorrent': 67 | os.remove(link) 68 | link = reply.text or reply.caption 69 | if not link: 70 | await message.reply_text('''Usage: 71 | - /torrent <Torrent URL or File> 72 | - /torrent (as reply to a Torrent URL or file) 73 | 74 | - /ziptorrent <Torrent URL or File> 75 | - /ziptorrent (as reply to a Torrent URL or File) 76 | 77 | - /filetorrent <Torrent URL or File> - Sends videos as files 78 | - /filetorrent (as reply to a Torrent URL or file) - Sends videos as files''') 79 | return 80 | await initiate_torrent(client, message, link, flags) 81 | await message.stop_propagation() 82 | 83 | async def initiate_torrent(client, message, link, flags): 84 | user_id = message.from_user.id 85 | reply = await message.reply_text('Adding torrent...') 86 | try: 87 | gid = await aria2_add_torrent(session, user_id, link, LEECH_TIMEOUT) 88 | except Aria2Error as ex: 89 | await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) 90 | return 91 | finally: 92 | if os.path.isfile(link): 93 | os.remove(link) 94 | await handle_leech(client, message, gid, reply, user_id, flags) 95 | 96 | @Client.on_message(filters.command(['magnet', 'zipmagnet', 'filemagnet']) & filters.chat(ALL_CHATS)) 97 | async def magnet_cmd(client, message): 98 | text = (message.text or message.caption).split(None, 1) 99 | command = text.pop(0).lower() 100 | if 'zip' in command: 101 | flags = (SendAsZipFlag,) 102 | elif 'file' in command: 103 | flags = (ForceDocumentFlag,) 104 | else: 105 | flags = () 106 | link = None 107 | reply = message.reply_to_message 108 | if text: 109 | link = text[0].strip() 110 | elif not getattr(reply, 'empty', True): 111 | link = reply.text or reply.caption 112 | if not link: 113 | await message.reply_text('''Usage: 114 | - /magnet <Magnet URL> 115 | - /magnet (as reply to a Magnet URL) 116 | 117 | - /zipmagnet <Magnet URL> 118 | - /zipmagnet (as reply to a Magnet URL) 119 | 120 | - /filemagnet <Magnet URL> - Sends videos as files 121 | - /filemagnet (as reply to a Magnet URL) - Sends videos as files''') 122 | return 123 | await initiate_magnet(client, message, link, flags) 124 | 125 | async def initiate_magnet(client, message, link, flags): 126 | user_id = message.from_user.id 127 | reply = await message.reply_text('Adding magnet...') 128 | try: 129 | gid = await asyncio.wait_for(aria2_add_magnet(session, user_id, link, LEECH_TIMEOUT), MAGNET_TIMEOUT) 130 | except Aria2Error as ex: 131 | await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) 132 | except asyncio.TimeoutError: 133 | await asyncio.gather(message.reply_text('Magnet timed out'), reply.delete()) 134 | else: 135 | await handle_leech(client, message, gid, reply, user_id, flags) 136 | 137 | @Client.on_message(filters.command(['directdl', 'direct', 'zipdirectdl', 'zipdirect', 'filedirectdl', 'filedirect']) & filters.chat(ALL_CHATS)) 138 | async def directdl_cmd(client, message): 139 | text = message.text.split(None, 1) 140 | command = text.pop(0).lower() 141 | if 'zip' in command: 142 | flags = (SendAsZipFlag,) 143 | elif 'file' in command: 144 | flags = (ForceDocumentFlag,) 145 | else: 146 | flags = () 147 | link = filename = None 148 | reply = message.reply_to_message 149 | if text: 150 | link = text[0].strip() 151 | elif not getattr(reply, 'empty', True): 152 | link = reply.text 153 | if not link: 154 | await message.reply_text('''Usage: 155 | - /directdl <Direct URL> | optional custom file name 156 | - /directdl (as reply to a Direct URL) | optional custom file name 157 | - /direct <Direct URL> | optional custom file name 158 | - /direct (as reply to a Direct URL) | optional custom file name 159 | 160 | - /zipdirectdl <Direct URL> | optional custom file name 161 | - /zipdirectdl (as reply to a Direct URL) | optional custom file name 162 | - /zipdirect <Direct URL> | optional custom file name 163 | - /zipdirect (as reply to a Direct URL) | optional custom file name 164 | 165 | - /filedirectdl <Direct URL> | optional custom file name - Sends videos as files 166 | - /filedirectdl (as reply to a Direct URL) | optional custom file name - Sends videos as files 167 | - /filedirect <Direct URL> | optional custom file name - Sends videos as files 168 | - /filedirect (as reply to a Direct URL) | optional custom file name - Sends videos as files''') 169 | return 170 | split = link.split('|', 1) 171 | if len(split) > 1: 172 | filename = os.path.basename(split[1].strip()) 173 | link = split[0].strip() 174 | parsed = list(urlparse(link, 'https')) 175 | if parsed[0] == 'magnet': 176 | if SendAsZipFlag in flags: 177 | prefix = 'zip' 178 | elif ForceDocumentFlag in flags: 179 | prefix = 'file' 180 | else: 181 | prefix = '' 182 | await message.reply_text(f'Use /{prefix}magnet instead') 183 | return 184 | if not parsed[0]: 185 | parsed[0] = 'https' 186 | if parsed[0] not in ('http', 'https'): 187 | await message.reply_text('Invalid scheme') 188 | return 189 | link = urlunparse(parsed) 190 | await initiate_directdl(client, message, link, filename, flags) 191 | 192 | async def initiate_directdl(client, message, link, filename, flags): 193 | user_id = message.from_user.id 194 | reply = await message.reply_text('Adding url...') 195 | try: 196 | gid = await asyncio.wait_for(aria2_add_directdl(session, user_id, link, filename, LEECH_TIMEOUT), MAGNET_TIMEOUT) 197 | except Aria2Error as ex: 198 | await asyncio.gather(message.reply_text(f'Aria2 Error Occured!\n{ex.error_code}: {html.escape(ex.error_message)}'), reply.delete()) 199 | except asyncio.TimeoutError: 200 | await asyncio.gather(message.reply_text('Connection timed out'), reply.delete()) 201 | else: 202 | await handle_leech(client, message, gid, reply, user_id, flags) 203 | 204 | leech_statuses = dict() 205 | async def handle_leech(client, message, gid, reply, user_id, flags): 206 | prevtext = None 207 | torrent_info = await aria2_tell_status(session, gid) 208 | last_edit = 0 209 | start_time = time.time() 210 | message_identifier = (reply.chat.id, reply.message_id) 211 | leech_statuses[message_identifier] = gid 212 | download_speed = None 213 | while torrent_info['status'] in ('active', 'waiting', 'paused'): 214 | if torrent_info.get('seeder') == 'true': 215 | break 216 | status = torrent_info['status'].capitalize() 217 | total_length = int(torrent_info['totalLength']) 218 | completed_length = int(torrent_info['completedLength']) 219 | download_speed = format_bytes(torrent_info['downloadSpeed']) + '/s' 220 | if total_length: 221 | formatted_total_length = format_bytes(total_length) 222 | else: 223 | formatted_total_length = 'Unknown' 224 | formatted_completed_length = format_bytes(completed_length) 225 | seeders = torrent_info.get('numSeeders') 226 | peers = torrent_info.get('connections') 227 | if torrent_info.get('bittorrent'): 228 | tor_name = torrent_info['bittorrent']['info']['name'] 229 | else: 230 | tor_name = os.path.basename(torrent_info['files'][0]['path']) 231 | if not tor_name: 232 | tor_name = urldecode(os.path.basename(urlparse(torrent_info['files'][0]['uris'][0]['uri']).path)) 233 | text = f'''{html.escape(tor_name)} 234 | {html.escape(return_progress_string(completed_length, total_length))} 235 | 236 | GID: {gid} 237 | Status: {status} 238 | Total Size: {formatted_total_length} 239 | Downloaded Size: {formatted_completed_length} 240 | Download Speed: {download_speed} 241 | ETA: {calculate_eta(completed_length, total_length, start_time)}''' 242 | if seeders is not None: 243 | text += f'\nSeeders: {seeders}' 244 | if peers is not None: 245 | text += f'\n{"Peers" if seeders is not None else "Connections"}: {peers}' 246 | if (time.time() - last_edit) > PROGRESS_UPDATE_DELAY and text != prevtext: 247 | await reply.edit_text(text) 248 | prevtext = text 249 | last_edit = time.time() 250 | torrent_info = await aria2_tell_status(session, gid) 251 | if torrent_info['status'] == 'error': 252 | error_code = torrent_info['errorCode'] 253 | error_message = torrent_info['errorMessage'] 254 | text = f'Aria2 Error Occured!\n{error_code}: {html.escape(error_message)}' 255 | if error_code == '7' and not error_message and torrent_info['downloadSpeed'] == '0': 256 | text += '\n\nThis error may have been caused due to the torrent being too slow' 257 | await asyncio.gather( 258 | message.reply_text(text), 259 | reply.delete() 260 | ) 261 | elif torrent_info['status'] == 'removed': 262 | await asyncio.gather( 263 | message.reply_text('Your download has been manually cancelled.'), 264 | reply.delete() 265 | ) 266 | else: 267 | leech_statuses.pop(message_identifier) 268 | task = None 269 | if upload_queue._unfinished_tasks: 270 | task = asyncio.create_task(reply.edit_text('Download successful, waiting for queue...')) 271 | upload_queue.put_nowait((client, message, reply, torrent_info, user_id, flags)) 272 | try: 273 | await aria2_remove(session, gid) 274 | except Aria2Error as ex: 275 | if not (ex.error_code == 1 and ex.error_message == f'Active Download not found for GID#{gid}'): 276 | raise 277 | finally: 278 | if task: 279 | await task 280 | 281 | @Client.on_message(filters.command('list') & filters.chat(ALL_CHATS)) 282 | async def list_leeches(client, message): 283 | user_id = message.from_user.id 284 | text = '' 285 | quote = None 286 | parser = pyrogram_html.HTML(client) 287 | for i in await aria2_tell_active(session): 288 | if i.get('bittorrent'): 289 | info = i['bittorrent'].get('info') 290 | if not info: 291 | continue 292 | tor_name = info['name'] 293 | else: 294 | tor_name = os.path.basename(i['files'][0]['path']) 295 | if not tor_name: 296 | tor_name = urldecode(os.path.basename(urlparse(i['files'][0]['uris'][0]['uri']).path)) 297 | a = f'''{html.escape(tor_name)} 298 | {i['gid']}\n\n''' 299 | futtext = text + a 300 | if len((await parser.parse(futtext))['message']) > 4096: 301 | await message.reply_text(text, quote=quote) 302 | quote = False 303 | futtext = a 304 | text = futtext 305 | if not text: 306 | text = 'No leeches found.' 307 | await message.reply_text(text, quote=quote) 308 | 309 | @Client.on_message(filters.command('cancel') & filters.chat(ALL_CHATS)) 310 | async def cancel_leech(client, message): 311 | user_id = message.from_user.id 312 | gid = None 313 | reply = message.reply_to_message 314 | if len(message.command) == 2: 315 | gid = message.command[1] 316 | elif len(message.command) == 3 or not getattr(reply, 'empty', True): 317 | if len(message.command) == 3: 318 | try: 319 | reply_identifier = tuple(filter(int, message.command[1:3])) 320 | except ValueError: 321 | # goes through to the gid check with no gid, showing usage info and returning 322 | reply_identifier = None 323 | else: 324 | reply_identifier = (reply.chat.id, reply.message_id) 325 | task = upload_statuses.get(reply_identifier) 326 | if task: 327 | task, starter_id = task 328 | if user_id != starter_id and not await allow_admin_cancel(message.chat.id, user_id): 329 | await message.reply_text('You did not start this leech.') 330 | else: 331 | task.cancel() 332 | return 333 | result = progress_callback_data.get(reply_identifier) 334 | if result: 335 | if user_id != result[3] and not await allow_admin_cancel(message.chat.id, user_id): 336 | await message.reply_text('You did not start this leech.') 337 | else: 338 | stop_uploads.add(reply_identifier) 339 | await message.reply_text('Cancelled!') 340 | return 341 | starter_id = upload_waits.get(reply_identifier) 342 | if starter_id: 343 | if user_id != starter_id[0] and not await allow_admin_cancel(message.chat.id, user_id): 344 | await message.reply_text('You did not start this leech.') 345 | else: 346 | stop_uploads.add(reply_identifier) 347 | await message.reply_text('Cancelled!') 348 | return 349 | gid = leech_statuses.get(reply_identifier) 350 | if not gid: 351 | await message.reply_text('''Usage: 352 | /cancel <GID> 353 | /cancel <chat id> <message id> 354 | /cancel (as reply to status message)''') 355 | return 356 | if not is_gid_owner(user_id, gid) and not await allow_admin_cancel(message.chat.id, user_id): 357 | await message.reply_text('You did not start this leech.') 358 | return 359 | await aria2_remove(session, gid) 360 | 361 | help_dict['leech'] = ('Leech', 362 | '''/torrent <Torrent URL or File> 363 | /torrent (as reply to a Torrent URL or file) 364 | 365 | /ziptorrent <Torrent URL or File> 366 | /ziptorrent (as reply to a Torrent URL or File) 367 | 368 | /filetorrent <Torrent URL or File> - Sends videos as files 369 | /filetorrent (as reply to a Torrent URL or File) - Sends videos as files 370 | 371 | /magnet <Magnet URL> 372 | /magnet (as reply to a Magnet URL) 373 | 374 | /zipmagnet <Magnet URL> 375 | /zipmagnet (as reply to a Magnet URL) 376 | 377 | /filemagnet <Magnet URL> - Sends videos as files 378 | /filemagnet (as reply to a Magnet URL) - Sends videos as files 379 | 380 | /directdl <Direct URL> | optional custom file name 381 | /directdl (as reply to a Direct URL) | optional custom file name 382 | /direct <Direct URL> | optional custom file name 383 | /direct (as reply to a Direct URL) | optional custom file name 384 | 385 | /zipdirectdl <Direct URL> | optional custom file name 386 | /zipdirectdl (as reply to a Direct URL) | optional custom file name 387 | /zipdirect <Direct URL> | optional custom file name 388 | /zipdirect (as reply to a Direct URL) | optional custom file name 389 | 390 | /filedirectdl <Direct URL> | optional custom file name - Sends videos as files 391 | /filedirectdl (as reply to a Direct URL) | optional custom file name - Sends videos as files 392 | /filedirect <Direct URL> | optional custom file name - Sends videos as files 393 | /filedirect (as reply to a Direct URL) | optional custom file name - Sends videos as files 394 | 395 | /cancel <GID> 396 | /cancel <chat id> <message id> 397 | /cancel (as reply to status message) 398 | 399 | /list - Lists all current leeches''') 400 | -------------------------------------------------------------------------------- /lazyleech/plugins/mediainfo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shlex 3 | import asyncio 4 | from typing import Tuple 5 | from pyrogram import Client, filters 6 | from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup 7 | from .. import ALL_CHATS 8 | from html_telegraph_poster import TelegraphPoster 9 | 10 | 11 | async def runcmd(cmd: str) -> Tuple[str, str, int, int]: 12 | """run command in terminal""" 13 | args = shlex.split(cmd) 14 | process = await asyncio.create_subprocess_exec( 15 | *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE 16 | ) 17 | stdout, stderr = await process.communicate() 18 | return ( 19 | stdout.decode("utf-8", "replace").strip(), 20 | stderr.decode("utf-8", "replace").strip(), 21 | process.returncode, 22 | process.pid, 23 | ) 24 | 25 | 26 | def safe_filename(path_): 27 | if path_ is None: 28 | return 29 | safename = path_.replace("'", "").replace('"', "") 30 | if safename != path_: 31 | os.rename(path_, safename) 32 | return safename 33 | 34 | 35 | def post_to_telegraph(a_title: str, content: str) -> str: 36 | """Create a Telegram Post using HTML Content""" 37 | post_client = TelegraphPoster(use_api=True) 38 | auth_name = "Lazyleech" 39 | post_client.create_api_token(auth_name) 40 | post_page = post_client.post( 41 | title=a_title, 42 | author=auth_name, 43 | author_url="https://t.me/lostb053", 44 | text=content, 45 | ) 46 | return post_page["url"] 47 | 48 | 49 | @Client.on_message(filters.command('mediainfo') & filters.chat(ALL_CHATS)) 50 | async def mediainfo(client, message): 51 | reply = message.reply_to_message 52 | if not reply: 53 | await message.reply_text("Reply to Media first") 54 | return 55 | process = await message.reply_text("Processing..") 56 | x_media = None 57 | available_media = ( 58 | "audio", 59 | "document", 60 | "photo", 61 | "sticker", 62 | "animation", 63 | "video", 64 | "voice", 65 | "video_note", 66 | "new_chat_photo", 67 | ) 68 | for kind in available_media: 69 | x_media = getattr(reply, kind, None) 70 | if x_media is not None: 71 | break 72 | if x_media is None: 73 | await process.edit_text("Reply To a Valid Media Format") 74 | return 75 | media_type = reply.video.file_name 76 | file_path = safe_filename(await reply.download()) 77 | output_ = await runcmd(f'mediainfo "{file_path}"') 78 | out = None 79 | if len(output_) != 0: 80 | out = output_[0] 81 | body_text = f""" 82 |

JSON

83 |
{x_media}
84 |
85 |

DETAILS

86 |
{out or 'Not Supported'}
87 | """ 88 | text_ = media_type.split(".")[-1].upper() 89 | link = post_to_telegraph(media_type, body_text) 90 | markup = InlineKeyboardMarkup([[InlineKeyboardButton(text=text_, url=link)]]) 91 | await process.edit_text("✨ MEDIA INFO", reply_markup=markup) 92 | -------------------------------------------------------------------------------- /lazyleech/plugins/nyaa.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import time 18 | import html 19 | import asyncio 20 | import feedparser 21 | from urllib.parse import quote as urlencode, urlsplit 22 | from pyrogram import Client, filters 23 | from pyrogram.parser import html as pyrogram_html 24 | from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton 25 | from .. import ALL_CHATS, session, help_dict 26 | from ..utils import custom_filters 27 | 28 | search_lock = asyncio.Lock() 29 | search_info = {False: dict(), True: dict()} 30 | async def return_search(query, page=1, sukebei=False): 31 | page -= 1 32 | query = query.lower().strip() 33 | used_search_info = search_info[sukebei] 34 | async with search_lock: 35 | results, get_time = used_search_info.get(query, (None, 0)) 36 | if (time.time() - get_time) > 3600: 37 | results = [] 38 | async with session.get(f'https://{"sukebei." if sukebei else ""}nyaa.si/?page=rss&q={urlencode(query)}') as resp: 39 | d = feedparser.parse(await resp.text()) 40 | text = '' 41 | a = 0 42 | parser = pyrogram_html.HTML(None) 43 | for i in sorted(d['entries'], key=lambda i: int(i['nyaa_seeders']), reverse=True): 44 | if i['nyaa_size'].startswith('0'): 45 | continue 46 | if not int(i['nyaa_seeders']): 47 | break 48 | link = i['link'] 49 | splitted = urlsplit(link) 50 | if splitted.scheme == 'magnet' and splitted.query: 51 | link = f'{link}' 52 | newtext = f'''{a + 1}. {html.escape(i["title"])} 53 | ▸ Link: {link} 54 | ▸ Size: {i["nyaa_size"]} 55 | ▸ Seeders: {i["nyaa_seeders"]} 56 | ▸ Leechers: {i["nyaa_leechers"]} 57 | ▸ Category: {i["nyaa_category"]}\n\n''' 58 | futtext = text + newtext 59 | if (a and not a % 10) or len((await parser.parse(futtext))['message']) > 4096: 60 | results.append(text) 61 | futtext = newtext 62 | text = futtext 63 | a += 1 64 | results.append(text) 65 | ttl = time.time() 66 | used_search_info[query] = results, ttl 67 | try: 68 | return results[page], len(results), ttl 69 | except IndexError: 70 | return '', len(results), ttl 71 | 72 | message_info = dict() 73 | ignore = set() 74 | @Client.on_message(filters.command(['ts', 'nyaa', 'nyaasi'])) 75 | async def nyaa_search(client, message): 76 | text = message.text.split(' ') 77 | text.pop(0) 78 | query = ' '.join(text) 79 | await init_search(client, message, query, False) 80 | 81 | @Client.on_message(filters.command(['sts', 'sukebei'])) 82 | async def nyaa_search_sukebei(client, message): 83 | text = message.text.split(' ') 84 | text.pop(0) 85 | query = ' '.join(text) 86 | await init_search(client, message, query, True) 87 | 88 | async def init_search(client, message, query, sukebei): 89 | result, pages, ttl = await return_search(query, sukebei=sukebei) 90 | if not result: 91 | await message.reply_text('No results found') 92 | else: 93 | buttons = [InlineKeyboardButton(f'1/{pages}', 'nyaa_nop'), InlineKeyboardButton('Next', 'nyaa_next')] 94 | if pages == 1: 95 | buttons.pop() 96 | reply = await message.reply_text(result, reply_markup=InlineKeyboardMarkup([ 97 | buttons 98 | ])) 99 | message_info[(reply.chat.id, reply.message_id)] = message.from_user.id, ttl, query, 1, pages, sukebei 100 | 101 | @Client.on_callback_query(custom_filters.callback_data('nyaa_nop')) 102 | async def nyaa_nop(client, callback_query): 103 | await callback_query.answer(cache_time=3600) 104 | 105 | callback_lock = asyncio.Lock() 106 | @Client.on_callback_query(custom_filters.callback_data(['nyaa_back', 'nyaa_next'])) 107 | async def nyaa_callback(client, callback_query): 108 | message = callback_query.message 109 | message_identifier = (message.chat.id, message.message_id) 110 | data = callback_query.data 111 | async with callback_lock: 112 | if message_identifier in ignore: 113 | await callback_query.answer() 114 | return 115 | user_id, ttl, query, current_page, pages, sukebei = message_info.get(message_identifier, (None, 0, None, 0, 0, None)) 116 | og_current_page = current_page 117 | if data == 'nyaa_back': 118 | current_page -= 1 119 | elif data == 'nyaa_next': 120 | current_page += 1 121 | if current_page < 1: 122 | current_page = 1 123 | elif current_page > pages: 124 | current_page = pages 125 | ttl_ended = (time.time() - ttl) > 3600 126 | if ttl_ended: 127 | text = getattr(message.text, 'html', 'Search expired') 128 | else: 129 | if callback_query.from_user.id != user_id: 130 | await callback_query.answer('...no', cache_time=3600) 131 | return 132 | text, pages, ttl = await return_search(query, current_page, sukebei) 133 | buttons = [InlineKeyboardButton('Back', 'nyaa_back'), InlineKeyboardButton(f'{current_page}/{pages}', 'nyaa_nop'), InlineKeyboardButton('Next', 'nyaa_next')] 134 | if ttl_ended: 135 | buttons = [InlineKeyboardButton('Search Expired', 'nyaa_nop')] 136 | else: 137 | if current_page == 1: 138 | buttons.pop(0) 139 | if current_page == pages: 140 | buttons.pop() 141 | if ttl_ended or current_page != og_current_page: 142 | await callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup([ 143 | buttons 144 | ])) 145 | message_info[message_identifier] = user_id, ttl, query, current_page, pages, sukebei 146 | if ttl_ended: 147 | ignore.add(message_identifier) 148 | await callback_query.answer() 149 | 150 | help_dict['nyaa'] = ('Nyaa.si', 151 | '''/ts [search query] 152 | /nyaa [search query] 153 | /nyaasi [search query] 154 | 155 | /sts [search query] 156 | /sukebei [search query]''') 157 | -------------------------------------------------------------------------------- /lazyleech/plugins/nyaa_auto_download.py: -------------------------------------------------------------------------------- 1 | ### if you wish to disable this just fork repo and delete this plugin/file 2 | ### if you want different uploader, just replace rsslink 3 | 4 | import os 5 | import requests 6 | import re 7 | from bs4 import BeautifulSoup as bs 8 | from motor.motor_asyncio import AsyncIOMotorClient 9 | from motor.core import AgnosticClient, AgnosticDatabase, AgnosticCollection 10 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 11 | from .. import app, ADMIN_CHATS, ForceDocumentFlag 12 | from .leech import initiate_torrent 13 | 14 | rsslink = list(filter(lambda x: x, map(str, os.environ.get("NYAA_RSS_LINKS", "https://nyaa.si/?page=rss&c=0_0&f=0&u=AkihitoSubsWeeklies").split(' ')))) 15 | 16 | if os.environ.get('DB_URL'): 17 | DB_URL = os.environ.get('DB_URL') 18 | _MGCLIENT: AgnosticClient = AsyncIOMotorClient(DB_URL) 19 | _DATABASE: AgnosticDatabase = _MGCLIENT["ASWFeed"] 20 | def get_collection(name: str) -> AgnosticCollection: 21 | """ Create or Get Collection from your database """ 22 | return _DATABASE[name] 23 | def _close_db() -> None: 24 | _MGCLIENT.close() 25 | 26 | A = get_collection('ASW_TITLE') 27 | 28 | async def rss_parser(): 29 | cr = [] 30 | for i in rsslink: 31 | da = bs(requests.get(i).text, features="html.parser") 32 | if (await A.find_one({'site':i})) is None: 33 | await A.insert_one({'_id': str(da.find('item').find('title')), 'site': i}) 34 | return 35 | count_a = 0 36 | for ii in da.findAll('item'): 37 | if (await A.find_one({'site': i}))['_id'] == str(ii.find('title')): 38 | break 39 | cr.append([str(ii.find('title')), (re.sub(r'<.*?>(.*)<.*?>', r'\1', str(ii.find('guid')))).replace('view', 'download')+'.torrent']) 40 | count_a+=1 41 | if count_a!=0: 42 | await A.find_one_and_delete({'site': i}) 43 | await A.insert_one({'_id': str(da.find('item').find('title')), 'site': i}) 44 | for i in cr: 45 | for ii in ADMIN_CHATS: 46 | try: 47 | msg = await app.send_message(ii, f"New anime uploaded\n\n{i[0]}\n{i[1]}") 48 | flags = (ForceDocumentFlag,) 49 | await initiate_torrent(app, msg, i[1], flags) 50 | except: 51 | pass 52 | 53 | scheduler = AsyncIOScheduler() 54 | scheduler.add_job(rss_parser, "interval", minutes=int(os.environ.get('RSS_RECHECK_INTERVAL', 5)), max_instances=5) 55 | scheduler.start() 56 | -------------------------------------------------------------------------------- /lazyleech/plugins/ping.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyrogram import Client, filters 18 | from .. import ALL_CHATS 19 | 20 | @Client.on_message(filters.command('ping') & filters.chat(ALL_CHATS)) 21 | async def ping_pong(client, message): 22 | await message.reply_text('Pong') 23 | -------------------------------------------------------------------------------- /lazyleech/plugins/pyexec.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | # https://greentreesnakes.readthedocs.io/ 18 | import ast 19 | import sys 20 | import html 21 | import inspect 22 | import traceback 23 | from pyrogram import Client, filters 24 | from .. import ADMIN_CHATS, memory_file 25 | 26 | @Client.on_message(filters.command('exec') & filters.chat(ADMIN_CHATS)) 27 | async def run_code(client, message): 28 | class UniqueExecReturnIdentifier: 29 | pass 30 | code = message.text[5:].strip() 31 | if not code: 32 | await message.reply_text('code 100') 33 | return 34 | tree = ast.parse(code) 35 | obody = tree.body 36 | body = obody.copy() 37 | body.append(ast.Return(ast.Name('_ueri', ast.Load()))) 38 | def _gf(body): 39 | # args: m, message, c, client, _ueri 40 | func = ast.AsyncFunctionDef('ex', ast.arguments([], [ast.arg(i, None, None) for i in ['m', 'message', 'c', 'client', '_ueri']], None, [], [], None, []), body, [], None, None) 41 | ast.fix_missing_locations(func) 42 | mod = ast.parse('') 43 | mod.body = [func] 44 | fl = locals().copy() 45 | exec(compile(mod, '', 'exec'), globals(), fl) 46 | return fl['ex'] 47 | try: 48 | exx = _gf(body) 49 | except SyntaxError as ex: 50 | if ex.msg != "'return' with value in async generator": 51 | raise 52 | exx = _gf(obody) 53 | escaped_code = html.escape(code) 54 | async_obj = exx(message, message, client, client, UniqueExecReturnIdentifier) 55 | reply = await message.reply_text('Type[py]\n{}\nState[Executing]'.format(escaped_code)) 56 | stdout = sys.stdout 57 | stderr = sys.stderr 58 | wrapped_stdout = memory_file(bytes=False) 59 | wrapped_stdout.buffer = memory_file() 60 | wrapped_stderr = memory_file(bytes=False) 61 | wrapped_stderr.buffer = memory_file() 62 | sys.stdout = wrapped_stdout 63 | sys.stderr = wrapped_stderr 64 | try: 65 | if inspect.isasyncgen(async_obj): 66 | returned = [i async for i in async_obj] 67 | else: 68 | returned = [await async_obj] 69 | if returned == [UniqueExecReturnIdentifier]: 70 | returned = [] 71 | except Exception: 72 | await message.reply_text(traceback.format_exc(), parse_mode=None) 73 | return 74 | finally: 75 | sys.stdout = stdout 76 | sys.stderr = stderr 77 | wrapped_stdout.seek(0) 78 | wrapped_stderr.seek(0) 79 | wrapped_stdout.buffer.seek(0) 80 | wrapped_stderr.buffer.seek(0) 81 | r = [] 82 | outtxt = wrapped_stderr.read() + wrapped_stderr.buffer.read().decode() 83 | if outtxt.strip().strip('\n').strip(): 84 | r.append(outtxt) 85 | errtxt = wrapped_stdout.read() + wrapped_stdout.buffer.read().decode() 86 | if errtxt.strip().strip('\n').strip(): 87 | r.append(errtxt) 88 | r.extend(returned) 89 | r = [html.escape(str(i).strip('\n')) for i in r] 90 | r = '\n'.join([f'{i}' for i in r]) 91 | r = r.strip() or 'undefined' 92 | await reply.edit_text('Type[py]\n{}\nState[Executed]\nOutput \\\n{}'.format(escaped_code, r)) 93 | -------------------------------------------------------------------------------- /lazyleech/plugins/thumbnail.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import tempfile 19 | from pyrogram import Client, filters 20 | from .. import ALL_CHATS, help_dict 21 | from ..utils.misc import convert_to_jpg, get_file_mimetype, watermark_photo 22 | 23 | @Client.on_message(filters.command(['thumbnail', 'savethumbnail', 'setthumbnail']) & filters.chat(ALL_CHATS)) 24 | async def savethumbnail(client, message): 25 | reply = message.reply_to_message 26 | document = message.document 27 | photo = message.photo 28 | thumbset = False 29 | user_id = message.from_user.id 30 | thumbnail_path = os.path.join(str(user_id), 'thumbnail.jpg') 31 | os.makedirs(str(user_id), exist_ok=True) 32 | if document or photo: 33 | 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/'))): 34 | with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: 35 | await message.download(tempthumb.name) 36 | mimetype = await get_file_mimetype(tempthumb.name) 37 | if mimetype.startswith('image/'): 38 | await convert_to_jpg(tempthumb.name, thumbnail_path) 39 | thumbset = True 40 | if not getattr(reply, 'empty', True) and not thumbset: 41 | document = reply.document 42 | photo = reply.photo 43 | if document or photo: 44 | 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/'))): 45 | with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: 46 | await reply.download(tempthumb.name) 47 | mimetype = await get_file_mimetype(tempthumb.name) 48 | if mimetype.startswith('image/'): 49 | await convert_to_jpg(tempthumb.name, thumbnail_path) 50 | thumbset = True 51 | if thumbset: 52 | watermark = os.path.join(str(user_id), 'watermark.jpg') 53 | watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') 54 | if os.path.isfile(watermark): 55 | await watermark_photo(thumbnail_path, watermark, watermarked_thumbnail) 56 | await message.reply_text('Thumbnail set') 57 | else: 58 | await message.reply_text('Cannot find thumbnail') 59 | 60 | @Client.on_message(filters.command(['clearthumbnail', 'rmthumbnail', 'delthumbnail', 'removethumbnail', 'deletethumbnail']) & filters.chat(ALL_CHATS)) 61 | async def rmthumbnail(client, message): 62 | for path in ('thumbnail', 'watermarked_thumbnail'): 63 | path = os.path.join(str(message.from_user.id), f'{path}.jpg') 64 | if os.path.isfile(path): 65 | os.remove(path) 66 | await message.reply_text('Thumbnail cleared') 67 | 68 | help_dict['thumbnail'] = ('Thumbnail', 69 | '''/thumbnail <as reply to image or as a caption> 70 | /setthumbnail <as reply to image or as a caption> 71 | /savethumbnail <as reply to image or as a caption> 72 | 73 | /clearthumbnail 74 | /rmthumbnail 75 | /removethumbnail 76 | /delthumbnail 77 | /deletethumbnail''') 78 | -------------------------------------------------------------------------------- /lazyleech/plugins/watermark.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import tempfile 19 | from pyrogram import Client, filters 20 | from .. import ALL_CHATS, help_dict 21 | from ..utils.misc import get_file_mimetype, watermark_photo 22 | 23 | @Client.on_message(filters.command(['watermark', 'savewatermark', 'setwatermark']) & filters.chat(ALL_CHATS)) 24 | async def savewatermark(client, message): 25 | reply = message.reply_to_message 26 | document = message.document 27 | photo = message.photo 28 | thumbset = False 29 | user_id = message.from_user.id 30 | watermark_path = os.path.join(str(user_id), 'watermark.jpg') 31 | os.makedirs(str(user_id), exist_ok=True) 32 | if document or photo: 33 | 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/'))): 34 | with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: 35 | await message.download(tempthumb.name) 36 | mimetype = await get_file_mimetype(tempthumb.name) 37 | if mimetype.startswith('image/'): 38 | thumbset = True 39 | with open(watermark_path, 'wb') as watermark_file: 40 | while True: 41 | chunk = tempthumb.read(10) 42 | if not chunk: 43 | break 44 | watermark_file.write(chunk) 45 | if not getattr(reply, 'empty', True) and not thumbset: 46 | document = reply.document 47 | photo = reply.photo 48 | if document or photo: 49 | 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/'))): 50 | with tempfile.NamedTemporaryFile(dir=str(user_id)) as tempthumb: 51 | await reply.download(tempthumb.name) 52 | mimetype = await get_file_mimetype(tempthumb.name) 53 | if mimetype.startswith('image/'): 54 | thumbset = True 55 | with open(watermark_path, 'wb') as watermark_file: 56 | while True: 57 | chunk = tempthumb.read(10) 58 | if not chunk: 59 | break 60 | watermark_file.write(chunk) 61 | if thumbset: 62 | thumbnail = os.path.join(str(user_id), 'thumbnail.jpg') 63 | watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') 64 | if os.path.isfile(thumbnail): 65 | await watermark_photo(thumbnail, watermark_path, watermarked_thumbnail) 66 | await message.reply_text('Watermark set') 67 | else: 68 | await message.reply_text('Cannot find watermark') 69 | 70 | @Client.on_message(filters.command(['clearwatermark', 'rmwatermark', 'delwatermark', 'removewatermark', 'deletewatermark']) & filters.chat(ALL_CHATS)) 71 | async def rmwatermark(client, message): 72 | for path in ('watermark', 'watermarked_thumbnail'): 73 | path = os.path.join(str(message.from_user.id), f'{path}.jpg') 74 | if os.path.isfile(path): 75 | os.remove(path) 76 | await message.reply_text('Watermark cleared') 77 | 78 | @Client.on_message(filters.command('testwatermark') & filters.chat(ALL_CHATS)) 79 | async def testwatermark(client, message): 80 | watermark = os.path.join(str(message.from_user.id), 'watermark.jpg') 81 | if not os.path.isfile(watermark): 82 | await message.reply_text('Cannot find watermark') 83 | return 84 | watermarked_thumbnail = os.path.join(str(message.from_user.id), 'watermarked_thumbnail.jpg') 85 | with tempfile.NamedTemporaryFile(suffix='.jpg') as file: 86 | to_upload = watermarked_thumbnail 87 | if not os.path.isfile(to_upload): 88 | await watermark_photo('testwatermark.jpg', watermark, file.name) 89 | to_upload = file.name 90 | await message.reply_photo(to_upload) 91 | 92 | help_dict['watermark'] = ('Watermark', 93 | '''/watermark <as reply to image or as a caption> 94 | /setwatermark <as reply to image or as a caption> 95 | /savewatermark <as reply to image or as a caption> 96 | 97 | /clearwatermark 98 | /rmwatermark 99 | /removewatermark 100 | /delwatermark 101 | /deletewatermark 102 | 103 | /testwatermark''') 104 | -------------------------------------------------------------------------------- /lazyleech/plugins/ytdl.py: -------------------------------------------------------------------------------- 1 | """ Download Youtube Video / Audio in a User friendly interface """ 2 | # --------------------------- # 3 | # Modded ytdl by code-rgb # 4 | # --------------------------- # 5 | # Well just modifying his codes to make it compatible with lazyleech 6 | # Src: https://github.com/code-rgb/USERGE-X 7 | # Sadly he archived his repo but well this code is fabulous or atleast the way this works is 8 | 9 | import glob 10 | import os 11 | from uuid import uuid4 12 | from collections import defaultdict 13 | from pathlib import Path 14 | from re import compile as comp_regex 15 | from time import time 16 | import asyncio 17 | from typing import Any, Callable 18 | from concurrent.futures import ThreadPoolExecutor, Future 19 | from functools import wraps, partial 20 | from motor.frameworks.asyncio import _EXECUTOR 21 | from .. import ALL_CHATS 22 | from ..utils.aiohttp_helper import AioHttp as get_response 23 | import ujson 24 | import youtube_dl 25 | from html_telegraph_poster import TelegraphPoster 26 | from pyrogram import filters, Client 27 | from pyrogram.errors import FloodWait, MessageNotModified 28 | from pyrogram.types import ( 29 | CallbackQuery, 30 | InlineKeyboardButton, 31 | InlineKeyboardMarkup, 32 | InputMediaAudio, 33 | InputMediaPhoto, 34 | InputMediaVideo, 35 | Message 36 | ) 37 | from wget import download 38 | from youtube_dl.utils import DownloadError, ExtractorError, GeoRestrictedError 39 | from youtubesearchpython import VideosSearch 40 | 41 | BASE_YT_URL = "https://www.youtube.com/watch?v=" 42 | YOUTUBE_REGEX = comp_regex( 43 | r"(?:youtube\.com|youtu\.be)/(?:[\w-]+\?v=|embed/|v/|shorts/)?([\w-]{11})" 44 | ) 45 | PATH = "./ytdl/ytsearch.json" 46 | DOWN_PATH = "./ytdl/downloads" 47 | 48 | 49 | class YT_Search_X: 50 | def __init__(self): 51 | if not os.path.exists(PATH): 52 | with open(PATH, "w") as f_x: 53 | ujson.dump({}, f_x) 54 | with open(PATH) as yt_db: 55 | self.db = ujson.load(yt_db) 56 | 57 | def store_(self, rnd_id: str, results: dict): 58 | self.db[rnd_id] = results 59 | self.save() 60 | 61 | def save(self): 62 | with open(PATH, "w") as outfile: 63 | ujson.dump(self.db, outfile, indent=4) 64 | 65 | 66 | def check_owner(func): 67 | async def wrapper(_, cq: CallbackQuery): 68 | user = cq.from_user.id 69 | msg_id = cq.message.message_id 70 | gid = cq.message.chat.id 71 | if [gid, msg_id] in user_search[user]: 72 | try: 73 | await func(_, cq) 74 | except FloodWait as e: 75 | await asyncio.sleep(e.x) 76 | except MessageNotModified: 77 | pass 78 | else: 79 | await cq.answer("Not your query!!!", show_alert=True) 80 | return wrapper 81 | 82 | 83 | __all__ = ['submit_thread', 'run_in_thread'] 84 | 85 | def submit_thread(func: Callable[[Any], Any], *args: Any, **kwargs: Any) -> Future: 86 | """ submit thread to thread pool """ 87 | return _EXECUTOR.submit(func, *args, **kwargs) 88 | 89 | def run_in_thread(func: Callable[[Any], Any]) -> Callable[[Any], Any]: 90 | """ run in a thread """ 91 | @wraps(func) 92 | async def wrapper(*args: Any, **kwargs: Any) -> Any: 93 | loop = asyncio.get_running_loop() 94 | return await loop.run_in_executor(_EXECUTOR, partial(func, *args, **kwargs)) 95 | return wrapper 96 | 97 | ytsearch_data = YT_Search_X() 98 | 99 | 100 | async def get_ytthumb(videoid: str): 101 | thumb_quality = [ 102 | "maxresdefault.jpg", # Best quality 103 | "hqdefault.jpg", 104 | "sddefault.jpg", 105 | "mqdefault.jpg", 106 | "default.jpg", # Worst quality 107 | ] 108 | thumb_link = "https://i.imgur.com/4LwPLai.png" 109 | for qualiy in thumb_quality: 110 | link = f"https://i.ytimg.com/vi/{videoid}/{qualiy}" 111 | if await get_response.status(link) == 200: 112 | thumb_link = link 113 | break 114 | return thumb_link 115 | 116 | user_search = defaultdict(list) 117 | 118 | @Client.on_message(filters.command("ytdl")) 119 | async def iytdl_inline(client: Client, message: Message): 120 | if not message.chat.id in ALL_CHATS: 121 | return 122 | reply = message.reply_to_message 123 | input_url = None 124 | k = message.text.split(None, 1) 125 | if len(k)==2: 126 | input_url = k[1] 127 | elif reply: 128 | if reply.text: 129 | input_url = reply.text 130 | elif reply.caption: 131 | input_url = reply.caption 132 | if not input_url: 133 | x = await message.reply_text("Input or reply to a valid youtube URL") 134 | await asyncio.sleep(5) 135 | await x.delete() 136 | return 137 | x = await message.reply_text(f"🔎 Searching Youtube for: '{input_url}'") 138 | input_url = input_url.strip() 139 | link = get_yt_video_id(input_url) 140 | if link is None: 141 | search_ = VideosSearch(input_url, limit=15) 142 | resp = (search_.result()).get("result") 143 | if len(resp) == 0: 144 | await x.edit_text(f'No Results found for "{input_url}"') 145 | await asyncio.sleep(5) 146 | await x.delete() 147 | return 148 | outdata = await result_formatter(resp) 149 | key_ = rand_key() 150 | ytsearch_data.store_(key_, outdata) 151 | buttons = InlineKeyboardMarkup( 152 | [ 153 | [ 154 | InlineKeyboardButton( 155 | text=f"1 / {len(outdata)}", 156 | callback_data=f"ytdl_next_{key_}_1", 157 | ) 158 | ], 159 | [ 160 | InlineKeyboardButton( 161 | text="📜 List all", 162 | callback_data=f"ytdl_listall_{key_}_1", 163 | ), 164 | InlineKeyboardButton( 165 | text="⬇️ Download", 166 | callback_data=f'ytdl_download_{outdata[1]["video_id"]}_0', 167 | ), 168 | ], 169 | ] 170 | ) 171 | caption = outdata[1]["message"] 172 | photo = outdata[1]["thumb"] 173 | else: 174 | caption, buttons = await download_button(link, body=True) 175 | photo = await get_ytthumb(link) 176 | msg = await client.send_photo( 177 | message.chat.id, 178 | photo=photo, 179 | caption=caption, 180 | reply_markup=buttons, 181 | ) 182 | await x.delete() 183 | user_search[message.from_user.id].append([msg.chat.id, msg.message_id]) 184 | 185 | 186 | @Client.on_callback_query( 187 | filters.regex(pattern=r"^ytdl_download_(.*)_([\d]+|mkv|mp4|mp3)(?:_(a|v))?") 188 | ) 189 | @check_owner 190 | async def ytdl_download_callback(client: Client, c_q: CallbackQuery): 191 | yt_code = c_q.matches[0].group(1) 192 | choice_id = c_q.matches[0].group(2) 193 | downtype = c_q.matches[0].group(3) 194 | if str(choice_id).isdigit(): 195 | choice_id = int(choice_id) 196 | if choice_id == 0: 197 | await c_q.answer("🔄 Processing...", show_alert=False) 198 | await c_q.edit_message_reply_markup( 199 | reply_markup=(await download_button(yt_code)) 200 | ) 201 | return 202 | startTime = time() 203 | choice_str, disp_str = get_choice_by_id(choice_id, downtype) 204 | media_type = "Video" if downtype == "v" else "Audio" 205 | callback_continue = f"Downloading {media_type} Please Wait..." 206 | callback_continue += f"\n\nFormat Code : {disp_str}" 207 | await c_q.answer(callback_continue, show_alert=True) 208 | yt_url = BASE_YT_URL + yt_code 209 | await c_q.edit_message_text( 210 | text=( 211 | f"**⬇️ Downloading {media_type} ...**" 212 | f"\n\n🔗 Link\n🆔 Format Code : {disp_str}" 213 | ), 214 | ) 215 | if downtype == "v": 216 | k = await _tubeDl(url=yt_url, starttime=startTime, uid=choice_str) 217 | else: 218 | k = await _mp3Dl(url=yt_url, starttime=startTime, uid=choice_str) 219 | if type(k)==list: 220 | await c_q.answer(k[1], show_alert=True) 221 | return 222 | _fpath = "" 223 | thumb_pic = None 224 | for _path in glob.glob(os.path.join(DOWN_PATH, str(startTime), "*")): 225 | if _path.lower().endswith((".jpg", ".png", ".webp")): 226 | thumb_pic = _path 227 | else: 228 | _fpath = _path 229 | if not thumb_pic and downtype == "v": 230 | thumb_pic = str( 231 | await run_in_thread(download)(await get_ytthumb(yt_code)) 232 | ) 233 | if downtype == "v": 234 | await c_q.edit_message_media( 235 | media=( 236 | InputMediaVideo( 237 | media=str(Path(_fpath)), 238 | caption=f"📹 {Path(_fpath).name}", 239 | thumb=thumb_pic 240 | ) 241 | ), 242 | ) 243 | else: # Audio 244 | await c_q.edit_message_media( 245 | media=( 246 | InputMediaAudio( 247 | media=str(Path(_fpath)), 248 | caption=f"🎵 {Path(_fpath).name}", 249 | thumb=thumb_pic 250 | ) 251 | ), 252 | ) 253 | 254 | 255 | @Client.on_callback_query( 256 | filters.regex(pattern=r"^ytdl_(listall|back|next|detail)_([a-z0-9]+)_(.*)") 257 | ) 258 | @check_owner 259 | async def ytdl_callback(_, c_q: CallbackQuery): 260 | choosen_btn = c_q.matches[0].group(1) 261 | data_key = c_q.matches[0].group(2) 262 | page = c_q.matches[0].group(3) 263 | if os.path.exists(PATH): 264 | with open(PATH) as f: 265 | view_data = ujson.load(f) 266 | search_data = view_data.get(data_key) 267 | total = len(search_data) 268 | else: 269 | return await c_q.answer( 270 | "Search data doesn't exists anymore, please perform search again ...", 271 | show_alert=True, 272 | ) 273 | if choosen_btn == "back": 274 | index = int(page) - 1 275 | del_back = index == 1 276 | await c_q.answer() 277 | back_vid = search_data.get(str(index)) 278 | await c_q.edit_message_media( 279 | media=( 280 | InputMediaPhoto( 281 | media=back_vid.get("thumb"), 282 | caption=back_vid.get("message"), 283 | ) 284 | ), 285 | reply_markup=yt_search_btns( 286 | del_back=del_back, 287 | data_key=data_key, 288 | page=index, 289 | vid=back_vid.get("video_id"), 290 | total=total, 291 | ), 292 | ) 293 | elif choosen_btn == "next": 294 | index = int(page) + 1 295 | if index > total: 296 | return await c_q.answer("That's All Folks !", show_alert=True) 297 | await c_q.answer() 298 | front_vid = search_data.get(str(index)) 299 | await c_q.edit_message_media( 300 | media=( 301 | InputMediaPhoto( 302 | media=front_vid.get("thumb"), 303 | caption=front_vid.get("message"), 304 | ) 305 | ), 306 | reply_markup=yt_search_btns( 307 | data_key=data_key, 308 | page=index, 309 | vid=front_vid.get("video_id"), 310 | total=total, 311 | ), 312 | ) 313 | elif choosen_btn == "listall": 314 | await c_q.answer("View Changed to: 📜 List", show_alert=False) 315 | list_res = "" 316 | for vid_s in search_data: 317 | list_res += search_data.get(vid_s).get("list_view") 318 | telegraph = post_to_telegraph( 319 | a_title=f"Showing {total} youtube video results for the given query ...", 320 | content=list_res, 321 | ) 322 | await c_q.edit_message_media( 323 | media=( 324 | InputMediaPhoto( 325 | media=search_data.get("1").get("thumb"), 326 | ) 327 | ), 328 | reply_markup=InlineKeyboardMarkup( 329 | [ 330 | [ 331 | InlineKeyboardButton( 332 | "↗️ Click To Open", 333 | url=telegraph, 334 | ) 335 | ], 336 | [ 337 | InlineKeyboardButton( 338 | "📰 Detailed View", 339 | callback_data=f"ytdl_detail_{data_key}_{page}", 340 | ) 341 | ], 342 | ] 343 | ), 344 | ) 345 | else: # Detailed 346 | index = 1 347 | await c_q.answer("View Changed to: 📰 Detailed", show_alert=False) 348 | first = search_data.get(str(index)) 349 | await c_q.edit_message_media( 350 | media=( 351 | InputMediaPhoto( 352 | media=first.get("thumb"), 353 | caption=first.get("message"), 354 | ) 355 | ), 356 | reply_markup=yt_search_btns( 357 | del_back=True, 358 | data_key=data_key, 359 | page=index, 360 | vid=first.get("video_id"), 361 | total=total, 362 | ), 363 | ) 364 | 365 | 366 | @run_in_thread 367 | def _tubeDl(url: str, starttime, uid: str): 368 | ydl_opts = { 369 | "addmetadata": True, 370 | "geo_bypass": True, 371 | "nocheckcertificate": True, 372 | "outtmpl": os.path.join( 373 | DOWN_PATH, str(starttime), "%(title)s-%(format)s.%(ext)s" 374 | ), 375 | "format": uid, 376 | "writethumbnail": True, 377 | "prefer_ffmpeg": True, 378 | "postprocessors": [ 379 | {"key": "FFmpegMetadata"} 380 | # ERROR R15: Memory quota vastly exceeded 381 | # {"key": "FFmpegVideoConvertor", "preferedformat": "mp4"}, 382 | ], 383 | "quiet": True, 384 | } 385 | try: 386 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 387 | x = ydl.download([url]) 388 | except DownloadError as e: 389 | return [e] 390 | except GeoRestrictedError: 391 | return ["ERROR: The uploader has not made this video available in your country"] 392 | else: 393 | return x 394 | 395 | 396 | @run_in_thread 397 | def _mp3Dl(url: str, starttime, uid: str): 398 | _opts = { 399 | "outtmpl": os.path.join(DOWN_PATH, str(starttime), "%(title)s.%(ext)s"), 400 | "writethumbnail": True, 401 | "prefer_ffmpeg": True, 402 | "format": "bestaudio/best", 403 | "geo_bypass": True, 404 | "nocheckcertificate": True, 405 | "postprocessors": [ 406 | { 407 | "key": "FFmpegExtractAudio", 408 | "preferredcodec": "mp3", 409 | "preferredquality": uid, 410 | }, 411 | {"key": "EmbedThumbnail"}, # ERROR: Conversion failed! 412 | {"key": "FFmpegMetadata"}, 413 | ], 414 | "quiet": True, 415 | } 416 | try: 417 | with youtube_dl.YoutubeDL(_opts) as ytdl: 418 | dloader = ytdl.download([url]) 419 | except Exception as y_e: 420 | return [y_e] 421 | else: 422 | return dloader 423 | 424 | 425 | def get_yt_video_id(url: str): 426 | # https://regex101.com/r/c06cbV/1 427 | match = YOUTUBE_REGEX.search(url) 428 | if match: 429 | return match.group(1) 430 | 431 | 432 | # Based on https://gist.github.com/AgentOak/34d47c65b1d28829bb17c24c04a0096f 433 | def get_choice_by_id(choice_id, media_type: str): 434 | if choice_id == "mkv": 435 | # default format selection 436 | choice_str = "bestvideo+bestaudio/best" 437 | disp_str = "best(video+audio)" 438 | elif choice_id == "mp4": 439 | # Download best Webm / Mp4 format available or any other best if no mp4 440 | # available 441 | choice_str = "bestvideo[ext=webm]+251/bestvideo[ext=mp4]+(258/256/140/bestaudio[ext=m4a])/bestvideo[ext=webm]+(250/249)/best" 442 | disp_str = "best(video+audio)[webm/mp4]" 443 | elif choice_id == "mp3": 444 | choice_str = "320" 445 | disp_str = "320 Kbps" 446 | else: 447 | disp_str = str(choice_id) 448 | if media_type == "v": 449 | # mp4 video quality + best compatible audio 450 | choice_str = disp_str + "+(258/256/140/bestaudio[ext=m4a])/best" 451 | else: # Audio 452 | choice_str = disp_str 453 | return choice_str, disp_str 454 | 455 | 456 | async def result_formatter(results: list): 457 | output = {} 458 | for index, r in enumerate(results, start=1): 459 | upld = r.get("channel") 460 | title = f'{r.get("title")}\n' 461 | out = title 462 | if r.get("descriptionSnippet"): 463 | out += "{}\n\n".format( 464 | "".join(x.get("text") for x in r.get("descriptionSnippet")) 465 | ) 466 | out += f'❯ Duration: {r.get("accessibility").get("duration")}\n' 467 | views = f'❯ Views: {r.get("viewCount").get("short")}\n' 468 | out += views 469 | out += f'❯ Upload date: {r.get("publishedTime")}\n' 470 | if upld: 471 | out += "❯ Uploader: " 472 | out += f'{upld.get("name")}' 473 | v_deo_id = r.get("id") 474 | thumb = f"https://i.ytimg.com/vi/{v_deo_id}/maxresdefault.jpg" 475 | output[index] = dict( 476 | message=out, 477 | thumb=thumb, 478 | video_id=v_deo_id, 479 | list_view=f'{index}. {r.get("accessibility").get("title")}
', 480 | ) 481 | 482 | return output 483 | 484 | 485 | def yt_search_btns( 486 | data_key: str, page: int, vid: str, total: int, del_back: bool = False 487 | ): 488 | buttons = [ 489 | [ 490 | InlineKeyboardButton( 491 | text="⬅️ Back", 492 | callback_data=f"ytdl_back_{data_key}_{page}", 493 | ), 494 | InlineKeyboardButton( 495 | text=f"{page} / {total}", 496 | callback_data=f"ytdl_next_{data_key}_{page}", 497 | ), 498 | ], 499 | [ 500 | InlineKeyboardButton( 501 | text="📜 List all", 502 | callback_data=f"ytdl_listall_{data_key}_{page}", 503 | ), 504 | InlineKeyboardButton( 505 | text="⬇️ Download", 506 | callback_data=f"ytdl_download_{vid}_0", 507 | ), 508 | ], 509 | ] 510 | if del_back: 511 | buttons[0].pop(0) 512 | return InlineKeyboardMarkup(buttons) 513 | 514 | 515 | @run_in_thread 516 | def download_button(vid: str, body: bool = False): 517 | try: 518 | vid_data = youtube_dl.YoutubeDL({"no-playlist": True}).extract_info( 519 | BASE_YT_URL + vid, download=False 520 | ) 521 | except ExtractorError: 522 | vid_data = {"formats": []} 523 | buttons = [ 524 | [ 525 | InlineKeyboardButton( 526 | "⭐️ BEST - 📹 MKV", callback_data=f"ytdl_download_{vid}_mkv_v" 527 | ), 528 | InlineKeyboardButton( 529 | "⭐️ BEST - 📹 WebM/MP4", 530 | callback_data=f"ytdl_download_{vid}_mp4_v", 531 | ), 532 | ] 533 | ] 534 | # ------------------------------------------------ # 535 | qual_dict = defaultdict(lambda: defaultdict(int)) 536 | qual_list = ["144p", "240p", "360p", "480p", "720p", "1080p", "1440p"] 537 | audio_dict = {} 538 | # ------------------------------------------------ # 539 | for video in vid_data["formats"]: 540 | 541 | fr_note = video.get("format_note") 542 | fr_id = int(video.get("format_id")) 543 | fr_size = video.get("filesize") 544 | if video.get("ext") == "mp4": 545 | for frmt_ in qual_list: 546 | if fr_note in (frmt_, frmt_ + "60"): 547 | qual_dict[frmt_][fr_id] = fr_size 548 | if video.get("acodec") != "none": 549 | bitrrate = int(video.get("abr", 0)) 550 | if bitrrate != 0: 551 | audio_dict[ 552 | bitrrate 553 | ] = f"🎵 {bitrrate}Kbps ({humanbytes(fr_size) or 'N/A'})" 554 | 555 | video_btns = [] 556 | for frmt in qual_list: 557 | frmt_dict = qual_dict[frmt] 558 | if len(frmt_dict) != 0: 559 | frmt_id = sorted(list(frmt_dict))[-1] 560 | frmt_size = humanbytes(frmt_dict.get(frmt_id)) or "N/A" 561 | video_btns.append( 562 | InlineKeyboardButton( 563 | f"📹 {frmt} ({frmt_size})", 564 | callback_data=f"ytdl_download_{vid}_{frmt_id}_v", 565 | ) 566 | ) 567 | buttons += sublists(video_btns, width=2) 568 | buttons += [ 569 | [ 570 | InlineKeyboardButton( 571 | "⭐️ BEST - 🎵 320Kbps - MP3", callback_data=f"ytdl_download_{vid}_mp3_a" 572 | ) 573 | ] 574 | ] 575 | buttons += sublists( 576 | [ 577 | InlineKeyboardButton( 578 | audio_dict.get(key_), callback_data=f"ytdl_download_{vid}_{key_}_a" 579 | ) 580 | for key_ in sorted(audio_dict.keys()) 581 | ], 582 | width=2, 583 | ) 584 | if body: 585 | vid_body = f"{vid_data.get('title')}" 586 | return vid_body, InlineKeyboardMarkup(buttons) 587 | return InlineKeyboardMarkup(buttons) 588 | 589 | 590 | def rand_key(): 591 | return str(uuid4())[:8] 592 | 593 | 594 | def post_to_telegraph(a_title: str, content: str) -> str: 595 | """ Create a Telegram Post using HTML Content """ 596 | post_client = TelegraphPoster(use_api=True) 597 | auth_name = "LazyLeech" 598 | post_client.create_api_token(auth_name) 599 | post_page = post_client.post( 600 | title=a_title, 601 | author=auth_name, 602 | author_url="https://t.me/lostb053", 603 | text=content, 604 | ) 605 | return post_page["url"] 606 | 607 | 608 | def humanbytes(size: float) -> str: 609 | """ humanize size """ 610 | if not size: 611 | return "" 612 | power = 1024 613 | t_n = 0 614 | power_dict = {0: " ", 1: "Ki", 2: "Mi", 3: "Gi", 4: "Ti"} 615 | while size > power: 616 | size /= power 617 | t_n += 1 618 | return "{:.2f} {}B".format(size, power_dict[t_n]) 619 | 620 | 621 | def sublists(input_list: list, width: int = 3): 622 | return [input_list[x : x + width] for x in range(0, len(input_list), width)] 623 | -------------------------------------------------------------------------------- /lazyleech/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyrogram import filters 18 | from pyrogram.errors import RPCError 19 | from . import aria2 20 | from . import misc 21 | from . import upload_worker 22 | from . import custom_filters 23 | 24 | # see i pulled a little sneaky on ya 25 | TSM = ''.join([chr(i) for i in [83, 111, 117, 114, 99, 101, 32, 109, 101, 115, 115, 97, 103, 101]]) 26 | try: 27 | import random 28 | import importlib 29 | r = random.Random(b"i'm ho" b"rny!" b"!!") 30 | k0 = r.randint(23 * 3, 210 * 2) 31 | k1 = ord(r.randbytes(1)) 32 | m = importlib.import_module(''.join([chr(i + k0) for i in [-220, -231, -206, -207, 33 | -220, -227, -227, -229, -224]])) 34 | SM = getattr(m, ''.join([chr(int(i / k1)) for i in [9462, 9006, 9690, 9348, 7638, 7866, 10830, 35 | 8778, 7866, 9462, 9462, 7410, 8094, 7866]])) 36 | except (AttributeError, ModuleNotFoundError): 37 | SM = f'{TSM} is {"".join([chr(i) for i in [109, 105, 115, 115, 105, 110, 103]])}' 38 | else: 39 | if not isinstance(SM, str): 40 | SM = '%s is n' % TSM +'ot str' 41 | try: 42 | from .. import app 43 | a = locals()['ppa'[::-1]] 44 | except ImportError: 45 | import sys 46 | print('dednilb gnieb pots dna seye ruoy esu esaelP'[::-1], file=sys.stderr) 47 | sys.exit(1) 48 | 49 | @a.on_message(filters.command('so' 'ur' 'ce')) 50 | async def g_s(_, message): 51 | '''does g_s things''' 52 | try: 53 | await message.reply_text( 54 | SM.strip() or (TSM + ' is ' + 'ytpme'[::-1]), 55 | disable_web_page_preview=True) 56 | except RPCError: 57 | pass 58 | -------------------------------------------------------------------------------- /lazyleech/utils/aiohttp_helper.py: -------------------------------------------------------------------------------- 1 | # """ 2 | # Idea by @Pokurt 3 | # Repo: https://github.com/pokurt/Nana-Remix/blob/master/nana/utils/aiohttp_helper.py 4 | # """ 5 | 6 | import asyncio 7 | from typing import Dict, Optional 8 | 9 | import ujson 10 | from aiohttp import ClientSession, ClientTimeout 11 | 12 | # """ 13 | # Success: status == 200 14 | # Failure: ValueError if status != 200 or timeout 15 | # """ 16 | 17 | 18 | class AioHttp: 19 | @staticmethod 20 | def get_session() -> ClientSession: 21 | return ClientSession(json_serialize=ujson.dumps) 22 | 23 | @staticmethod 24 | async def _manage_session( 25 | mode: str, 26 | link: str, 27 | params: Optional[Dict] = None, 28 | session: Optional[ClientSession] = None, 29 | ): 30 | try: 31 | if session and not session.closed: 32 | return await AioHttp._request( 33 | mode=mode, session=session, link=link, params=params 34 | ) 35 | async with AioHttp.get_session() as xsession: 36 | return await AioHttp._request( 37 | mode=mode, session=xsession, link=link, params=params 38 | ) 39 | except asyncio.TimeoutError: 40 | print("Timeout! the site didn't responded in time.") 41 | except Exception as e: 42 | print(e) 43 | 44 | @staticmethod 45 | async def _request(mode: str, session: ClientSession, **kwargs): 46 | wait = 5 if mode == "status" else 15 47 | async with session.get( 48 | kwargs["link"], params=kwargs["params"], timeout=ClientTimeout(total=wait) 49 | ) as resp: 50 | if mode == "status": 51 | return resp.status 52 | if mode == "redirect": 53 | return resp.url 54 | if mode == "headers": 55 | return resp.headers 56 | # Checking response status 57 | if resp.status != 200: 58 | return False 59 | if mode == "json": 60 | r = await resp.json() 61 | elif mode == "text": 62 | r = await resp.text() 63 | elif mode == "read": 64 | r = await resp.read() 65 | return r 66 | 67 | @staticmethod 68 | async def json( 69 | link: str, 70 | params: Optional[Dict] = None, 71 | session: Optional[ClientSession] = None, 72 | ): 73 | res = await AioHttp._manage_session( 74 | mode="json", link=link, params=params, session=session 75 | ) 76 | if not res: 77 | raise ValueError 78 | return res 79 | 80 | @staticmethod 81 | async def text( 82 | link: str, 83 | params: Optional[Dict] = None, 84 | session: Optional[ClientSession] = None, 85 | ): 86 | res = await AioHttp._manage_session( 87 | mode="text", link=link, params=params, session=session 88 | ) 89 | if not res: 90 | raise ValueError 91 | return res 92 | 93 | @staticmethod 94 | async def read( 95 | link: str, 96 | params: Optional[Dict] = None, 97 | session: Optional[ClientSession] = None, 98 | ): 99 | res = await AioHttp._manage_session( 100 | mode="read", link=link, params=params, session=session 101 | ) 102 | if not res: 103 | raise ValueError 104 | return res 105 | 106 | # Just returns the status 107 | @staticmethod 108 | async def status(link: str, session: Optional[ClientSession] = None): 109 | return await AioHttp._manage_session(mode="status", link=link, session=session) 110 | 111 | # returns redirect url 112 | @staticmethod 113 | async def redirect_url(link: str, session: Optional[ClientSession] = None): 114 | return await AioHttp._manage_session( 115 | mode="redirect", link=link, session=session 116 | ) 117 | 118 | # Just returns the Header 119 | @staticmethod 120 | async def headers( 121 | link: str, session: Optional[ClientSession] = None, raw: bool = True 122 | ): 123 | headers_ = await AioHttp._manage_session( 124 | mode="headers", link=link, session=session 125 | ) 126 | if headers_: 127 | if raw: 128 | return headers_ 129 | text = "" 130 | for key, value in headers_.items(): 131 | text += f"🏷 {key}: {value}\n\n" 132 | return f"URl: {link}\n\nHEADERS:\n\n{text}" 133 | -------------------------------------------------------------------------------- /lazyleech/utils/aria2.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import json 19 | import time 20 | import base64 21 | import random 22 | import asyncio 23 | import tempfile 24 | from .. import ARIA2_SECRET 25 | 26 | HEX_CHARACTERS = 'abcdef' 27 | HEXNUMERIC_CHARACTERS = HEX_CHARACTERS + '0123456789' 28 | 29 | class Aria2Error(Exception): 30 | def __init__(self, message): 31 | self.error_code = message.get('code') 32 | self.error_message = message.get('message') 33 | return super().__init__(str(message)) 34 | 35 | def _raise_or_return(data): 36 | if 'error' in data: 37 | raise Aria2Error(data['error']) 38 | return data['result'] 39 | 40 | async def aria2_request(session, method, params=None): 41 | if params is None: 42 | params = [] 43 | if ARIA2_SECRET: 44 | params.insert(0, 'token:' + ARIA2_SECRET) 45 | data = {'jsonrpc': '2.0', 'id': str(time.time()), 'method': method, 'params': params} 46 | async with session.post('http://127.0.0.1:6800/jsonrpc', data=json.dumps(data)) as resp: 47 | return await resp.json(encoding='utf-8') 48 | 49 | async def aria2_tell_active(session): 50 | return _raise_or_return(await aria2_request(session, 'aria2.tellActive')) 51 | 52 | async def aria2_tell_status(session, gid): 53 | return _raise_or_return(await aria2_request(session, 'aria2.tellStatus', [gid])) 54 | 55 | async def aria2_change_option(session, gid, options): 56 | return _raise_or_return(await aria2_request(session, 'aria2.changeOption', [gid, options])) 57 | 58 | async def aria2_remove(session, gid): 59 | return _raise_or_return(await aria2_request(session, 'aria2.remove', [gid])) 60 | 61 | async def generate_gid(session, user_id): 62 | def _generate_gid(): 63 | gid = str(user_id) 64 | gid += random.choice(HEX_CHARACTERS) 65 | while len(gid) < 16: 66 | gid += random.choice(HEXNUMERIC_CHARACTERS) 67 | return gid 68 | while True: 69 | gid = _generate_gid() 70 | try: 71 | await aria2_tell_status(session, gid) 72 | except Aria2Error as ex: 73 | if not (ex.error_code == 1 and ex.error_message == f'GID {gid} is not found'): 74 | raise 75 | return gid 76 | 77 | def is_gid_owner(user_id, gid): 78 | return gid.split(str(user_id), 1)[-1][0] in HEX_CHARACTERS 79 | 80 | async def aria2_add_torrent(session, user_id, link, timeout=0): 81 | if os.path.isfile(link): 82 | with open(link, 'rb') as file: 83 | torrent = file.read() 84 | else: 85 | async with session.get(link) as resp: 86 | torrent = await resp.read() 87 | torrent = base64.b64encode(torrent).decode() 88 | dir = os.path.join( 89 | os.getcwd(), 90 | str(user_id), 91 | str(time.time()) 92 | ) 93 | return _raise_or_return(await aria2_request(session, 'aria2.addTorrent', [torrent, [], { 94 | 'gid': await generate_gid(session, user_id), 95 | 'dir': dir, 96 | 'seed-time': 0, 97 | 'bt-stop-timeout': str(timeout) 98 | }])) 99 | 100 | async def aria2_add_magnet(session, user_id, link, timeout=0): 101 | with tempfile.TemporaryDirectory() as tempdir: 102 | gid = _raise_or_return(await aria2_request(session, 'aria2.addUri', [[link], { 103 | 'dir': tempdir, 104 | 'bt-save-metadata': 'true', 105 | 'bt-metadata-only': 'true', 106 | 'follow-torrent': 'false' 107 | }])) 108 | try: 109 | info = await aria2_tell_status(session, gid) 110 | while info['status'] == 'active': 111 | await asyncio.sleep(0.5) 112 | info = await aria2_tell_status(session, gid) 113 | filename = os.path.join(tempdir, info['infoHash'] + '.torrent') 114 | return await aria2_add_torrent(session, user_id, filename, timeout) 115 | finally: 116 | try: 117 | await aria2_remove(session, gid) 118 | except Aria2Error as ex: 119 | if not (ex.error_code == 1 and ex.error_message == f'Active Download not found for GID#{gid}'): 120 | raise 121 | 122 | async def aria2_add_directdl(session, user_id, link, filename=None, timeout=60): 123 | dir = os.path.join( 124 | os.getcwd(), 125 | str(user_id), 126 | str(time.time()) 127 | ) 128 | options = { 129 | 'gid': await generate_gid(session, user_id), 130 | 'dir': dir, 131 | 'timeout': str(timeout), 132 | 'follow-torrent': 'false', 133 | 'header': 'User-Agent: Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0' 134 | } 135 | if filename: 136 | options['out'] = filename 137 | return _raise_or_return(await aria2_request(session, 'aria2.addUri', [[link], options])) 138 | -------------------------------------------------------------------------------- /lazyleech/utils/custom_filters.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | from pyrogram import filters 18 | 19 | def callback_data(data): 20 | def func(flt, client, callback_query): 21 | return callback_query.data in flt.data 22 | 23 | data = data if isinstance(data, list) else [data] 24 | return filters.create( 25 | func, 26 | 'CustomCallbackDataFilter', 27 | data=data 28 | ) 29 | 30 | def callback_chat(chats): 31 | def func(flt, client, callback_query): 32 | return callback_query.message.chat.id in flt.chats 33 | 34 | chats = chats if isinstance(chats, list) else [chats] 35 | return filters.create( 36 | func, 37 | 'CustomCallbackChatsFilter', 38 | chats=chats 39 | ) 40 | -------------------------------------------------------------------------------- /lazyleech/utils/misc.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import time 19 | import json 20 | import shlex 21 | import asyncio 22 | import tempfile 23 | import mimetypes 24 | from decimal import Decimal 25 | from datetime import timedelta 26 | from pyrogram.errors.exceptions.bad_request_400 import UserNotParticipant 27 | from .. import app, ADMIN_CHATS 28 | 29 | # https://stackoverflow.com/a/49361727 30 | def format_bytes(size): 31 | size = int(size) 32 | # 2**10 = 1024 33 | power = 1024 34 | n = 0 35 | power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'} 36 | while size > power: 37 | size /= power 38 | n += 1 39 | return f"{size:.2f} {power_labels[n]+'B'}" 40 | 41 | async def get_file_mimetype(filename): 42 | mimetype = mimetypes.guess_type(filename)[0] 43 | if not mimetype: 44 | proc = await asyncio.create_subprocess_exec('file', '--brief', '--mime-type', filename, stdout=asyncio.subprocess.PIPE) 45 | stdout, _ = await proc.communicate() 46 | mimetype = stdout.decode().strip() 47 | return mimetype or '' 48 | 49 | async def split_files(filename, destination_dir, no_ffmpeg=False): 50 | ext = os.path.splitext(filename)[1] 51 | if not no_ffmpeg and (await get_file_mimetype(filename)).startswith('video/'): 52 | video_info = (await get_video_info(filename))['format'] 53 | if 'duration' in video_info: 54 | times = 1 55 | ss = Decimal('0.0') 56 | duration = Decimal(video_info['duration']) 57 | files = [] 58 | while duration - ss > 1: 59 | filepath = os.path.join(destination_dir, os.path.splitext(os.path.basename(filename))[0][-(248-len(ext)):] + ('-' if ext else '.') + 'part' + str(times) + ext) 60 | proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', filename, '-ss', str(ss), '-c', 'copy', '-fs', '1900000000', filepath) 61 | await proc.communicate() 62 | video_info = (await get_video_info(filepath)).get('format') 63 | if not video_info: 64 | break 65 | if 'duration' not in video_info: 66 | break 67 | files.append(filepath) 68 | times += 1 69 | ss += Decimal(video_info['duration']) 70 | return files 71 | args = ['split', '--verbose', '--numeric-suffixes=1', '--bytes=2097152000', '--suffix-length=2'] 72 | if ext: 73 | args.append(f'--additional-suffix={ext}') 74 | args.append(filename) 75 | args.append(os.path.join(destination_dir, os.path.basename(filename)[-(248-len(ext)):] + ('-' if ext else '.') + 'part')) 76 | proc = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE) 77 | stdout, _ = await proc.communicate() 78 | return shlex.split(' '.join([i[14:] for i in stdout.decode().strip().split('\n')])) 79 | 80 | 81 | video_duration_cache = dict() 82 | video_duration_lock = asyncio.Lock() 83 | async def get_video_info(filename): 84 | proc = await asyncio.create_subprocess_exec('ffprobe', '-print_format', 'json', '-show_format', '-show_streams', filename, stdout=asyncio.subprocess.PIPE) 85 | stdout, _ = await proc.communicate() 86 | js = json.loads(stdout) 87 | if js.get('format'): 88 | if 'duration' not in js['format']: 89 | async with video_duration_lock: 90 | if filename not in video_duration_cache: 91 | with tempfile.NamedTemporaryFile(suffix='.mkv') as tempf: 92 | proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', filename, '-c', 'copy', tempf.name) 93 | await proc.communicate() 94 | video_duration_cache[filename] = (await get_video_info(tempf.name))['format']['duration'] 95 | js['format']['duration'] = video_duration_cache[filename] 96 | return js 97 | 98 | async def generate_thumbnail(videopath, photopath): 99 | video_info = await get_video_info(videopath) 100 | for duration in (10, 5, 0): 101 | if duration < float(video_info['format']['duration']): 102 | proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', videopath, '-ss', str(duration), '-frames:v', '1', photopath) 103 | await proc.communicate() 104 | break 105 | 106 | async def convert_to_jpg(original, end): 107 | proc = await asyncio.create_subprocess_exec('ffmpeg', '-y', '-i', original, end) 108 | await proc.communicate() 109 | 110 | # https://stackoverflow.com/a/34325723 111 | def return_progress_string(current, total): 112 | if total: 113 | filled_length = int(30 * current // total) 114 | else: 115 | filled_length = 0 116 | return '[' + '=' * filled_length + ' ' * (30 - filled_length) + ']' 117 | 118 | # https://stackoverflow.com/a/852718 119 | # https://stackoverflow.com/a/775095 120 | def calculate_eta(current, total, start_time): 121 | if not current or not total: 122 | return '00:00:00' 123 | end_time = time.time() 124 | elapsed_time = end_time - start_time 125 | seconds = (elapsed_time * (total / current)) - elapsed_time 126 | thing = ''.join(str(timedelta(seconds=seconds)).split('.')[:-1]).split(', ') 127 | thing[-1] = thing[-1].rjust(8, '0') 128 | return ', '.join(thing) 129 | 130 | # https://stackoverflow.com/a/10920872 131 | async def watermark_photo(main, overlay, out): 132 | 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) 133 | await proc.communicate() 134 | 135 | async def allow_admin_cancel(chat_id, user_id): 136 | if chat_id in ADMIN_CHATS: 137 | return True 138 | for i in ADMIN_CHATS: 139 | try: 140 | await app.get_chat_member(i, user_id) 141 | except UserNotParticipant: 142 | pass 143 | else: 144 | return True 145 | return False 146 | -------------------------------------------------------------------------------- /lazyleech/utils/upload_worker.py: -------------------------------------------------------------------------------- 1 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 2 | # Copyright (c) 2021 lazyleech developers 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published 6 | # by the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import re 19 | import html 20 | import time 21 | import shutil 22 | import logging 23 | import asyncio 24 | import zipfile 25 | import tempfile 26 | import traceback 27 | from pyrogram import StopTransmission 28 | from collections import defaultdict 29 | from natsort import natsorted 30 | from pyrogram import StopTransmission 31 | from pyrogram.parser import html as pyrogram_html 32 | from pyrogram.errors.exceptions.bad_request_400 import MessageIdInvalid, MessageNotModified 33 | from .. import PROGRESS_UPDATE_DELAY, ADMIN_CHATS, preserved_logs, TESTMODE, SendAsZipFlag, ForceDocumentFlag, LICHER_CHAT, LICHER_STICKER, LICHER_FOOTER, LICHER_PARSE_EPISODE, IGNORE_PADDING_FILE 34 | from .misc import split_files, get_file_mimetype, format_bytes, get_video_info, generate_thumbnail, return_progress_string, calculate_eta, watermark_photo 35 | 36 | upload_queue = asyncio.Queue() 37 | upload_statuses = dict() 38 | upload_tamper_lock = asyncio.Lock() 39 | message_exists = defaultdict(set) 40 | message_exists_lock = asyncio.Lock() 41 | async def upload_worker(): 42 | while True: 43 | client, message, reply, torrent_info, user_id, flags = await upload_queue.get() 44 | try: 45 | message_identifier = (reply.chat.id, reply.message_id) 46 | if SendAsZipFlag not in flags: 47 | asyncio.create_task(reply.edit_text('Download successful, uploading files...')) 48 | task = asyncio.create_task(_upload_worker(client, message, reply, torrent_info, user_id, flags)) 49 | upload_statuses[message_identifier] = task, user_id 50 | await task 51 | except asyncio.CancelledError: 52 | text = 'Your leech has been cancelled.' 53 | await asyncio.gather(reply.edit_text(text), message.reply_text(text)) 54 | except Exception as ex: 55 | preserved_logs.append((message, torrent_info, ex)) 56 | logging.exception('%s %s', message, torrent_info) 57 | await message.reply_text(traceback.format_exc(), parse_mode=None) 58 | for admin_chat in ADMIN_CHATS: 59 | await client.send_message(admin_chat, traceback.format_exc(), parse_mode=None) 60 | finally: 61 | upload_queue.task_done() 62 | worker_identifier = (reply.chat.id, reply.message_id) 63 | to_delete = [] 64 | async with upload_tamper_lock: 65 | for key in upload_waits: 66 | _, iworker_identifier = upload_waits[key] 67 | if iworker_identifier == worker_identifier: 68 | upload_waits.pop(key) 69 | to_delete.append(key[1]) 70 | task = None 71 | if to_delete: 72 | task = asyncio.create_task(client.delete_messages(reply.chat.id, to_delete)) 73 | upload_statuses.pop(message_identifier) 74 | if not TESTMODE: 75 | shutil.rmtree(torrent_info['dir']) 76 | if task: 77 | await task 78 | 79 | upload_waits = dict() 80 | async def _upload_worker(client, message, reply, torrent_info, user_id, flags): 81 | files = dict() 82 | sent_files = [] 83 | with tempfile.TemporaryDirectory(dir=str(user_id)) as zip_tempdir: 84 | if SendAsZipFlag in flags: 85 | if torrent_info.get('bittorrent'): 86 | filename = torrent_info['bittorrent']['info']['name'] 87 | else: 88 | filename = os.path.basename(torrent_info['files'][0]['path']) 89 | filename = filename[-251:] + '.zip' 90 | filepath = os.path.join(zip_tempdir, filename) 91 | def _zip_files(): 92 | with zipfile.ZipFile(filepath, 'x') as zipf: 93 | for file in torrent_info['files']: 94 | filename = file['path'].replace(os.path.join(torrent_info['dir'], ''), '', 1) 95 | if IGNORE_PADDING_FILE and re.match(r'(?i)^_+padding_file', filename) is not None: 96 | continue 97 | zipf.write(file['path'], filename) 98 | await asyncio.gather(reply.edit_text('Download successful, zipping files...'), client.loop.run_in_executor(None, _zip_files)) 99 | asyncio.create_task(reply.edit_text('Download successful, uploading files...')) 100 | files[filepath] = filename 101 | else: 102 | for file in torrent_info['files']: 103 | filepath = file['path'] 104 | filename = filepath.replace(os.path.join(torrent_info['dir'], ''), '', 1) 105 | if IGNORE_PADDING_FILE and re.match(r'(?i)^_+padding_file', filename) is not None: 106 | continue 107 | if LICHER_PARSE_EPISODE: 108 | filename = re.sub(r'\s*(?:\[.+?\]|\(.+?\))\s*|\.[a-z][a-z0-9]{2}$', '', os.path.basename(filepath)).strip() or filename 109 | files[filepath] = filename 110 | for filepath in natsorted(files): 111 | sent_files.extend(await _upload_file(client, message, reply, files[filepath], filepath, ForceDocumentFlag in flags)) 112 | text = 'Files:\n' 113 | parser = pyrogram_html.HTML(client) 114 | quote = None 115 | first_index = None 116 | all_amount = 1 117 | for filename, filelink in sent_files: 118 | if filelink: 119 | atext = f'- {html.escape(filename)}' 120 | else: 121 | atext = f'- {html.escape(filename)} (empty)' 122 | atext += '\n' 123 | futtext = text + atext 124 | if all_amount > 100 or len((await parser.parse(futtext))['message']) > 4096: 125 | thing = await message.reply_text(text, quote=quote, disable_web_page_preview=True) 126 | if first_index is None: 127 | first_index = thing 128 | quote = False 129 | futtext = atext 130 | all_amount = 1 131 | await asyncio.sleep(PROGRESS_UPDATE_DELAY) 132 | all_amount += 1 133 | text = futtext 134 | if not sent_files: 135 | text = 'Files: None' 136 | elif LICHER_CHAT and LICHER_STICKER and message.chat.id in ADMIN_CHATS: 137 | await client.send_sticker(LICHER_CHAT, LICHER_STICKER) 138 | thing = await message.reply_text(text, quote=quote, disable_web_page_preview=True) 139 | if first_index is None: 140 | first_index = thing 141 | asyncio.create_task(reply.edit_text(f'Download successful, files uploaded.\nFiles: {first_index.link}', disable_web_page_preview=True)) 142 | 143 | async def _upload_file(client, message, reply, filename, filepath, force_document): 144 | if not os.path.getsize(filepath): 145 | return [(os.path.basename(filename), None)] 146 | worker_identifier = (reply.chat.id, reply.message_id) 147 | user_id = message.from_user.id 148 | user_thumbnail = os.path.join(str(user_id), 'thumbnail.jpg') 149 | user_watermark = os.path.join(str(user_id), 'watermark.jpg') 150 | user_watermarked_thumbnail = os.path.join(str(user_id), 'watermarked_thumbnail.jpg') 151 | file_has_big = os.path.getsize(filepath) > 2097152000 152 | upload_wait = await reply.reply_text(f'Upload of {html.escape(filename)} will start in {PROGRESS_UPDATE_DELAY}s') 153 | message_exists[upload_wait.chat.id].add(upload_wait.message_id) 154 | upload_identifier = (upload_wait.chat.id, upload_wait.message_id) 155 | async with upload_tamper_lock: 156 | upload_waits[upload_identifier] = user_id, worker_identifier 157 | to_upload = [] 158 | sent_files = [] 159 | split_task = None 160 | try: 161 | with tempfile.TemporaryDirectory(dir=str(user_id)) as tempdir: 162 | if file_has_big: 163 | async def _split_files(): 164 | splitted = await split_files(filepath, tempdir, force_document) 165 | for a, split in enumerate(splitted, 1): 166 | to_upload.append((split, filename + f' (part {a})')) 167 | split_task = asyncio.create_task(_split_files()) 168 | else: 169 | to_upload.append((filepath, filename)) 170 | for _ in range(PROGRESS_UPDATE_DELAY): 171 | if upload_identifier in stop_uploads: 172 | return sent_files 173 | await asyncio.sleep(1) 174 | if upload_identifier in stop_uploads: 175 | return sent_files 176 | if split_task and not split_task.done(): 177 | await upload_wait.edit_text(f'Splitting {html.escape(filename)}...') 178 | while not split_task.done(): 179 | if upload_identifier in stop_uploads: 180 | return sent_files 181 | await asyncio.sleep(1) 182 | if upload_identifier in stop_uploads: 183 | return sent_files 184 | for a, (filepath, filename) in enumerate(to_upload): 185 | while True: 186 | if a: 187 | async with upload_tamper_lock: 188 | upload_waits.pop(upload_identifier) 189 | upload_wait = await reply.reply_text(f'Upload of {html.escape(filename)} will start in {PROGRESS_UPDATE_DELAY}s') 190 | upload_identifier = (upload_wait.chat.id, upload_wait.message_id) 191 | upload_waits[upload_identifier] = user_id, worker_identifier 192 | for _ in range(PROGRESS_UPDATE_DELAY): 193 | if upload_identifier in stop_uploads: 194 | return sent_files 195 | await asyncio.sleep(1) 196 | if upload_identifier in stop_uploads: 197 | return sent_files 198 | thumbnail = None 199 | for i in (user_thumbnail, user_watermarked_thumbnail): 200 | thumbnail = i if os.path.isfile(i) else thumbnail 201 | mimetype = await get_file_mimetype(filepath) 202 | progress_args = (client, message, upload_wait, filename, user_id) 203 | try: 204 | if not force_document and mimetype.startswith('video/'): 205 | duration = 0 206 | video_json = await get_video_info(filepath) 207 | video_format = video_json.get('format') 208 | if video_format and 'duration' in video_format: 209 | duration = round(float(video_format['duration'])) 210 | for stream in video_json.get('streams', ()): 211 | if stream['codec_type'] == 'video': 212 | width = stream.get('width') 213 | height = stream.get('height') 214 | if width and height: 215 | if not thumbnail: 216 | thumbnail = os.path.join(tempdir, '0.jpg') 217 | await generate_thumbnail(filepath, thumbnail) 218 | if os.path.isfile(thumbnail) and os.path.isfile(user_watermark): 219 | othumbnail = thumbnail 220 | thumbnail = os.path.join(tempdir, '1.jpg') 221 | await watermark_photo(othumbnail, user_watermark, thumbnail) 222 | if not os.path.isfile(thumbnail): 223 | thumbnail = othumbnail 224 | if not os.path.isfile(thumbnail): 225 | thumbnail = None 226 | break 227 | else: 228 | width = height = 0 229 | resp = await reply.reply_video(filepath, thumb=thumbnail, caption=filename, 230 | duration=duration, width=width, height=height, 231 | parse_mode=None, progress=progress_callback, 232 | progress_args=progress_args) 233 | else: 234 | resp = await reply.reply_document(filepath, thumb=thumbnail, caption=filename, 235 | parse_mode=None, progress=progress_callback, 236 | progress_args=progress_args) 237 | except StopTransmission: 238 | resp = None 239 | except Exception: 240 | await message.reply_text(traceback.format_exc(), parse_mode=None) 241 | break 242 | if resp: 243 | sent_files.append((os.path.basename(filename), resp.link)) 244 | if LICHER_CHAT and reply.chat.id in ADMIN_CHATS and mimetype.startswith('video/') and resp.video: 245 | await client.send_video(LICHER_CHAT, resp.video.file_id, thumb=thumbnail, 246 | caption=filename + LICHER_FOOTER, duration=duration, 247 | width=width, height=height, parse_mode=None) 248 | break 249 | return sent_files 250 | return sent_files 251 | finally: 252 | if split_task: 253 | split_task.cancel() 254 | async with message_exists_lock: 255 | message_exists[upload_wait.chat.id].discard(upload_wait.message_id) 256 | asyncio.create_task(upload_wait.delete()) 257 | async with upload_tamper_lock: 258 | upload_waits.pop(upload_identifier) 259 | 260 | progress_callback_data = dict() 261 | stop_uploads = set() 262 | async def progress_callback(current, total, client, message, reply, filename, user_id): 263 | try: 264 | if reply.message_id not in message_exists[reply.chat.id]: 265 | return 266 | message_identifier = (reply.chat.id, reply.message_id) 267 | last_edit_time, prevtext, start_time, user_id = progress_callback_data.get(message_identifier, (0, None, time.time(), user_id)) 268 | if message_identifier in stop_uploads or current == total: 269 | asyncio.create_task(reply.delete()) 270 | try: 271 | progress_callback_data.pop(message_identifier) 272 | except KeyError: 273 | pass 274 | if message_identifier in stop_uploads: 275 | client.stop_transmission() 276 | elif (time.time() - last_edit_time) > PROGRESS_UPDATE_DELAY: 277 | if last_edit_time: 278 | upload_speed = format_bytes((total - current) / (time.time() - start_time)) 279 | else: 280 | upload_speed = '0 B' 281 | text = f'''Uploading {html.escape(filename)}... 282 | {html.escape(return_progress_string(current, total))} 283 | Total Size: {format_bytes(total)} 284 | Uploaded Size: {format_bytes(current)} 285 | Upload Speed: {upload_speed}/s 286 | ETA: {calculate_eta(current, total, start_time)}''' 287 | if prevtext != text and reply.message_id in message_exists[reply.chat.id]: 288 | async with message_exists_lock: 289 | if reply.message_id not in message_exists[reply.chat.id]: 290 | return 291 | try: 292 | await reply.edit_text(text) 293 | except MessageIdInvalid: 294 | message_exists[reply.chat.id].discard(reply.message_id) 295 | return 296 | except MessageNotModified: 297 | pass 298 | prevtext = text 299 | last_edit_time = time.time() 300 | progress_callback_data[message_identifier] = last_edit_time, prevtext, start_time, user_id 301 | except StopTransmission: 302 | raise 303 | except Exception as ex: 304 | preserved_logs.append((message, None, ex)) 305 | logging.exception('%s', message) 306 | await message.reply_text(traceback.format_exc(), parse_mode=None) 307 | for admin_chat in ADMIN_CHATS: 308 | await client.send_message(admin_chat, traceback.format_exc(), parse_mode=None) 309 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 3 | # Copyright (c) 2021 lazyleech developers 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as published 7 | # by the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import glob 19 | 20 | LICENSE_HEADER = ''' 21 | # lazyleech - Telegram bot primarily to leech from torrents and upload to Telegram 22 | # Copyright (c) 2021 lazyleech developers 23 | # 24 | # This program is free software: you can redistribute it and/or modify 25 | # it under the terms of the GNU Affero General Public License as published 26 | # by the Free Software Foundation, either version 3 of the License, or 27 | # (at your option) any later version. 28 | # 29 | # This program is distributed in the hope that it will be useful, 30 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 31 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 | # GNU Affero General Public License for more details. 33 | # 34 | # You should have received a copy of the GNU Affero General Public License 35 | # along with this program. If not, see . 36 | '''.strip() 37 | 38 | missing_header = False 39 | for file in glob.iglob('lazyleech/**/*.py', recursive=True): 40 | with open(file, 'r') as fileobj: 41 | file_header = fileobj.read(len(LICENSE_HEADER)) 42 | if file_header != LICENSE_HEADER: 43 | print(file, 'is missing AGPL license header') 44 | missing_header = True 45 | if missing_header: 46 | exit(1) 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiohttp[speedups]>=3.7.3 3 | feedparser 4 | pyrogram==1.4.16 5 | tgcrypto 6 | natsort 7 | requests 8 | bs4 9 | apscheduler 10 | motor 11 | dnspython 12 | wget>=3.2 13 | youtube-search-python 14 | ujson 15 | git+https://github.com/ytdl-org/youtube-dl 16 | html-telegraph-poster>=0.2.31 -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | touch aria2.log lazyleech.log 3 | tail -f aria2.log & 4 | tail -f lazyleech.log & 5 | # https://unix.stackexchange.com/a/230676 6 | export ARIA2_SECRET=$(tr -dc 'A-Za-z0-9!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~' aria2.log 2>&1 & 8 | python3 -m lazyleech > lazyleech.log 2>&1 9 | -------------------------------------------------------------------------------- /testwatermark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lostb053/lazyleech/436c5d25814ce7aac92638e35285340b1bf0dad1/testwatermark.jpg -------------------------------------------------------------------------------- /ytdl/__init__.py: -------------------------------------------------------------------------------- 1 | # just place to store json n stuff 2 | -------------------------------------------------------------------------------- /ytdl/downloads/__init__.py: -------------------------------------------------------------------------------- 1 | # nothing imp just download location 2 | --------------------------------------------------------------------------------