commit 70666b050e30376148e09867f95c9e6aabb03e41 Author: ben Date: Wed Sep 6 20:37:15 2023 -0400 first commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a3fb768 --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +# +# Copyright (C) 2008-2014 The LuCI Team +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=rTorrent LuCI web interface +LUCI_DEPENDS:=+rtorrent-rpc +luaexpat +luasocket +luasec +screen + +PKG_LICENSE:=GPLv3 + +define Package//luci-app-rtorrent/conffiles + /etc/config/rtorrent + /etc/rtorrent.conf +endef + +define Package/luci-app-rtorrent/postinst + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache +endef + +define Package/luci-app-rtorrent/postrm + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/README.md b/README.md new file mode 100644 index 0000000..d867e75 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# luci-app-rtorrent +rTorrent client for OpenWrt's LuCI web interface + +## Features +- List all torrent downloads +- Add new torrent by url/magnet uri/file +- Stop/start/delete torrents +- Mark torrents with tags +- Set priority per file +- Enable/disable and add trackers to torrent +- Detailed peer listing +- Completely LuCI based interface +- OpenWrt device independent (written in lua) +- Opkg package manager support +- RSS feed downloader (automatically download torrents that match the specified criteria) +- Support for [Transdroid](https://www.transdroid.org/) ([Transdrone](https://play.google.com/store/apps/details?id=org.transdroid.lite)) + +## Screenshots +[luci-app-rtorrent 0.1.3](https://github.com/wolandmaster/luci-app-rtorrent/wiki/Screenshots) + +## Install instructions +(for Openwrt 15.05.1 Chaos Calmer) + +### Install rtorrent-rpc +``` +opkg update +opkg install rtorrent-rpc +``` +(Note: If you going to install rtorrent-rpc to an own [opkg destination](https://wiki.openwrt.org/doc/techref/opkg#installation_destinations) then you have to install libopenssl to the root destination before) + +### Create rTorrent config file + +#### Minimal _/root/.rtorrent.rc_ file: +``` +directory = /path/to/downloads/ +session = /path/to/session/ + +scgi_port = 127.0.0.1:5000 + +schedule = rss_downloader,300,300,"execute=/usr/lib/lua/rss_downloader.lua" +``` +#### Sample _/root/.rtorrent.rc_ file: +http://pissedoffadmins.com/os/linux/sample-rtorrent-rc-file.html + +#### Recommended kernel parameters to avoid low memory issues: +``` +cat /etc/sysctl.conf +... +# handle rtorrent related low memory issues +vm.swappiness=95 +vm.vfs_cache_pressure=200 +vm.min_free_kbytes=4096 +vm.overcommit_memory=2 +vm.overcommit_ratio=60 +``` + +### Create init.d script (optional) + +#### Install screen +``` +opkg install screen +``` + +#### Create _/etc/init.d/rtorrent_ script +``` +#!/bin/sh /etc/rc.common + +START=99 +STOP=99 + +SCREEN=/usr/sbin/screen +PROG=/usr/bin/rtorrent + +start() { + sleep 3 + $SCREEN -dm -t rtorrent nice -19 $PROG +} + +stop() { + killall rtorrent +} +``` + +#### Start rtorrent +``` +chmod +x /etc/init.d/rtorrent +/etc/init.d/rtorrent enable +/etc/init.d/rtorrent start +``` + +### Install wget +(the wget in busybox does not support https) +``` +opkg install ca-certificates +opkg install wget +ln -sf $(which wget-ssl) /usr/bin/wget +``` + +### Install luci-app-rtorrent +``` +wget -nv https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest/e1a1ba8004c4220f -O /etc/opkg/keys/e1a1ba8004c4220f +echo 'src/gz luci_app_rtorrent https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest' >> /etc/opkg.conf +opkg update +opkg install luci-app-rtorrent +``` + +### Upgrade already installed version +``` +opkg update +opkg upgrade luci-app-rtorrent +``` + +### References + + + diff --git a/htdocs/cgi-bin/rtorrent b/htdocs/cgi-bin/rtorrent new file mode 100644 index 0000000..e8d33fd --- /dev/null +++ b/htdocs/cgi-bin/rtorrent @@ -0,0 +1,4 @@ +#!/usr/bin/lua +local rtorrent = require "rtorrent" +io.write(rtorrent.rpc(io.read("*all"))) +io.flush() diff --git a/htdocs/luci-static/resources/icons/filetypes/7z.png b/htdocs/luci-static/resources/icons/filetypes/7z.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/7z.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/ac3.png b/htdocs/luci-static/resources/icons/filetypes/ac3.png new file mode 100755 index 0000000..73e900c --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/ac3.png @@ -0,0 +1 @@ +music.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/app.png b/htdocs/luci-static/resources/icons/filetypes/app.png new file mode 100644 index 0000000..1dee9e3 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/app.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/archive.png b/htdocs/luci-static/resources/icons/filetypes/archive.png new file mode 100644 index 0000000..fd4bbcc Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/archive.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/arj.png b/htdocs/luci-static/resources/icons/filetypes/arj.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/arj.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/avi.png b/htdocs/luci-static/resources/icons/filetypes/avi.png new file mode 100755 index 0000000..1827c55 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/avi.png @@ -0,0 +1 @@ +video.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/bat.png b/htdocs/luci-static/resources/icons/filetypes/bat.png new file mode 100755 index 0000000..cd8b150 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/bat.png @@ -0,0 +1 @@ +script.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/bmp.png b/htdocs/luci-static/resources/icons/filetypes/bmp.png new file mode 100755 index 0000000..4a0532b --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/bmp.png @@ -0,0 +1 @@ +image.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/bz2.png b/htdocs/luci-static/resources/icons/filetypes/bz2.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/bz2.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/c.png b/htdocs/luci-static/resources/icons/filetypes/c.png new file mode 100755 index 0000000..1327266 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/c.png @@ -0,0 +1 @@ +code.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/cab.png b/htdocs/luci-static/resources/icons/filetypes/cab.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/cab.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/cgi.png b/htdocs/luci-static/resources/icons/filetypes/cgi.png new file mode 100755 index 0000000..cd8b150 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/cgi.png @@ -0,0 +1 @@ +script.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/code.png b/htdocs/luci-static/resources/icons/filetypes/code.png new file mode 100644 index 0000000..0c76bd1 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/code.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/cpp.png b/htdocs/luci-static/resources/icons/filetypes/cpp.png new file mode 100755 index 0000000..1327266 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/cpp.png @@ -0,0 +1 @@ +code.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/css.png b/htdocs/luci-static/resources/icons/filetypes/css.png new file mode 100644 index 0000000..f907e44 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/css.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/db.png b/htdocs/luci-static/resources/icons/filetypes/db.png new file mode 100644 index 0000000..bddba1f Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/db.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/dir.png b/htdocs/luci-static/resources/icons/filetypes/dir.png new file mode 100644 index 0000000..784e8fa Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/dir.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/doc.png b/htdocs/luci-static/resources/icons/filetypes/doc.png new file mode 100644 index 0000000..ae8ecbf Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/doc.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/dts.png b/htdocs/luci-static/resources/icons/filetypes/dts.png new file mode 100755 index 0000000..73e900c --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/dts.png @@ -0,0 +1 @@ +music.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/elf.png b/htdocs/luci-static/resources/icons/filetypes/elf.png new file mode 100755 index 0000000..2479d59 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/elf.png @@ -0,0 +1 @@ +linux.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/exe.png b/htdocs/luci-static/resources/icons/filetypes/exe.png new file mode 100755 index 0000000..ba5adcc --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/exe.png @@ -0,0 +1 @@ +app.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/f4v.png b/htdocs/luci-static/resources/icons/filetypes/f4v.png new file mode 100755 index 0000000..c9c6662 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/f4v.png @@ -0,0 +1 @@ +flash.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/file.png b/htdocs/luci-static/resources/icons/filetypes/file.png new file mode 100644 index 0000000..8b8b1ca Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/file.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/flac.png b/htdocs/luci-static/resources/icons/filetypes/flac.png new file mode 100755 index 0000000..73e900c --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/flac.png @@ -0,0 +1 @@ +music.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/flash.png b/htdocs/luci-static/resources/icons/filetypes/flash.png new file mode 100644 index 0000000..5769120 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/flash.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/flv.png b/htdocs/luci-static/resources/icons/filetypes/flv.png new file mode 100755 index 0000000..c9c6662 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/flv.png @@ -0,0 +1 @@ +flash.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/gif.png b/htdocs/luci-static/resources/icons/filetypes/gif.png new file mode 100755 index 0000000..4a0532b --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/gif.png @@ -0,0 +1 @@ +image.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/gz.png b/htdocs/luci-static/resources/icons/filetypes/gz.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/gz.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/h.png b/htdocs/luci-static/resources/icons/filetypes/h.png new file mode 100755 index 0000000..1327266 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/h.png @@ -0,0 +1 @@ +code.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/htm.png b/htdocs/luci-static/resources/icons/filetypes/htm.png new file mode 100755 index 0000000..28d03f5 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/htm.png @@ -0,0 +1 @@ +html.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/html.png b/htdocs/luci-static/resources/icons/filetypes/html.png new file mode 100644 index 0000000..6ed2490 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/html.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/image.png b/htdocs/luci-static/resources/icons/filetypes/image.png new file mode 100644 index 0000000..4a158fe Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/image.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/iso.png b/htdocs/luci-static/resources/icons/filetypes/iso.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/iso.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/jar.png b/htdocs/luci-static/resources/icons/filetypes/jar.png new file mode 100755 index 0000000..c69737d --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/jar.png @@ -0,0 +1 @@ +java.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/java.png b/htdocs/luci-static/resources/icons/filetypes/java.png new file mode 100644 index 0000000..b7bfcd1 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/java.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/jpeg.png b/htdocs/luci-static/resources/icons/filetypes/jpeg.png new file mode 100755 index 0000000..4a0532b --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/jpeg.png @@ -0,0 +1 @@ +image.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/jpg.png b/htdocs/luci-static/resources/icons/filetypes/jpg.png new file mode 100755 index 0000000..4a0532b --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/jpg.png @@ -0,0 +1 @@ +image.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/linux.png b/htdocs/luci-static/resources/icons/filetypes/linux.png new file mode 100644 index 0000000..52699bf Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/linux.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/lua.png b/htdocs/luci-static/resources/icons/filetypes/lua.png new file mode 100755 index 0000000..cd8b150 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/lua.png @@ -0,0 +1 @@ +script.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/mkv.png b/htdocs/luci-static/resources/icons/filetypes/mkv.png new file mode 100755 index 0000000..1827c55 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/mkv.png @@ -0,0 +1 @@ +video.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/mov.png b/htdocs/luci-static/resources/icons/filetypes/mov.png new file mode 100755 index 0000000..1827c55 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/mov.png @@ -0,0 +1 @@ +video.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/mp3.png b/htdocs/luci-static/resources/icons/filetypes/mp3.png new file mode 100755 index 0000000..73e900c --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/mp3.png @@ -0,0 +1 @@ +music.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/mp4.png b/htdocs/luci-static/resources/icons/filetypes/mp4.png new file mode 100755 index 0000000..1827c55 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/mp4.png @@ -0,0 +1 @@ +video.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/music.png b/htdocs/luci-static/resources/icons/filetypes/music.png new file mode 100644 index 0000000..a8b3ede Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/music.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/nfo.png b/htdocs/luci-static/resources/icons/filetypes/nfo.png new file mode 100755 index 0000000..8ac1dae --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/nfo.png @@ -0,0 +1 @@ +txt.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/pdf.png b/htdocs/luci-static/resources/icons/filetypes/pdf.png new file mode 100644 index 0000000..8f8095e Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/pdf.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/php.png b/htdocs/luci-static/resources/icons/filetypes/php.png new file mode 100644 index 0000000..7868a25 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/php.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/pl.png b/htdocs/luci-static/resources/icons/filetypes/pl.png new file mode 100755 index 0000000..cd8b150 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/pl.png @@ -0,0 +1 @@ +script.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/png.png b/htdocs/luci-static/resources/icons/filetypes/png.png new file mode 100755 index 0000000..4a0532b --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/png.png @@ -0,0 +1 @@ +image.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/ppt.png b/htdocs/luci-static/resources/icons/filetypes/ppt.png new file mode 100644 index 0000000..c4eff03 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/ppt.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/psd.png b/htdocs/luci-static/resources/icons/filetypes/psd.png new file mode 100644 index 0000000..73c5b3f Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/psd.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/rar.png b/htdocs/luci-static/resources/icons/filetypes/rar.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/rar.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/script.png b/htdocs/luci-static/resources/icons/filetypes/script.png new file mode 100644 index 0000000..63fe6ce Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/script.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/sh.png b/htdocs/luci-static/resources/icons/filetypes/sh.png new file mode 100755 index 0000000..cd8b150 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/sh.png @@ -0,0 +1 @@ +script.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/sqlite.png b/htdocs/luci-static/resources/icons/filetypes/sqlite.png new file mode 100755 index 0000000..e25de78 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/sqlite.png @@ -0,0 +1 @@ +db.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/srt.png b/htdocs/luci-static/resources/icons/filetypes/srt.png new file mode 100755 index 0000000..8ac1dae --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/srt.png @@ -0,0 +1 @@ +txt.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/tar.png b/htdocs/luci-static/resources/icons/filetypes/tar.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/tar.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/tgz.png b/htdocs/luci-static/resources/icons/filetypes/tgz.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/tgz.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/txt.png b/htdocs/luci-static/resources/icons/filetypes/txt.png new file mode 100644 index 0000000..813f712 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/txt.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/up.png b/htdocs/luci-static/resources/icons/filetypes/up.png new file mode 100644 index 0000000..c073cd0 Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/up.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/video.png b/htdocs/luci-static/resources/icons/filetypes/video.png new file mode 100644 index 0000000..b0ce7bb Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/video.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/wma.png b/htdocs/luci-static/resources/icons/filetypes/wma.png new file mode 100755 index 0000000..73e900c --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/wma.png @@ -0,0 +1 @@ +music.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/wmv.png b/htdocs/luci-static/resources/icons/filetypes/wmv.png new file mode 100755 index 0000000..1827c55 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/wmv.png @@ -0,0 +1 @@ +video.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/filetypes/xls.png b/htdocs/luci-static/resources/icons/filetypes/xls.png new file mode 100644 index 0000000..b977d7e Binary files /dev/null and b/htdocs/luci-static/resources/icons/filetypes/xls.png differ diff --git a/htdocs/luci-static/resources/icons/filetypes/zip.png b/htdocs/luci-static/resources/icons/filetypes/zip.png new file mode 100755 index 0000000..b7993b2 --- /dev/null +++ b/htdocs/luci-static/resources/icons/filetypes/zip.png @@ -0,0 +1 @@ +archive.png \ No newline at end of file diff --git a/htdocs/luci-static/resources/icons/unknown_tracker.png b/htdocs/luci-static/resources/icons/unknown_tracker.png new file mode 100644 index 0000000..ceb92f7 Binary files /dev/null and b/htdocs/luci-static/resources/icons/unknown_tracker.png differ diff --git a/luasrc/controller/rtorrent.lua b/luasrc/controller/rtorrent.lua new file mode 100644 index 0000000..93072ea --- /dev/null +++ b/luasrc/controller/rtorrent.lua @@ -0,0 +1,35 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local nixio = require "nixio" +local dm = require "luci.model.cbi.rtorrent.download" + +module("luci.controller.rtorrent", package.seeall) + +function index() + entry({"admin", "rtorrent"}, firstchild(), translate("Torrent"), 45).dependent = false + entry({"admin", "rtorrent", "main"}, form("rtorrent/main"), translate("Torrent List"), 10).leaf = true + entry({"admin", "rtorrent", "add"}, form("rtorrent/add", {autoapply=true}), translate("Add Torrent"), 20) + entry({"admin", "rtorrent", "rss"}, arcombine(cbi("rtorrent/rss"), cbi("rtorrent/rss-rule")), translate("RSS Downloader"), 30).leaf = true + entry({"admin", "rtorrent", "admin"}, form("rtorrent/admin/rtorrent"), translate("Torrent Settings"), 40) + + entry({"admin", "rtorrent", "info"}, form("rtorrent/torrent/info"), nil).leaf = true + entry({"admin", "rtorrent", "files"}, form("rtorrent/torrent/files"), nil).leaf = true + entry({"admin", "rtorrent", "trackers"}, form("rtorrent/torrent/trackers"), nil).leaf = true + entry({"admin", "rtorrent", "peers"}, form("rtorrent/torrent/peers"), nil).leaf = true + + entry({"admin", "rtorrent", "download"}, call("download"), nil).leaf = true + entry({"admin", "rtorrent", "downloadall"}, call("downloadall"), nil).leaf = true + + entry({"admin", "rtorrent", "admin", "rtorrent"}, form("rtorrent/admin/rtorrent"), nil).leaf = true + entry({"admin", "rtorrent", "admin", "rss"}, cbi("rtorrent/admin/rss"), nil).leaf = true +end + +function download() + dm.download_file(nixio.bin.b64decode(luci.dispatcher.context.requestpath[4])) +end + +function downloadall() + dm.download_all(nixio.bin.b64decode(luci.dispatcher.context.requestpath[4])) +end + diff --git a/luasrc/model/cbi/rtorrent/add.lua b/luasrc/model/cbi/rtorrent/add.lua new file mode 100644 index 0000000..6b8dd5a --- /dev/null +++ b/luasrc/model/cbi/rtorrent/add.lua @@ -0,0 +1,74 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local bencode = require "bencode" +local nixio = require "nixio" +local rtorrent = require "rtorrent" +local xmlrpc = require "xmlrpc" +local common = require "luci.model.cbi.rtorrent.common" +require "luci.model.cbi.rtorrent.string" + +f = SimpleForm("rtorrent", translate("Add Torrent")) +f.submit = "Add" + +local torrent + +uri = f:field(TextValue, "uri", translate("Torrent
or magnet URI")) +uri.rows = 1 + +function uri.validate(self, value, section) + if "magnet:" == string.sub(value:trim(), 1, 7) then + torrent = bencode.encode({ ["magnet-uri"] = value:trim() }) + else + local ok, res = common.get(value) + if not ok then return nil, "Not able to download torrent: " .. res end + local tab, err = bencode.decode(res) + if not tab then return nil, "Not able to parse torrent file: " .. err end + torrent = res + end + return value +end + +file = f:field(FileUpload, "file", translate("Upload torrent file")) +file.root_directory = "/etc/luci-uploads" + +function file.validate(self, value, section) + torrent = nixio.fs.readfile(value) + self:remove(section) + local tab, err = bencode.decode(torrent) + if not tab then return nil, "Not able to parse torrent file: " .. err end + return value +end + +dir = f:field(Value, "dir", translate("Download directory")) +dir.default = rtorrent.call("directory.default") +dir.datatype = "directory" +dir.rmempty = false + +tags = f:field(Value, "tags", translate("Tags")) +local user = luci.dispatcher.context.authuser +tags.default = "all" .. (user ~= "root" and " " .. user or "") +tags.rmempty = false + +start = f:field(Flag, "start", translate("Start now")) +start.default = "1" +start.rmempty = false + +function f.handle(self, state, data) + if state == FORM_VALID and torrent and #torrent > 0 then + local params = {} + table.insert(params, data.start == "1" and "load.raw_start" or "load.raw") + table.insert(params, "") -- target + table.insert(params, xmlrpc.newTypedValue((nixio.bin.b64encode(torrent)), "base64")) + table.insert(params, "d.directory.set=\"" .. data.dir .. "\"") + table.insert(params, "d.custom1.set=\"" .. data.tags .. "\"") + if data.uri then + table.insert(params, "d.custom3.set=" .. nixio.bin.b64encode(data.uri)) + end + rtorrent.call(unpack(params)) + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/add")) + end + return true +end + +return f diff --git a/luasrc/model/cbi/rtorrent/admin/rss.lua b/luasrc/model/cbi/rtorrent/admin/rss.lua new file mode 100644 index 0000000..f66f682 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/admin/rss.lua @@ -0,0 +1,48 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local common = require "luci.model.cbi.rtorrent.common" +local nixio = require "nixio" + +m = Map("rtorrent", "Admin - RSS Downloader") + +s = m:section(TypedSection, "rss-feed") +s.addremove = true +s.anonymous = true +s.sortable = true +s.template = "cbi/tblsection" +s.render = function(self, section, scope) + luci.template.render("rtorrent/tabmenu", { self = { + pages = common.get_admin_pages(), + page = "RSS" + }}) + TypedSection.render(self, section, scope) +end + +name = s:option(Value, "name", "Name") +name.rmempty = false + +url = s:option(Value, "url", "RSS Feed URL") +url.size = "65" +url.rmempty = false + +enabled = s:option(Flag, "enabled", "Enabled") +enabled.rmempty = false + +t = m:section(NamedSection, "logging", "rss", "Logging") + +feed_logging = t:option(Flag, "feed_logging", "Enable RSS feed logging") + +feed_logfile = t:option(Value, "feed_logfile", "RSS feed logfile") +feed_logfile:depends("feed_logging", 1) + +function feed_logfile.validate(self, value, section) + local parent_folder = nixio.fs.dirname(value) + if parent_folder == "." or nixio.fs.stat(parent_folder, "type") ~= "dir" then + return nil, "Wrong filename, please use absolute path!" + end + return value +end + +return m + diff --git a/luasrc/model/cbi/rtorrent/admin/rtorrent.lua b/luasrc/model/cbi/rtorrent/admin/rtorrent.lua new file mode 100644 index 0000000..19ae2db --- /dev/null +++ b/luasrc/model/cbi/rtorrent/admin/rtorrent.lua @@ -0,0 +1,126 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" +local common = require "luci.model.cbi.rtorrent.common" + +local config = rtorrent.batchcall({ + "throttle.global_up.max_rate", "throttle.global_down.max_rate", + "throttle.max_downloads.global", "throttle.max_uploads.global", + "throttle.max_uploads", + "throttle.min_peers.normal", "throttle.max_peers.normal", + "throttle.min_peers.seed", "throttle.max_peers.seed" +}) + +local function set_config(key, value) + if tonumber(value) ~= config[key] then + rtorrent.call(key .. ".set", "", value) + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/admin/rtorrent")) + end +end + +f = SimpleForm("rtorrent", translate("Admin - rTorrent")) + +speed = f:section(SimpleSection) +speed.title = translate("Bandwidth limits") +speed.render = function(self, ...) + luci.template.render("rtorrent/tabmenu", { self = { + pages = common.get_admin_pages(), + page = "rTorrent" + }}) + SimpleSection.render(self, ...) +end + +upload_rate = speed:option(Value, "upload_rate", translate("Upload limit (KiB/sec)"), + translate("Global upload rate (0: unlimited)")) +upload_rate.rmempty = false +upload_rate.default = config["throttle.global_up.max_rate"] / 1024 +upload_rate.datatype = "uinteger" +function upload_rate.write(self, section, value) + set_config("throttle.global_up.max_rate", value .. "k") +end + +download_rate = speed:option(Value, "download_rate", translate("Download limit (KiB/sec)"), + translate("Global downlaod rate (0: unlimited)")) +download_rate.rmempty = false +download_rate.default = config["throttle.global_down.max_rate"] / 1024 +download_rate.datatype = "uinteger" +function download_rate.write(self, section, value) + set_config("throttle.global_down.max_rate", value .. "k") +end + + +global_limits = f:section(SimpleSection) +global_limits.title = translate("Global limits") + +max_downloads_global = global_limits:option(Value, "max_downloads_global", translate("Download slots"), + translate("Maximum number of simultaneous downloads")) +max_downloads_global.rmempty = false +max_downloads_global.default = config["throttle.max_downloads.global"] +max_downloads_global.datatype = "uinteger" +function max_downloads_global.write(self, section, value) + set_config("throttle.max_downloads.global", value) +end + +max_uploads_global = global_limits:option(Value, "max_uploads_global", translate("Upload slots"), + translate("Maximum number of simultaneous uploads")) +max_uploads_global.rmempty = false +max_uploads_global.default = config["throttle.max_uploads.global"] +max_uploads_global.datatype = "uinteger" +function max_uploads_global.write(self, section, value) + set_config("throttle.max_uploads.global", value) +end + + +torrent_limits = f:section(SimpleSection) +torrent_limits.title = translate("Torrent limits") + +max_uploads = torrent_limits:option(Value, "max_uploads", translate("Maximum uploads"), + translate("Maximum number of simultanious uploads per torrent")) +max_uploads.rmempty = false +max_uploads.default = config["throttle.max_uploads"] +max_uploads.datatype = "uinteger" +function max_uploads.write(self, section, value) + set_config("throttle.max_uploads", value) +end + +min_peers = torrent_limits:option(Value, "min_peers", translate("Minimum peers"), + translate("Minimum number of peers to connect to per torrent")) +min_peers.rmempty = false +min_peers.default = config["throttle.min_peers.normal"] +min_peers.datatype = "uinteger" +function min_peers.write(self, section, value) + set_config("throttle.min_peers.normal", value) +end + +max_peers = torrent_limits:option(Value, "max_peers", translate("Maximum peers"), + translate("Maximum number of peers to connect to per torrent")) +max_peers.rmempty = false +max_peers.default = config["throttle.max_peers.normal"] +max_peers.datatype = "uinteger" +function max_peers.write(self, section, value) + set_config("throttle.max_peers.normal", value) +end + +min_peers_seed = torrent_limits:option(Value, "min_peers_seed", translate("Minimum seeds"), + translate("Minimum number of seeds for completed torrents (-1 = same as peers)")) +min_peers_seed.rmempty = false +min_peers_seed.default = config["throttle.min_peers.seed"] +min_peers_seed.datatype = "integer" +function min_peers_seed.write(self, section, value) + set_config("throttle.min_peers.seed", value) +end + +max_peers_seed = torrent_limits:option(Value, "max_peers_seed", translate("Maximum seeds"), + translate("Maximum number of seeds for completed torrents (-1 = same as peers)")) +max_peers_seed.rmempty = false +max_peers_seed.default = config["throttle.max_peers.seed"] +max_peers_seed.datatype = "integer" +function max_peers_seed.write(self, section, value) + set_config("throttle.max_peers.seed", value) +end + +-- dir = f:field(DummyValue, "dummy", luci.dispatcher.context.authuser) + +return f + diff --git a/luasrc/model/cbi/rtorrent/common.lua b/luasrc/model/cbi/rtorrent/common.lua new file mode 100644 index 0000000..db48a06 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/common.lua @@ -0,0 +1,116 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local http = require "socket.http" +local https = require "ssl.https" +local ssl = require "ssl" +local ltn12 = require "ltn12" +local fs = require "nixio.fs" +local dispatcher = require "luci.dispatcher" +require "luci.model.cbi.rtorrent.string" + +local string, os, io, math, assert = string, os, io, math, assert +local ipairs, table, unpack, tonumber = ipairs, table, unpack, tonumber + +local COOKIES_FILE = "/etc/cookies.txt" + +module "luci.model.cbi.rtorrent.common" + +function get_torrent_pages(hash) + return { + { name = "Info", link = dispatcher.build_url("admin/rtorrent/info/") .. hash }, + { name = "File List", link = dispatcher.build_url("admin/rtorrent/files/") .. hash }, + { name = "Tracker List", link = dispatcher.build_url("admin/rtorrent/trackers/") .. hash }, + { name = "Peer List", link = dispatcher.build_url("admin/rtorrent/peers/") .. hash } + } +end + +function get_admin_pages() + return { + { name = "rTorrent", link = dispatcher.build_url("admin/rtorrent/admin/rtorrent") }, + { name = "RSS", link = dispatcher.build_url("admin/rtorrent/admin/rss") } + } +end + +function human_size(bytes) + local symbol = {[0]="B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} + local exp = bytes > 0 and math.floor(math.log(bytes) / math.log(1024)) or 0 + local value = bytes / math.pow(1024, exp) + local acc = bytes > 0 and 2 - math.floor(math.log10(value)) or 2 + if acc < 0 then acc = 0 end + return string.format("%." .. acc .. "f " .. symbol[exp], value) +end + +function human_time(sec) + local t = os.date("*t", sec) + if t["day"] > 25 then return "∞" + elseif t["day"] > 1 then return string.format("%dd
%dh %dm", t["day"] - 1, t["hour"], t["min"]) + elseif t["hour"] > 1 then return string.format("%dh
%dm %ds", t["hour"] - 1, t["min"], t["sec"]) + elseif t["min"] > 0 then return string.format("%dm %ds", t["min"], t["sec"]) + else return string.format("%ds", t["sec"]) end +end + +function div(body, ...) + local class = {} + for _, c in ipairs({...}) do + if c then table.insert(class, c) end + end + if #class > 0 then + return "
%s
" % {table.concat(class, " "), body} + else + return body + end +end + +function wget(url) + local file = assert(io.popen("/usr/bin/wget " + .. "--quiet " + .. "--user-agent=unknown " + .. "--referer=http://www.google.com " + .. "--load-cookies=" .. COOKIES_FILE .. " " + .. "--output-document=- " + .. url, "r")) + local response = file:read("*all") + file:close() + return response +end + +function get(url) + local response = {} + local proto = url:starts("https://") and https or http + proto.TIMEOUT = 5 + local body, code, headers, status = proto.request { + method = "GET", + headers = { + ["Referer"] = "http://www.google.com", + ["User-Agent"] = "unknown", + ["Cookie"] = get_cookies(url) + }, + url = url, + redirect = (proto.PORT == 80) and true or nil, + sink = ltn12.sink.table(response) + } + if code == 301 then return get(headers["location"]) end + if code == 200 then return true, table.concat(response) + else + local body = wget(url) + if body:len() > 0 then return true, body + else return false, status end + end +end + +function get_cookies(url) + local cookies = {} + for _, line in ipairs(fs.readfile(COOKIES_FILE):split("\n\r")) do + if not line:match("^\s*#.*") then + local domain, tailmatch, path, secure, expiration, name, value = unpack(line:split()) + local url_match = (secure == "TRUE") and "^https://" or "^https?://" + url_match = url_match .. (tailmatch == "TRUE" and ".*" or "") .. domain .. path + if url:match(url_match) and tonumber(expiration) >= os.time() then + table.insert(cookies, name .. "=" .. value) + end + end + end + return table.concat(cookies, "; ") +end + diff --git a/luasrc/model/cbi/rtorrent/download.lua b/luasrc/model/cbi/rtorrent/download.lua new file mode 100644 index 0000000..aa01e1b --- /dev/null +++ b/luasrc/model/cbi/rtorrent/download.lua @@ -0,0 +1,66 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local nixio = require "nixio" +local http = require "luci.http" +local ltn12 = require "luci.ltn12" +local sys = require "luci.sys" +local common = require "luci.model.cbi.rtorrent.common" +require "luci.model.cbi.rtorrent.string" + +local ipairs, string = ipairs, string + +local PROTECTED_PATH = {"/bin", "/dev", "/etc", "/lib", "/overlay", "/root", "/sbin", "/tmp", "/usr", "/var", "/www"} + +module("luci.model.cbi.rtorrent.download", package.seeall) + +function security_check(file) + for _, path_prefix in ipairs(PROTECTED_PATH) do + if file:starts(path_prefix) then + http.write("

Access Denied

") + return false + end + end + return true +end + +function download_file(file) + file = nixio.fs.realpath(file) + if security_check(file) then + local f = nixio.open(file, "r") + http.header('Content-Disposition', 'attachment; filename="%s"' % nixio.fs.basename(file)) + http.header('Content-Length', nixio.fs.stat(file, "size")) + http.prepare_content("application/octet-stream") + repeat + local buf = f:read(2^13) -- 8k + http.write(buf) + until (buf == "") + f:close() + end +end + +function download_all(path) + path = nixio.fs.realpath(path) + if security_check(path) then + if string.find(string.lower(http.getenv("HTTP_USER_AGENT")), "linux") then + download_all_as_tar(path) + else + download_all_as_zip(path) + end + end +end + +function download_all_as_zip(path) + local reader = sys.ltn12_popen("zip -0 -j -r - \"%s\"" % path) + http.header('Content-Disposition', 'attachment; filename="%s.zip"' % nixio.fs.basename(path)) + http.prepare_content("application/zip") + ltn12.pump.all(reader, http.write) +end + +function download_all_as_tar(path) + local reader = sys.ltn12_popen("tar -cf - -C \"%s\" ." % path) + http.header('Content-Disposition', 'attachment; filename="%s.tar"' % nixio.fs.basename(path)) + http.prepare_content("application/x-tar") + ltn12.pump.all(reader, http.write) +end + diff --git a/luasrc/model/cbi/rtorrent/main.lua b/luasrc/model/cbi/rtorrent/main.lua new file mode 100644 index 0000000..9dc9f61 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/main.lua @@ -0,0 +1,288 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +-- Custom fields: +-- d.custom1: tags (space delimited) +-- d.custom2: tracker favicon +-- d.custom3: url of torrent file +-- d.custom4: not used +-- d.custom5: when "1": delete files from disk on erase + +local common = require "luci.model.cbi.rtorrent.common" +local rtorrent = require "rtorrent" +require "luci.model.cbi.rtorrent.string" + +local selected, format, total = {}, {}, {} + +local methods = { "hash", "name", "size_bytes", "bytes_done", "hashing", "state", "complete", + "peers_accounted", "peers_complete", "down.rate", "up.rate", "ratio", "up.total", + "timestamp.started", "timestamp.finished", "custom1", "custom2" } + +function status(d) + if d["hashing"] > 0 then return "hash" + elseif d["state"] == 0 then return "stop" + elseif d["state"] > 0 then + if d["complete"] == 0 then return "down" + else return "seed" end + else return "unknown" end +end + +function eta(d) + if d["bytes_done"] < d["size_bytes"] then + if d["down_rate"] > 0 then + return common.human_time((d["size_bytes"] - d["bytes_done"]) / d["down_rate"]) + else return "∞" end + else return "--" end +end + +function favicon(d) + if not d["custom2"] or d["custom2"] == "" then + d["custom2"] = "/luci-static/resources/icons/unknown_tracker.png" + for _, t in pairs(rtorrent.multicall("t.", d["hash"], 0, "url", "is_enabled")) do + if t["is_enabled"] then + local domain = t["url"]:match("[%w%.:/]*[%./](%w+%.%w+)") + if domain then + local icon = "http://" .. domain .. "/favicon.ico" + if common.get(icon) then + d["custom2"] = icon + break + end + end + end + end + rtorrent.call("d.custom2.set", d["hash"], d["custom2"]) + end + return d["custom2"] +end + +function has_tag(tags, tag) + for _, t in ipairs(tags) do + if t.name:lower() == tag:lower() then return true end + end + return false +end + +function get_tags(details) + local l = {} + local has_incomplete = false + for _, d in ipairs(details) do + for _, p in ipairs(d["custom1"]:split()) do + if not has_tag(l, p) then + table.insert(l, {name = p:ucfirst(), link = luci.dispatcher.build_url("admin/rtorrent/main/%s" % p)}) + end + end + if d["complete"] == 0 then + has_incomplete = true + end + end + if has_incomplete then + table.insert(l, {name = translate("Incomplete"), link = luci.dispatcher.build_url("admin/rtorrent/main/incomplete")}) + end + return l +end + +function filter(details, page) + local filtered = {} + for _, d in ipairs(details) do + if string.find(" " .. d["custom1"] .. " ", " " .. page .. " ") then + table.insert(filtered, d) + end + if page == "incomplete" and d["complete"] == 0 then + table.insert(filtered, d) + end + end + return filtered +end + +function format.icon(d, v) + return "" +end + +function format.name(d, v) + total["name"] = (total["name"] or 0) + 1 + local url = luci.dispatcher.build_url("admin/rtorrent/files/" .. d["hash"]) + return "%s" % {url, v} +end + +function format.size_bytes(d, v) + total["size_bytes"] = (total["size_bytes"] or 0) + v + return "
%s
" % {v, common.human_size(v)} +end + +function format.done_percent(d, v) + return string.format("%.1f%%", v) +end + +function format.status(d, v) + return common.div(v, v == "stop" and "red", v == "seed" and "blue", + v == "down" and "green", v == "hash" and "green") +end + +function format.down_rate(d, v) + total["down_rate"] = (total["down_rate"] or 0) + v + return string.format("%.2f", v / 1000) +end + +function format.up_rate(d, v) + total["up_rate"] = (total["up_rate"] or 0) + v + return string.format("%.2f", v / 1000) +end + +function format.ratio(d, v) + return common.div(string.format("%.2f", v / 1000), v < 1000 and "red" or "green") + -- "title: Total uploaded: " .. common.human_size(d["up_total"])) +end + +function format.eta(d, v) + local download_started = d["timestamp_started"] == 0 + and translate("not yet started") or os.date("!%Y-%m-%d %H:%M:%S", d["timestamp_started"]) + local download_finished = d["timestamp_finished"] == 0 + and translate("not yet finished") or os.date("!%Y-%m-%d %H:%M:%S", d["timestamp_finished"]) + return "
%s
" % + {download_started, download_finished, v } +end + +function add_custom(details) + for _, d in ipairs(details) do + -- refactor: swap favicon (custom1) and tags (custom2) + if d["custom1"]:ends(".ico") or d["custom1"]:ends(".png") then + local temp = d["custom1"] + d["custom1"] = d["custom2"] + d["custom2"] = temp + rtorrent.call("d.custom1.set", d["hash"], d["custom1"]) + rtorrent.call("d.custom2.set", d["hash"], d["custom2"]) + end + d["status"] = status(d) + d["done_percent"] = 100.0 * d["bytes_done"] / d["size_bytes"] + d["eta"] = eta(d) + d["icon"] = favicon(d) + d["custom1"] = (d["custom1"] == "") and "all" or d["custom1"] + end +end + +function add_summary(details) + table.insert(details, { + ["name"] = "TOTAL: " .. total["name"] .. " pcs.", + ["size_bytes"] = common.human_size(total["size_bytes"]), + ["down_rate"] = string.format("%.2f", total["down_rate"] / 1000), + ["up_rate"] = string.format("%.2f", total["up_rate"] / 1000), + ["select"] = "%hidden%" + }) +end + +function html_format(details) + table.sort(details, function(a, b) return a["name"] < b["name"] end) + for _, d in ipairs(details) do + for m, v in pairs(d) do + d[m] = format[m] and format[m](d, v) or tostring(v) + end + end +end + +f = SimpleForm("rtorrent", translate("Torrent List")) +f.reset = false +f.submit = false + +local details = rtorrent.multicall("d.", "default", unpack(methods)) +add_custom(details) +local tags = get_tags(details) +local user = luci.dispatcher.context.authuser +local page = arg[1] or (has_tag(tags, user) and user or "all") +local filtered = filter(details, page) + +html_format(filtered) +if #filtered > 1 then add_summary(filtered) end +t = f:section(Table, filtered) +t.template = "rtorrent/list" +t.pages = tags +t.page = page +t.headcol = 2 + +AbstractValue.tooltip = function(self, s) self.hint = s return self end + +t:option(DummyValue, "icon").rawhtml = true +t:option(DummyValue, "name", translate("Name")).rawhtml = true +t:option(DummyValue, "size_bytes", translate("Size")):tooltip("Full size of torrent").rawhtml = true +t:option(DummyValue, "done_percent", translate("Done")):tooltip("Download done percent").rawhtml = true +t:option(DummyValue, "status", translate("Status")).rawhtml = true +t:option(DummyValue, "peers_accounted", "↑"):tooltip("Seeder count").rawhtml = true +t:option(DummyValue, "peers_complete", "↓"):tooltip("Leecher count").rawhtml = true +t:option(DummyValue, "down_rate", translate("Down
Speed")):tooltip("Download speed in kb/s").rawhtml = true +t:option(DummyValue, "up_rate", translate("Up
Speed")):tooltip("Upload speed in kb/s").rawhtml = true +t:option(DummyValue, "ratio", translate("Ratio")):tooltip("Download/upload ratio").rawhtml = true +t:option(DummyValue, "eta", translate("ETA")):tooltip("Estimated Time of Arrival").rawhtml = true + +select = t:option(Flag, "select") +select.template = "rtorrent/fvalue" + +function select.write(self, section, value) + table.insert(selected, filtered[section].hash) +end + +s = f:section(SimpleSection) +s.template = "rtorrent/buttonsection" +s.style = "float: right;" + +start = s:option(Button, "start", translate("Start")) +start.template = "rtorrent/button" +start.inputstyle = "apply" + +function start.write(self, section, value) + if next(selected) ~= nil then + for _, hash in ipairs(selected) do + rtorrent.call("d.open", hash) + rtorrent.call("d.start", hash) + end + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) + end +end + +stop = s:option(Button, "stop", translate("Stop")) +stop.template = "rtorrent/button" +stop.inputstyle = "reset" + +function stop.write(self, section, value) + if next(selected) ~= nil then + for _, hash in ipairs(selected) do + rtorrent.call("d.stop", hash) + rtorrent.call("d.close", hash) + end + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) + end +end + +remove = s:option(Button, "remove", translate("Remove")) +remove.template = "rtorrent/button" +remove.inputstyle = "remove" + +function remove.write(self, section, value) + if next(selected) ~= nil then + for _, hash in ipairs(selected) do + rtorrent.call("d.close", hash) + rtorrent.call("d.erase", hash) + end + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) + end +end + +r = f:section(SimpleSection) +r.template = "rtorrent/buttonsection" +r.style = "float: right;" + +delete = r:option(Button, "delete", translate("Remove and delete data")) +delete.template = "rtorrent/button" +delete.inputstyle = "remove" + +function delete.write(self, section, value) + if next(selected) ~= nil then + for _, hash in ipairs(selected) do + rtorrent.call("d.custom5.set", hash, "1") + rtorrent.call("d.close", hash) + rtorrent.call("d.erase", hash) + end + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/main/" .. page)) + end +end + +return f + diff --git a/luasrc/model/cbi/rtorrent/rss-rule.lua b/luasrc/model/cbi/rtorrent/rss-rule.lua new file mode 100644 index 0000000..05ad850 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/rss-rule.lua @@ -0,0 +1,52 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" + +m = Map("rtorrent") +m.redirect = luci.dispatcher.build_url("admin/rtorrent/rss") + +local name = m:get(arg[1], "name") +m.title = "%s - %s" %{"RSS Downloader", name or "(Unnamed rule)"} + +s = m:section(NamedSection, arg[1], "rss-rule") +s.anonymous = true +s.addremove = false + +enabled = s:option(Flag, "enabled", "Enabled") +enabled.rmempty = false + +name = s:option(Value, "name", "Name") +name.rmempty = false + +match = s:option(TextValue, "match", "Match (lua regex)") +match.template = "rtorrent/tvalue" +match.rmempty = false +match.rows = 1 + +exclude = s:option(TextValue, "exclude", "Exclude (lua regex)") +exclude.rows = 1 + +s:option(Value, "minsize", "Min size (MiB):") +s:option(Value, "maxsize", "Max size (MiB):") + +feed = s:option(MultiValue, "feed", "Feed") +feed.delimiter = ";" +m.uci:foreach(m.config, "rss-feed", function(f) + feed:value(f.name, f.name) +end) + +tags = s:option(Value, "tags", "Add tags") +tags.default = "all" + +destdir = s:option(Value, "destdir", "Download directory") +destdir.default = rtorrent.call("directory.default") +destdir.datatype = "directory" +destdir.rmempty = false + +autostart = s:option(Flag, "autostart", "Start download") +autostart.default = "1" +autostart.rmempty = false + +return m + diff --git a/luasrc/model/cbi/rtorrent/rss.lua b/luasrc/model/cbi/rtorrent/rss.lua new file mode 100644 index 0000000..e43bc09 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/rss.lua @@ -0,0 +1,39 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +m = Map("rtorrent", "RSS Downloader") + +s = m:section(TypedSection, "rss-rule") +s.addremove = true +s.anonymous = true +s.sortable = true +s.template = "cbi/tblsection" +s.extedit = luci.dispatcher.build_url("admin/rtorrent/rss/%s") +s.template_addremove = "rtorrent/rss_addrule" + +function s.parse(self, ...) + TypedSection.parse(self, ...) + + local newrule_name = m:formvalue("_newrule.name") + local newrule_submit = m:formvalue("_newrule.submit") + + if newrule_submit then + newrule = TypedSection.create(self, section) + self.map:set(newrule, "name", newrule_name) + + m.uci:save("rtorrent") + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/rss", newrule)) + end +end + +name = s:option(DummyValue, "name", "Name") +name.width = "30%" + +match = s:option(DummyValue, "match", "Match") +match.width = "60%" + +enabled = s:option(Flag, "enabled", "Enabled") +enabled.rmempty = false + +return m + diff --git a/luasrc/model/cbi/rtorrent/string.lua b/luasrc/model/cbi/rtorrent/string.lua new file mode 100644 index 0000000..433278f --- /dev/null +++ b/luasrc/model/cbi/rtorrent/string.lua @@ -0,0 +1,30 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +function string.starts(str, begin) + if not str then return false end + return string.sub(str, 1, string.len(begin)) == begin +end + +function string.ends(str, tail) + if not str then return false end + return string.sub(str, -string.len(tail)) == tail +end + +function string.split(str, sep) + if sep == nil then sep = "%s" end + local t = {} + for s in str:gmatch("([^" .. sep .. "]+)") do + table.insert(t, s) + end + return t +end + +function string.ucfirst(str) + return (str:gsub("^%l", string.upper)) +end + +function string.trim(str) + return str:match("^()%s*$") and "" or str:match("^%s*(.*%S)") +end + diff --git a/luasrc/model/cbi/rtorrent/torrent/files.lua b/luasrc/model/cbi/rtorrent/torrent/files.lua new file mode 100644 index 0000000..19f9a7a --- /dev/null +++ b/luasrc/model/cbi/rtorrent/torrent/files.lua @@ -0,0 +1,138 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" +local nixio = require "nixio" +local common = require "luci.model.cbi.rtorrent.common" + +local hash = arg[1] +local details = rtorrent.batchcall({"name", "base_path"}, hash, "d.") +local files = rtorrent.multicall("f.", hash, 0, "path", "path_depth", "path_components", "size_bytes", + "size_chunks", "completed_chunks", "priority", "frozen_path") + +local format, total = {}, {} + +function format.icon(r, v) + local icon_path = "/luci-static/resources/icons/filetypes" + local ext = v:match("%.([^%.]+)$") + if ext and nixio.fs.stat("/www/%s/%s.png" % {icon_path, ext:lower()}, "type") then + return "%s/%s.png" % {icon_path, ext:lower()} + end + return "%s/%s.png" % {icon_path, "file"} +end + +function format.dir(r, v) + return " " .. v +end + +function format.file(r, v) + total["name"] = (total["name"] or 0) + 1 + local url = luci.dispatcher.build_url("admin/rtorrent/download/") .. nixio.bin.b64encode(r["frozen_path"]) + local link = r["chunks_percent"] == 100 and "" .. v .. "" or v + return " " .. link +end + +function format.size_bytes(r, v) + total["size_bytes"] = (total["size_bytes"] or 0) + v + return common.human_size(v) +end + +function format.chunks_percent(r, v) + return common.div(string.format("%.1f%%", v), v < 100 and "red") +end + +function format.priority(r, v) + return tostring(v) +end + +function add_custom(files) + for i, r in ipairs(files) do + r["id"] = i + r["chunks_percent"] = r["completed_chunks"] * 100.0 / r["size_chunks"] + end +end + +function add_summary(list) + table.insert(list, { + ["name"] = (translate("TOTAL").. ": ".. total["name"]), + ["size_bytes"] = common.human_size(total["size_bytes"]), + ["priority"] = "%hidden%" + }) +end + +function path_compare(a, b) + if a["path_depth"] ~= b["path_depth"] and (a["path_depth"] == 1 or b["path_depth"] == 1) then + return a["path_depth"] > b["path_depth"] + end + return a["path"] < b["path"] +end + +local list, last_path = {}, {} +add_custom(files) +table.sort(files, path_compare) +for _, r in ipairs(files) do + for i, p in ipairs(r["path_components"]) do + if last_path[i] ~= p then + local t = i == #r["path_components"] and "file" or "dir" + local n = {} + if t == "file" then + for m, v in pairs(r) do + n[m] = format[m] and format[m](r, v) or tostring(v) + end + else + n["priority"] = "%hidden%" + end + n["name"] = string.rep(" ", i - 1) .. format[t](r, p) + table.insert(list, n) + end + last_path[i] = p + end +end + +f = SimpleForm("rtorrent", details["name"]) +f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") +if nixio.fs.stat(details["base_path"], "type") == "dir" and table.getn(list) > 1 then + f.cancel = "Download all" +else + f.cancel = false +end + +if #list > 1 then add_summary(list) end +t = f:section(Table, list) +t.template = "rtorrent/list" +t.pages = common.get_torrent_pages(hash) +t.page = "File List" + +AbstractValue.tooltip = function(self, s) self.hint = s return self end + +t:option(DummyValue, "name", translate("Name")).rawhtml = true +t:option(DummyValue, "size_bytes", translate("Size")) +t:option(DummyValue, "chunks_percent", translate("Done")):tooltip("Download done percent").rawhtml = true +prio = t:option(ListValue, "priority", translate("Priority")):tooltip("Rotate priority") +prio.template = "rtorrent/lvalue" +prio.onclick = [[ + var inputs = document.getElementsByClassName("cbi-input-select"); + for (var i = 0; i < inputs.length; i++) { + if (inputs[i].selectedIndex < inputs[i].length - 1) { + inputs[i].selectedIndex++; + } else { + inputs[i].selectedIndex = 0; + } + } +]] +prio:value("0", "off") +prio:value("1", "normal") +prio:value("2", "high") + +function prio.write(self, section, value) + rtorrent.call("f.priority.set", hash .. ":f" .. (list[tonumber(section)].id - 1), tonumber(value)) + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/files/%s" % hash)) +end + +function f:on_cancel() + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/downloadall/") + .. nixio.bin.b64encode(details["base_path"])) +end + +return f + diff --git a/luasrc/model/cbi/rtorrent/torrent/info.lua b/luasrc/model/cbi/rtorrent/torrent/info.lua new file mode 100644 index 0000000..38093e7 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/torrent/info.lua @@ -0,0 +1,48 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" +local common = require "luci.model.cbi.rtorrent.common" + +local hash = arg[1] +local details = rtorrent.batchcall({"name", "custom1", "timestamp.started", "timestamp.finished"}, hash, "d.") + +f = SimpleForm("rtorrent", details["name"]) +f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") + +t = f:section(Table, list) +t.template = "rtorrent/list" +t.pages = common.get_torrent_pages(hash) +t.page = "Info" + +hash_id = f:field(DummyValue, "hash", translate("Hash")) +function hash_id.cfgvalue(self, section) + return hash +end + +started = f:field(DummyValue, "started", translate("Download started")) +started.value = details["timestamp.started"] == 0 + and "not yet started" + or os.date("!%Y-%m-%d %H:%M:%S", details["timestamp.started"]) + +finished = f:field(DummyValue, "finished", translate("Download finished")) +finished.value = details["timestamp.finished"] == 0 + and "not yet finished" + or os.date("!%Y-%m-%d %H:%M:%S", details["timestamp.finished"]) + +tags = f:field(Value, "tags", translate("Tags")) +tags.default = details["custom1"] +tags.rmempty = false + +function tags.write(self, section, value) + rtorrent.call("d.custom1.set", hash, value) +end + +function f.handle(self, state, data) + if state == FORM_VALID then + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/info/") .. hash) + end + return true +end + +return f diff --git a/luasrc/model/cbi/rtorrent/torrent/peers.lua b/luasrc/model/cbi/rtorrent/torrent/peers.lua new file mode 100644 index 0000000..947392a --- /dev/null +++ b/luasrc/model/cbi/rtorrent/torrent/peers.lua @@ -0,0 +1,119 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" +local http = require "socket.http" +local common = require "luci.model.cbi.rtorrent.common" + +local hash = arg[1] +local details = rtorrent.batchcall({"name"}, hash, "d.") +local format, total, map = {}, {}, {} + +local geoplugin_net = { + address = "http://www.geoplugin.net/json.gp?ip=%s", + fields = { geoplugin_countryCode = "country_code", geoplugin_countryName = "country", geoplugin_region = "region", + geoplugin_city = "city", geoplugin_latitude = "latitude", geoplugin_longitude = "longitude" +}} + +local ip2geo = geoplugin_net + +function map.googlemap(latitude, longitude, zoom) + return "https://google.com/maps/place/%s,%s/@%s,%s,%sz" % {latitude, longitude, latitude, longitude, zoom} +end + +function map.openstreetmap(latitude, longitude, zoom) + return "http://www.openstreetmap.org/?mlat=%s&mlon=%s#map=%s/%s/%s/m" % {latitude, longitude, zoom, latitude, longitude} +end + +function format.address(r, v) + total["address"] = (total["address"] or 0) + 1 + local map = map.googlemap(r.latitude, r.longitude, 11) + -- local map = map.openstreetmap(r.latitude, r.longitude, 11) + -- local flag = "" % r.country_code:lower() + -- local flag = "" % r.country_code:lower() + local flag = "" % r.country_code:lower() + return "%s %s" % {flag, map, v} +end + +function format.completed_percent(r, v) + return string.format("%.1f%%", v) +end + +function format.down_rate(d, v) + total["down_rate"] = (total["down_rate"] or 0) + v + return string.format("%.2f", v / 1000) +end + +function format.up_rate(d, v) + total["up_rate"] = (total["up_rate"] or 0) + v + return string.format("%.2f", v / 1000) +end + +function format.down_total(d, v) + return "
%s
" % {v, common.human_size(v)} +end + +function format.up_total(d, v) + return format.down_total(d, v) +end + +function json2table(json) + loadstring("j2t = " .. string.gsub(string.gsub(json, '([,%{])%s*\n?%s*"', '%1["'), '"%s*:%s*', '"]='))() + return j2t +end + +function add_location(r) + for i, j in pairs(json2table(http.request(ip2geo.address % r.address))) do + if ip2geo.fields[i] then r[ip2geo.fields[i]] = j end + end + local location = {} + for _, k in ipairs({"country", "region", "city"}) do + if r[k] ~= "" and not tonumber(r[k]) then + table.insert(location, (r[k]:gsub("\u(%x%x%x%x)", "&#x%1"))) + end + end + r["location"] = table.concat(location, "/") +end + +function add_summary(list) + table.insert(list, { + ["address"] = (translate("TOTAL").. ": " .. total["address"]), + ["down_rate"] = string.format("%.2f", total["down_rate"] / 1000), + ["up_rate"] = string.format("%.2f", total["up_rate"] / 1000) + }) +end + +local list = rtorrent.multicall("p.", hash, 0, "address", "completed_percent", "client_version", + "down_rate", "up_rate", "up_total", "down_total") + +for _, r in ipairs(list) do + add_location(r) + for k, v in pairs(r) do + r[k] = format[k] and format[k](r, v) or tostring(v) + end +end + +f = SimpleForm("rtorrent", details["name"]) +f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") +f.reset = false +f.submit = false + +if #list > 1 then add_summary(list) end +t = f:section(Table, list) +t.template = "rtorrent/list" +t.pages = common.get_torrent_pages(hash) +t.page = "Peer List" + +AbstractValue.tooltip = function(self, s) self.hint = s return self end + +t:option(DummyValue, "address", translate("Address")):tooltip("Peer IP address").rawhtml = true +t:option(DummyValue, "client_version", translate("Client")):tooltip("Client version") +t:option(DummyValue, "location", translate("Location")):tooltip("Location: country/region/city").rawhtml = true +t:option(DummyValue, "completed_percent", translate("Done")):tooltip("Download done percent") +t:option(DummyValue, "down_rate", translate("Down
Speed")):tooltip("Download speed in kb/s") +t:option(DummyValue, "up_rate", translate("Up
Speed")):tooltip("Upload speed in kb/s") +t:option(DummyValue, "down_total", translate("Downloaded")):tooltip("Total downloaded").rawhtml = true +t:option(DummyValue, "up_total", translate("Uploaded")):tooltip("Total uploaded").rawhtml = true + +return f + diff --git a/luasrc/model/cbi/rtorrent/torrent/trackers.lua b/luasrc/model/cbi/rtorrent/torrent/trackers.lua new file mode 100644 index 0000000..783be63 --- /dev/null +++ b/luasrc/model/cbi/rtorrent/torrent/trackers.lua @@ -0,0 +1,89 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local rtorrent = require "rtorrent" +local common = require "luci.model.cbi.rtorrent.common" + +local hash = arg[1] +local details = rtorrent.batchcall({"name"}, hash, "d.") + +local format, total = {}, {} + +function format.url(r, v) + total["url"] = (total["url"] or 0) + 1 +end + +function format.scrape_downloaded(r, v) + total["scrape_downloaded"] = (total["scrape_downloaded"] or 0) + v + return tostring(v) +end + +function format.scrape_complete(r, v) + total["scrape_complete"] = (total["scrape_complete"] or 0) + v + return tostring(v) +end + +function format.scrape_incomplete(r, v) + total["scrape_incomplete"] = (total["scrape_incomplete"] or 0) + v + return tostring(v) +end + +function format.scrape_time_last(r, v) + return common.human_time(os.time() - v) +end + +function add_summary(list) + table.insert(list, { + ["url"] = (translate("TOTAL").. ": " .. total["url"]), + ["scrape_downloaded"] = tostring(total["scrape_downloaded"]), + ["scrape_complete"] = tostring(total["scrape_complete"]), + ["scrape_incomplete"] = tostring(total["scrape_incomplete"]), + ["is_enabled"] = "%hidden%" + }) +end + +local list = rtorrent.multicall("t.", hash, 0, "is_enabled", "url", "scrape_downloaded", "scrape_complete", + "scrape_incomplete", "scrape_time_last") + +for _, r in ipairs(list) do + for k, v in pairs(r) do + r[k] = format[k] and format[k](r, v) or tostring(v) + end +end + +f = SimpleForm("rtorrent", details["name"]) +f.redirect = luci.dispatcher.build_url("admin/rtorrent/main") + +if #list > 1 then add_summary(list) end +t = f:section(Table, list) +t.template = "rtorrent/list" +t.pages = common.get_torrent_pages(hash) +t.page = "Tracker List" + +AbstractValue.tooltip = function(self, s) self.hint = s return self end + +t:option(DummyValue, "url", translate("Url")) +t:option(DummyValue, "scrape_downloaded", translate("D")):tooltip("Downloaded") +t:option(DummyValue, "scrape_complete", translate("S")):tooltip("Seeders") +t:option(DummyValue, "scrape_incomplete", translate("L")):tooltip("Leechers") +t:option(DummyValue, "scrape_time_last", translate("Updated")):tooltip("Last update time").rawhtml = true + +enabled = t:option(Flag, "is_enabled", translate("Enabled")) +enabled.template = "rtorrent/fvalue" +enabled.rmempty = false +enabled.rawhtml = true + +function enabled.write(self, section, value) + if value ~= tostring(list[section].is_enabled) then + rtorrent.call("t.is_enabled.set", hash .. ":t" .. (section - 1), tonumber(value)) + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/trackers/%s" % hash)) + end +end + +add = f:field(Value, "add_tracker", translate("Add tracker")) +function add.write(self, section, value) + rtorrent.call("d.tracker.insert", hash, table.getn(list), value) + luci.http.redirect(luci.dispatcher.build_url("admin/rtorrent/trackers/%s" % hash)) +end + +return f diff --git a/luasrc/view/rtorrent/button.htm b/luasrc/view/rtorrent/button.htm new file mode 100644 index 0000000..91a6ede --- /dev/null +++ b/luasrc/view/rtorrent/button.htm @@ -0,0 +1,3 @@ +<%+cbi/valueheader%> +" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. ifattr(self.style, "style") .. attr("value", self:cfgvalue(section) or self.inputtitle or self.title)%> /> +<%+cbi/valuefooter%> diff --git a/luasrc/view/rtorrent/buttonsection.htm b/luasrc/view/rtorrent/buttonsection.htm new file mode 100644 index 0000000..6574cff --- /dev/null +++ b/luasrc/view/rtorrent/buttonsection.htm @@ -0,0 +1,5 @@ +
+
> + <% self:render_children(1, { valueheader = "rtorrent/empty", valuefooter = "rtorrent/empty" }) %> +
+
diff --git a/luasrc/view/rtorrent/empty.htm b/luasrc/view/rtorrent/empty.htm new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/luasrc/view/rtorrent/empty.htm @@ -0,0 +1 @@ + diff --git a/luasrc/view/rtorrent/fvalue.htm b/luasrc/view/rtorrent/fvalue.htm new file mode 100644 index 0000000..ea81410 --- /dev/null +++ b/luasrc/view/rtorrent/fvalue.htm @@ -0,0 +1,11 @@ +<%+cbi/valueheader%> + <% if not self:cfgvalue(section) or self:cfgvalue(section) ~= "%hidden%" then -%> + /> + /> + <%- end %> +<%+cbi/valuefooter%> diff --git a/luasrc/view/rtorrent/list.htm b/luasrc/view/rtorrent/list.htm new file mode 100644 index 0000000..a74d27a --- /dev/null +++ b/luasrc/view/rtorrent/list.htm @@ -0,0 +1,51 @@ +<%- +local rowcnt = 1 +function rowstyle() + rowcnt = rowcnt + 1 + return (rowcnt % 2) + 1 +end +-%> + + + +<%+rtorrent/tabmenu%> +
+
<%=self.description%>
+
+
+
+ <%- for i, k in pairs(self.children) do -%> +
<%=ifattr(k.onclick, "onclick", k.onclick)%>><%=k.title%>
+ <%- end -%> +
+ + <%- for j, k in ipairs(self:cfgsections()) do + section = k + scope = { valueheader = "rtorrent/empty", valuefooter = "rtorrent/empty" } + -%> +
+ <%- for i, node in ipairs(self.children) do -%> +
+
"> + <%- node:render(section, scope or {}) -%> +
+
">
+
+ <%- end -%> +
+ <%- end -%> +
+
+
diff --git a/luasrc/view/rtorrent/lvalue.htm b/luasrc/view/rtorrent/lvalue.htm new file mode 100644 index 0000000..3555e74 --- /dev/null +++ b/luasrc/view/rtorrent/lvalue.htm @@ -0,0 +1,20 @@ +<%+cbi/valueheader%> +<% if not self:cfgvalue(section) or self:cfgvalue(section) ~= "%hidden%" then -%> +<% if self.widget == "select" then %> + +<% elseif self.widget == "radio" then + local c = 0 + for i, key in pairs(self.keylist) do + c = c + 1 +%> + /> + ><%=self.vallist[i]%> +<% if c == self.size then c = 0 %><% if self.orientation == "horizontal" then %> <% else %>
<% end %> +<% end end %> +<% end %> +<%- end %> +<%+cbi/valuefooter%> diff --git a/luasrc/view/rtorrent/rss_addrule.htm b/luasrc/view/rtorrent/rss_addrule.htm new file mode 100644 index 0000000..c6ca882 --- /dev/null +++ b/luasrc/view/rtorrent/rss_addrule.htm @@ -0,0 +1,10 @@ + + + + + +
+ + + +
diff --git a/luasrc/view/rtorrent/tabmenu.htm b/luasrc/view/rtorrent/tabmenu.htm new file mode 100644 index 0000000..3c11434 --- /dev/null +++ b/luasrc/view/rtorrent/tabmenu.htm @@ -0,0 +1,9 @@ +
    + <%- for i, k in ipairs(self.pages) do -%> + <%- if k.name:lower() ~= "protected" then if self.page:lower() == k.name:lower() then -%> +
  • <%=k.name%>
  • + <%- else -%> +
  • <%=k.name%>
  • + <%- end end -%> + <%- end -%> +
diff --git a/luasrc/view/rtorrent/tvalue.htm b/luasrc/view/rtorrent/tvalue.htm new file mode 100644 index 0000000..964e0a6 --- /dev/null +++ b/luasrc/view/rtorrent/tvalue.htm @@ -0,0 +1,7 @@ + +<%+cbi/tvalue%> diff --git a/root/etc/config/rtorrent b/root/etc/config/rtorrent new file mode 100644 index 0000000..e69de29 diff --git a/root/etc/cookies.txt b/root/etc/cookies.txt new file mode 100644 index 0000000..fb1672b --- /dev/null +++ b/root/etc/cookies.txt @@ -0,0 +1,14 @@ +# Netscape Cookie File + +# From left-to-right, the cookie data consists of the following fields: +# domain - The domain that created and that can read the variable. +# tailmatch - A TRUE/FALSE value indicating if all machines within a given domain can access the variable. +# path - The path within the domain that the variable is valid for. Use / for any url. +# secure - A TRUE/FALSE value indicating if a secure connection with the domain is needed to access the variable. +# expiration - The UNIX time that the variable will expire on, max: 2147483647 (2038-01-19). +# name - The name of the variable. +# value - The value of the variable. + +# domain tailmatch path secure expiration name value +.netscape.com TRUE / FALSE 2147483647 dummy 1234 + diff --git a/root/etc/init.d/rtorrent b/root/etc/init.d/rtorrent new file mode 100755 index 0000000..4f791ee --- /dev/null +++ b/root/etc/init.d/rtorrent @@ -0,0 +1,17 @@ +#!/bin/sh /etc/rc.common + +START=99 +STOP=99 + +start() { + screen -dmS rtorrent nice -19 rtorrent -n -o import=/etc/rtorrent.conf +} + +boot() { + start +} + +stop() { + killall rtorrent +} + diff --git a/root/etc/rtorrent.conf b/root/etc/rtorrent.conf new file mode 100644 index 0000000..e59cd1f --- /dev/null +++ b/root/etc/rtorrent.conf @@ -0,0 +1,89 @@ +# This is an example resource file for rTorrent. +# Enable/modify the options as needed. Remember to uncomment +# the options you wish to enable. + +# Default directory to save the downloaded torrents +#directory = /path/to/downloads/ + +# Default session directory +#session = /path/to/session/ + +# rTorrent xml-rpc scgi port +# Use the same port in luci-app-rtorrent configuration! +scgi_port = 127.0.0.1:5000 + +# Port range to use for listening +port_range = 21244-21244 +port_random = no + +# Enable DHT support for trackerless torrents or when all trackers are down. +# May be set to "disable" (completely disable DHT), "off" (do not start DHT), +# "auto" (start and stop DHT as needed), or "on" (start DHT immediately). +# The default is "off". For DHT to work, a session directory must be defined. +dht = auto +dht_port = 6881 + +# Enable tracker requests +trackers.enable = 1 + +# Set whether the client should try to connect to UDP trackers +trackers.use_udp.set = true + +# Enable peer exchange (for torrents not marked private) +protocol.pex.set = yes + +# Global upload and download rate in KiB. "0" for unlimited +#throttle.global_down.max_rate.set_kb = 0 +#throttle.global_up.max_rate.set_kb = 0 + +# Maximum number of simultaneous downloads/uploads +#throttle.max_downloads.global.set = 0 +#throttle.max_uploads.global.set = 0 + +# Maximum number of simultanious uploads per torrent +#throttle.max_uploads.set = 20 + +# Maximum and minimum number of peers to connect to per torrent +#throttle.min_peers.normal.set = 40 +#throttle.max_peers.normal.set = 100 + +# Same as above but for seeding completed torrents (-1 = same as downloading) +#throttle.min_peers.seed.set = 10 +#throttle.max_peers.seed.set = 50 + +# Check hash for finished torrents +check_hash = no + +# Encryption options, set to none (default) or any combination of the following: +# allow_incoming, try_outgoing, require, require_RC4, enable_retry, prefer_plaintext +encryption = allow_incoming,try_outgoing,enable_retry + +# Set the umask applied to all files created by rTorrent +system.umask.set = 022 + +# Support non-ascii characters in the filenames +encoding_list = UTF-8 + +# Maximum number of socket connections rtorrent can accept/make +#network.max_open_sockets.set = 300 + +# Maximum number of open files rtorrent can keep open +#network.max_open_files.set = 600 + +# Maximum number of simultaneous HTTP request +#network.http.max_open.set = 50 + +# CURL option to lower DNS timeout +#network.http.dns_cache_timeout.set = 25 + +# Max packet size using xmlrpc +#network.xmlrpc.size_limit.set = 2M + +# rTorrent scheduler/events +schedule = rss_downloader,300,300,"execute2=/usr/lib/lua/rss_downloader.lua" +method.set_key = event.download.erased,on_erase,"branch=d.custom5=,\"execute2={rm,-rf,--,$d.base_path=}\"" + +# Logging +#log.execute = /path/to/log/rtorrent.execute.log +#log.xmlrpc = /path/to/log/rtorrent.xmlrpc.log + diff --git a/root/etc/uci-defaults/rtorrent b/root/etc/uci-defaults/rtorrent new file mode 100644 index 0000000..d4eb74a --- /dev/null +++ b/root/etc/uci-defaults/rtorrent @@ -0,0 +1,6 @@ +#!/bin/sh +/etc/init.d/rtorrent enable +/etc/init.d/rtorrent start +rm -fr /tmp/luci-indexcache /tmp/luci-modulecache +exit 0 + diff --git a/root/usr/lib/lua/bencode.lua b/root/usr/lib/lua/bencode.lua new file mode 100644 index 0000000..4fb52eb --- /dev/null +++ b/root/usr/lib/lua/bencode.lua @@ -0,0 +1,180 @@ +-- Copyright (c) 2009, 2010, 2011, 2012 by Moritz Wilhelmy +-- Copyright (c) 2009 by Kristofer Karlsson +-- Public domain lua-module for handling bittorrent-bencoded data. +-- This module includes both a recursive decoder and a recursive encoder. + +local sort, concat, insert = table.sort, table.concat, table.insert +local pairs, ipairs, type, tonumber = pairs, ipairs, type, tonumber +local sub, find = string.sub, string.find + +local M = {} + +-- helpers + +local function islist(t) + local n = #t + for k, v in pairs(t) do + if type(k) ~= "number" + or k % 1 ~= 0 -- integer? + or k < 1 + or k > n + then + return false + end + end + for i = 1, n do + if t[i] == nil then + return false + end + end + return true +end + +-- encoder functions + +local encode_rec -- encode_list/dict and encode_rec are mutually recursive... + +local function encode_list(t, x) + + insert(t, "l") + + for _,v in ipairs(x) do + local err,ev = encode_rec(t, v); if err then return err,ev end + end + + insert(t, "e") +end + +local function encode_dict(t, x) + insert(t, "d") + -- bittorrent requires the keys to be sorted. + local sortedkeys = {} + for k, v in pairs(x) do + if type(k) ~= "string" then + return "bencoding requires dictionary keys to be strings", k + end + insert(sortedkeys, k) + end + sort(sortedkeys) + + for k, v in ipairs(sortedkeys) do + local err,ev = encode_rec(t, v); if err then return err,ev end + err,ev = encode_rec(t, x[v]); if err then return err,ev end + end + insert(t, "e") +end + +local function encode_int(t, x) + + if x % 1 ~= 0 then return "number is not an integer", x end + insert(t, "i" ) + insert(t, x ) + insert(t, "e" ) +end + +local function encode_str(t, x) + + insert(t, #x ) + insert(t, ":" ) + insert(t, x ) +end + +encode_rec = function(t, x) + + local typx = type(x) + if typx == "string" then return encode_str (t, x) + elseif typx == "number" then return encode_int (t, x) + elseif typx == "table" then + + if islist(x) then return encode_list (t, x) + else return encode_dict (t, x) + end + else + return "type cannot be converted to an acceptable type for bencoding", typx + end +end + +-- call recursive bencoder function with empty table, stringify that table. +-- this is the only encode* function visible to module users. +M.encode = function (x) + + local t = {} + local err, val = encode_rec(t,x) + if not err then + return concat(t) + else + return nil, err, val + end +end + +-- decoder functions + +local function decode_integer(s, index) + local a, b, int = find(s, "^(%-?%d+)e", index) + if not int then return nil, "not a number", nil end + int = tonumber(int) + if not int then return nil, "not a number", int end + return int, b + 1 +end + +local function decode_list(s, index) + local t = {} + while sub(s, index, index) ~= "e" do + local obj, ev + obj, index, ev = M.decode(s, index) + if not obj then return obj, index, ev end + insert(t, obj) + end + index = index + 1 + return t, index +end + +local function decode_dictionary(s, index) + local t = {} + while sub(s, index, index) ~= "e" do + local obj1, obj2, ev + + obj1, index, ev = M.decode(s, index) + if not obj1 then return obj1, index, ev end + + obj2, index, ev = M.decode(s, index) + if not obj2 then return obj2, index, ev end + + t[obj1] = obj2 + end + index = index + 1 + return t, index +end + +local function decode_string(s, index) + local a, b, len = find(s, "^([0-9]+):", index) + if not len then return nil, "not a length", len end + index = b + 1 + + local v = sub(s, index, index + len - 1) + if #v < tonumber(len) then return nil, "truncated string at end of input", v end + index = index + len + return v, index +end + + +M.decode = function (s, index) + if not s then return nil, "no data", nil end + index = index or 1 + local t = sub(s, index, index) + if not t then return nil, "truncation error", nil end + + if t == "i" then + return decode_integer(s, index + 1) + elseif t == "l" then + return decode_list(s, index + 1) + elseif t == "d" then + return decode_dictionary(s, index + 1) + elseif t >= '0' and t <= '9' then + return decode_string(s, index) + else + return nil, "invalid type", t + end +end + +return M diff --git a/root/usr/lib/lua/rss_downloader.lua b/root/usr/lib/lua/rss_downloader.lua new file mode 100644 index 0000000..bf0b1e3 --- /dev/null +++ b/root/usr/lib/lua/rss_downloader.lua @@ -0,0 +1,161 @@ +#!/usr/bin/lua +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local uci = require "luci.model.uci".cursor() +local date = require "luci.http.protocol.date" +local bencode = require "bencode" +local xml = require "lxp.lom" +local xmlrpc = require "xmlrpc" +local rtorrent = require "rtorrent" +local nixio = require "nixio" +local common = require "luci.model.cbi.rtorrent.common" +require "luci.model.cbi.rtorrent.string" + +CONFIG = "rtorrent" + +function log(str) + print(os.date("%Y-%m-%d %H:%M:%S") .. " " .. str) +end + +function uci_get_all_sections(config, stype, filter) + local sections = {} + uci:foreach(config, stype, function(s) + if not filter or filter(s) then + table.insert(sections, s) + end + end) + return sections +end + +function get_feeds(filter) + return uci_get_all_sections(CONFIG, "rss-feed", filter) +end + +function get_rules(filter) + return uci_get_all_sections(CONFIG, "rss-rule", filter) +end + +function filter_enabled(s) + return s.enabled == "1" +end + +function next_tag(t, tag, i) + if not i then i = 1 end + if t ~= nil then + while t[i] do + if type(t[i]) == "table" and t[i].tag == tag then + return t[i], i + end + i = i + 1 + end + end + return nil, i +end + +function parse_feed(url) + local ok, content = common.get(url) + if ok and content ~= nil then + local rss, err = xml.parse(content) + if rss ~= nil then + return rss + else + log("Failed to parse rss feed: " .. err) + end + else + log("Failed to download rss feed: " .. url) + end +end + +function contains(tbl, value) + for _, v in ipairs(tbl) do + if v == value then + return true + end + end + return false +end + +function get_torrent_size(torrent) + local sha1_size = 20 + local t, err = bencode.decode(torrent) + if t then + local piece_length = tonumber(t["info"]["piece length"]) + local piece_count = t["info"]["pieces"]:len() / sha1_size + return piece_length * piece_count / 1024 / 1024 + else + log("Failed to parse torrent file: " .. tostring(torrent)) + return nil + end +end + +function get_torrent_link(item) + local enclosure = next_tag(item, "enclosure") + if enclosure == nil then + return next_tag(item, "link")[1] + end + return enclosure.attr.url +end + +--[[ M A I N ]]-- + +-- TODO: fix character encoding (eg.: lua expat does not support iso8859-2) +-- string.gsub(rss, "[^\128-\193]", "") + +local feed_logfile = uci:get(CONFIG, "logging", "feed_logfile") +if feed_logfile ~= nil then + feed_log = assert(io.open(feed_logfile, "a")) +end + +local rules = get_rules(filter_enabled) +for _, feed in ipairs(get_feeds(filter_enabled)) do + log("Processing \"" .. feed.name .. "\" rss feed") + local rss = parse_feed(feed.url) + local channel = next_tag(rss, "channel") + local item, i = next_tag(channel, "item") + local lastupdate = tonumber(feed.lastupdate) or 0 + if item then + uci:set(CONFIG, feed[".name"], "lastupdate", date.to_unix(next_tag(item, "pubDate")[1])) + uci:save(CONFIG) + uci:commit(CONFIG) + end + while item do + local pubdate = date.to_unix(next_tag(item, "pubDate")[1]) + if pubdate > lastupdate then + local title = next_tag(item, "title")[1] + if feed_log ~= nil then + feed_log:write(os.date("!%Y-%m-%d %H:%M:%S", pubdate) .. + " (" .. feed.name .. ") " .. title .. "\n") + end + for _, rule in ipairs(rules) do + if rule.feed and contains(rule.feed:split(";"), feed.name) + and title:lower():find(rule.match:lower()) + and (not rule.exclude or not title:lower():find(rule.exclude:lower())) then + local link = get_torrent_link(item) + local ok, torrent = common.get(link) + local size = get_torrent_size(torrent) + if ok and size + and (not rule.minsize or (size >= tonumber(rule.minsize))) + and (not rule.maxsize or (size <= tonumber(rule.maxsize))) then + log("Matched rss rule: " .. rule.name .. " (" .. link .. ")") + local params = {} + table.insert(params, rule.autostart == "1" and "load.raw_start" or "load.raw") + table.insert(params, "") -- target + table.insert(params, xmlrpc.newTypedValue((nixio.bin.b64encode(torrent)), "base64")) + table.insert(params, "d.directory.set=\"" .. rule.destdir .. "\"") + if rule.tags then table.insert(params, "d.custom1.set=\"" .. rule.tags .. "\"") end + table.insert(params, "d.custom3.set=" .. nixio.bin.b64encode(link)) + rtorrent.call(unpack(params)) + end + end + end + end + item, i = next_tag(channel, "item", i + 1) + end +end + +if feed_log ~= nil then + feed_log:flush() + feed_log:close() +end + diff --git a/root/usr/lib/lua/rtorrent.lua b/root/usr/lib/lua/rtorrent.lua new file mode 100644 index 0000000..be4c814 --- /dev/null +++ b/root/usr/lib/lua/rtorrent.lua @@ -0,0 +1,103 @@ +-- Copyright 2014-2018 Sandor Balazsi +-- Licensed to the public under the GNU General Public License. + +local ipairs, string, tostring, tonumber, table = ipairs, string, tostring, tonumber, table +local assert, type, unpack = assert, type, unpack + +local nixio = require "nixio" +local socket = require "socket" +local xmlrpc = require "xmlrpc" +local scgi = require "xmlrpc.scgi" + +local SCGI_ADDRESS = "localhost" +local SCGI_PORT = 5000 + +local HTTP_AUTH_USER = "rtorrent" +local HTTP_AUTH_PASSWORD = "czNz9JwdcLYcGDcVnZbQ" + +module "rtorrent" + +function map(array, func) + local new_array = {} + for i, v in ipairs(array) do + new_array[i] = func(v) + end + return new_array +end + +function alter(prefix, methods, postfix) + methods = map(methods, function(method) + if method == 0 then return method end + if prefix then method = prefix .. method end + if postfix then method = method .. postfix end + return method + end) + return methods +end + +function format(method_type, res, methods) + local formatted = {} + for _, r in ipairs(res) do + local item = {} + for i, v in ipairs(r) do + item[methods[method_type == "d." and i or i + 1]:gsub("%.", "_")] = v + end + table.insert(formatted, item) + end + return formatted +end + +function call(method, ...) + local ok, res = scgi.call(SCGI_ADDRESS, SCGI_PORT, method, ...) + if not ok and res == "socket connect failed" then + assert(ok, "\n\nFailed to connect to rtorrent: rpc port not reachable!\n" + .. "Possible reasons:\n" + .. "- not the rpc version of rtorrent is installed\n" + .. "- scgi port is not defined in .rtorrent.rc (scgi_port = 127.0.0.1:5000)\n" + .. "- rtorrent is not running (ps | grep rtorrent)\n") + end + assert(ok, string.format("XML-RPC call failed on client: %s", tostring(res))) + return res +end + +function rpc(xml) + local auth = "Basic " .. nixio.bin.b64encode(HTTP_AUTH_USER .. ":" .. HTTP_AUTH_PASSWORD) + if auth ~= nixio.getenv("HTTP_AUTHORIZATION") then + return 'Status: 401 Unauthorized\r\n' + .. 'WWW-Authenticate: Basic realm="rTorrent"\r\n\r\n' + end + local sock = socket.connect(SCGI_ADDRESS, SCGI_PORT) + if sock ~= nil then + sock:send(scgi.netstring(xml)) + local err, code, headers, body = scgi.receive(sock) + if tonumber(code) == 200 then + return 'Status: 200 OK\r\n' + .. 'Content-Type: application/xml\r\n\r\n' .. body + end + end + return 'Status: 500 Internal Server Error\r\n\r\n' +end + +function multicall(method_type, filter, ...) + local res = (method_type == "d.") + and call(method_type .. "multicall2", "", filter, unpack(alter(method_type, {...}, "="))) + or call(method_type .. "multicall", filter, unpack(alter(method_type, {...}, "="))) + return format(method_type, res, {...}) +end + +function batchcall(methods, params, prefix, postfix) + local p = type(params) == "table" and params or { params } + local methods_array = {} + for _, m in ipairs(alter(prefix, methods, postfix)) do + table.insert(methods_array, { + ["methodName"] = m, + ["params"] = xmlrpc.newTypedValue(p, "array") + }) + end + local res = {} + for i, r in ipairs(call("system.multicall", xmlrpc.newTypedValue(methods_array, "array"))) do + res[methods[i]] = r[1] + end + return res +end + diff --git a/root/usr/lib/lua/xmlrpc/init.lua b/root/usr/lib/lua/xmlrpc/init.lua new file mode 100644 index 0000000..5644f42 --- /dev/null +++ b/root/usr/lib/lua/xmlrpc/init.lua @@ -0,0 +1,608 @@ +-- Copyright 2003-2010 Kepler Project +-- XML-RPC implementation for Lua. + +local lxp = require "lxp" +local lom = require "lxp.lom" + +local assert, error, ipairs, pairs, select, type, tonumber, unpack = assert, error, ipairs, pairs, select, type, tonumber, unpack +local format, gsub, strfind, strsub = string.format, string.gsub, string.find, string.sub +local concat, tinsert = table.concat, table.insert +local ceil = math.ceil +local parse = lom.parse + +module (...) + +_COPYRIGHT = "Copyright (C) 2003-2014 Kepler Project" +_DESCRIPTION = "LuaXMLRPC is a library to make remote procedure calls using XML-RPC" +_PKGNAME = "LuaXMLRPC" +_VERSION_MAJOR = 1 +_VERSION_MINOR = 2 +_VERSION_MICRO = 2 +_VERSION = _VERSION_MAJOR .. "." .. _VERSION_MINOR .. "." .. _VERSION_MICRO + +--------------------------------------------------------------------- +-- XML-RPC Parser +--------------------------------------------------------------------- + +--------------------------------------------------------------------- +local function trim (s) + return (type(s) == "string" and gsub (s, "^%s*(.-)%s*$", "%1")) +end + +--------------------------------------------------------------------- +local function is_space (s) + return type(s) == "string" and trim(s) == "" +end + +--------------------------------------------------------------------- +-- Get next non-space element from tab starting from index i. +-- @param tab Table. +-- @param i Numeric index. +-- @return Object and its position on table; nil and an invalid index +-- when there is no more elements. +--------------------------------------------------------------------- +function next_nonspace (tab, i) + if not i then i = 1 end + while is_space (tab[i]) do i = i+1 end + return tab[i], i +end + +--------------------------------------------------------------------- +-- Get next element of tab with the given tag starting from index i. +-- @param tab Table. +-- @param tag String with the name of the tag. +-- @param i Numeric index. +-- @return Object and its position on table; nil and an invalid index +-- when there is no more elements. +--------------------------------------------------------------------- +local function next_tag (tab, tag, i) + if not i then i = 1 end + while tab[i] do + if type (tab[i]) == "table" and tab[i].tag == tag then + return tab[i], i + end + i = i + 1 + end + return nil, i +end + +--------------------------------------------------------------------- +local function x2number (tab) + if tab.tag == "int" or tab.tag == "i4" or tab.tag == "i8" or tab.tag == "double" then + return tonumber (next_nonspace (tab, 1), 10) + end +end + +--------------------------------------------------------------------- +local function x2boolean (tab) + if tab.tag == "boolean" then + local v = next_nonspace (tab, 1) + return v == true or v == "true" or tonumber (v) == 1 or false + end +end + +--------------------------------------------------------------------- +local function x2string (tab) + return tab.tag == "string" and (tab[1] or "") +end + +--------------------------------------------------------------------- +local function x2date (tab) + return tab.tag == "dateTime.iso8601" and next_nonspace (tab, 1) +end + +--------------------------------------------------------------------- +local function x2base64 (tab) + return tab.tag == "base64" and next_nonspace (tab, 1) +end + +--------------------------------------------------------------------- +local function x2name (tab) + return tab.tag == "name" and next_nonspace (tab, 1) +end + +local x2value + +--------------------------------------------------------------------- +-- Disassemble a member object in its name and value parts. +-- @param tab Table with a DOM representation. +-- @return String (name) and Object (value). +-- @see x2name, x2value. +--------------------------------------------------------------------- +local function x2member (tab) + return + x2name (next_tag(tab,"name")), + x2value (next_tag(tab,"value")) +end + +--------------------------------------------------------------------- +-- Disassemble a struct object into a Lua table. +-- @param tab Table with DOM representation. +-- @return Table with "name = value" pairs. +--------------------------------------------------------------------- +local function x2struct (tab) + if tab.tag == "struct" then + local res = {} + for i = 1, #tab do + if not is_space (tab[i]) then + local name, val = x2member (tab[i]) + res[name] = val + end + end + return res + end +end + +--------------------------------------------------------------------- +-- Disassemble an array object into a Lua table. +-- @param tab Table with DOM representation. +-- @return Table. +--------------------------------------------------------------------- +local function x2array (tab) + if tab.tag == "array" then + local d = next_tag (tab, "data") + local res = {} + for i = 1, #d do + if not is_space (d[i]) then + tinsert (res, x2value (d[i])) + end + end + return res + end +end + +--------------------------------------------------------------------- +local xmlrpc_types = { + int = x2number, + i4 = x2number, + i8 = x2number, + boolean = x2boolean, + string = x2string, + double = x2number, + ["dateTime.iso8601"] = x2date, + base64 = x2base64, + struct = x2struct, + array = x2array, +} + +local x2param, x2fault + +--------------------------------------------------------------------- +-- Disassemble a methodResponse into a Lua object. +-- @param tab Table with DOM representation. +-- @return Boolean (indicating wether the response was successful) +-- and (a Lua object representing the return values OR the fault +-- string and the fault code). +--------------------------------------------------------------------- +local function x2methodResponse (tab) + assert (type(tab) == "table", "Not a table") + assert (tab.tag == "methodResponse", + "Not a `methodResponse' tag: "..tab.tag) + local t = next_nonspace (tab, 1) + if t.tag == "params" then + return true, unpack (x2param (t)) + elseif t.tag == "fault" then + local f = x2fault (t) + return false, f.faultString, f.faultCode + else + error ("Couldn't find a nor a element") + end +end + +--------------------------------------------------------------------- +-- Disassemble a value element into a Lua object. +-- @param tab Table with DOM representation. +-- @return Object. +--------------------------------------------------------------------- +x2value = function (tab) + local t = tab.tag + assert (t == "value", "Not a `value' tag: "..t) + local n = next_nonspace (tab) + if type(n) == "string" or type(n) == "number" then + return n + elseif type (n) == "table" then + local t = n.tag + local get = xmlrpc_types[t] + if not get then error ("Invalid <"..t.."> element") end + return get (next_nonspace (tab)) + elseif type(n) == "nil" then + -- the next best thing is to assume it's an empty string + return "" + + end +end + +--------------------------------------------------------------------- +-- Disassemble a fault element into a Lua object. +-- @param tab Table with DOM representation. +-- @return Object. +--------------------------------------------------------------------- +x2fault = function (tab) + assert (tab.tag == "fault", "Not a `fault' tag: "..tab.tag) + return x2value (next_nonspace (tab)) +end + +--------------------------------------------------------------------- +-- Disassemble a param element into a Lua object. +-- Ignore white spaces between elements. +-- @param tab Table with DOM representation. +-- @return Object. +--------------------------------------------------------------------- +x2param = function (tab) + assert (tab.tag == "params", "Not a `params' tag") + local res = {} + local p, i = next_nonspace (tab, 1) + while p do + if p.tag == "param" then + tinsert (res, x2value (next_tag (p, "value"))) + end + p, i = next_nonspace (tab, i+1) + end + return res +end + +--------------------------------------------------------------------- +-- Disassemble a methodName element into a Lua object. +-- @param tab Table with DOM representation. +-- @return Object. +--------------------------------------------------------------------- +local function x2methodName (tab) + assert (tab.tag == "methodName", "Not a `methodName' tag: "..tab.tag) + return (next_nonspace (tab, 1)) +end + +--------------------------------------------------------------------- +-- Disassemble a methodCall element into its name and a list of parameters. +-- @param tab Table with DOM representation. +-- @return Object. +--------------------------------------------------------------------- +local function x2methodCall (tab) + assert (tab.tag == "methodCall", "Not a `methodCall' tag: "..tab.tag) + return + x2methodName (next_tag (tab,"methodName")), + x2param (next_tag (tab,"params")) +end + +--------------------------------------------------------------------- +-- End of XML-RPC Parser +--------------------------------------------------------------------- + +--------------------------------------------------------------------- +-- Convert a Lua Object into an XML-RPC string. +--------------------------------------------------------------------- + +--------------------------------------------------------------------- +local formats = { + boolean = "%d", + number = "%d", + string = "%s", + base64 = "%s", + + array = "\n%s\n", + double = "%s", + int = "%s", + struct = "%s", + + member = "%s%s", + value = "%s", + + param = "%s", + + params = [[ + + %s + ]], + + fault = [[ + + %s + ]], + + methodCall = [[ + + + %s +%s + +]], + + methodResponse = [[ + + +%s +]], +} +formats.table = formats.struct + +local toxml = {} +toxml.double = function (v,t) return format (formats.double, v) end +toxml.int = function (v,t) return format (formats.int, v) end +toxml.string = function (v,t) return format (formats.string, v) end +toxml.base64 = function (v,t) return format (formats.base64, v) end + +--------------------------------------------------------------------- +-- Build a XML-RPC representation of a boolean. +-- @param v Object. +-- @return String. +--------------------------------------------------------------------- +function toxml.boolean (v) + local n = (v and 1) or 0 + return format (formats.boolean, n) +end + +--------------------------------------------------------------------- +-- Build a XML-RPC representation of a number. +-- @param v Object. +-- @param t Object representing the XML-RPC type of the value. +-- @return String. +--------------------------------------------------------------------- +function toxml.number (v, t) + local tt = (type(t) == "table") and t["*type"] + if tt == "int" or tt == "i4" or tt == "i8" then + return toxml.int (v, t) + elseif tt == "double" then + return toxml.double (v, t) + elseif v == ceil(v) then + return toxml.int (v, t) + else + return toxml.double (v, t) + end +end + +--------------------------------------------------------------------- +-- @param typ Object representing a type. +-- @return Function that generate an XML element of the given type. +-- The object could be a string (as usual in Lua) or a table with +-- a field named "type" that should be a string with the XML-RPC +-- type name. +--------------------------------------------------------------------- +local function format_func (typ) + if type (typ) == "table" then + return toxml[typ.type] + else + return toxml[typ] + end +end + +--------------------------------------------------------------------- +-- @param val Object representing an array of values. +-- @param typ Object representing the type of the value. +-- @return String representing the equivalent XML-RPC value. +--------------------------------------------------------------------- +function toxml.array (val, typ) + local ret = {} + local et = typ.elemtype + local f = format_func (et) + for i,v in ipairs (val) do + if et and et ~= "array" then + tinsert (ret, format (formats.value, f (v, et))) + else + local ct,cv = type_val(v) + local cf = format_func(ct) + tinsert (ret, format (formats.value, cf(cv, ct))) + end + + end + return format (formats.array, concat (ret, '\n')) +end + +--------------------------------------------------------------------- +--------------------------------------------------------------------- +function toxml.struct (val, typ) + local ret = {} + if type (typ) == "table" then + for n,t in pairs (typ.elemtype) do + local f = format_func (t) + tinsert (ret, format (formats.member, n, f (val[n], t))) + end + else + for i, v in pairs (val) do + tinsert (ret, toxml.member (i, v)) + end + end + return format (formats.struct, concat (ret)) +end + +toxml.table = toxml.struct + +--------------------------------------------------------------------- +--------------------------------------------------------------------- +function toxml.member (n, v) + return format (formats.member, n, toxml.value (v)) +end + +--------------------------------------------------------------------- +-- Get type and value of object. +--------------------------------------------------------------------- +function type_val (obj) + local t = type (obj) + local v = obj + if t == "table" then + t = obj["*type"] or "table" + v = obj["*value"] or obj + end + return t, v +end + +--------------------------------------------------------------------- +-- Convert a Lua object to a XML-RPC object (plain string). +--------------------------------------------------------------------- +function toxml.value (obj) + local to, val = type_val (obj) + if type(to) == "table" then + return format (formats.value, toxml[to.type] (val, to)) + else + -- primitive (not structured) types. + --return format (formats[to], val) + return format (formats.value, toxml[to] (val, to)) + end +end + +--------------------------------------------------------------------- +-- @param ... List of parameters. +-- @return String representing the `params' XML-RPC element. +--------------------------------------------------------------------- +function toxml.params (...) + local params_list = {} + for i = 1, select ("#", ...) do + params_list[i] = format (formats.param, toxml.value (select (i, ...))) + end + return format (formats.params, concat (params_list, '\n ')) +end + +--------------------------------------------------------------------- +-- @param method String with method's name. +-- @param ... List of parameters. +-- @return String representing the `methodCall' XML-RPC element. +--------------------------------------------------------------------- +function toxml.methodCall (method, ...) + local idx = strfind (method, "[^A-Za-z_.:/0-9]") + if idx then + error (format ("Invalid character `%s'", strsub (method, idx, idx))) + end + return format (formats.methodCall, method, toxml.params (...)) +end + +--------------------------------------------------------------------- +-- @param err String with error message. +-- @return String representing the `fault' XML-RPC element. +--------------------------------------------------------------------- +function toxml.fault (err) + local code + local message = err + if type (err) == "table" then + code = err.code + message = err.message + end + return format (formats.fault, toxml.value { + faultCode = { ["*type"] = "int", ["*value"] = code or err.faultCode or 1 }, + faultString = message or err.faultString or "fatal error", + }) +end + +--------------------------------------------------------------------- +-- @param ok Boolean indicating if the response was correct or a +-- fault one. +-- @param params Object containing the response contents. +-- @return String representing the `methodResponse' XML-RPC element. +--------------------------------------------------------------------- +function toxml.methodResponse (ok, params) + local resp + if ok then + resp = toxml.params (params) + else + resp = toxml.fault (params) + end + return format (formats.methodResponse, resp) +end + +--------------------------------------------------------------------- +-- End of converter from Lua to XML-RPC. +--------------------------------------------------------------------- + + +--------------------------------------------------------------------- +-- Create a representation of an array with the given element type. +--------------------------------------------------------------------- +function newArray (elemtype) + return { type = "array", elemtype = elemtype, } +end + +--------------------------------------------------------------------- +-- Create a representation of a structure with the given members. +--------------------------------------------------------------------- +function newStruct (members) + return { type = "struct", elemtype = members, } +end + +--------------------------------------------------------------------- +-- Create a representation of a value according to a type. +-- @param val Any Lua value. +-- @param typ A XML-RPC type. +--------------------------------------------------------------------- +function newTypedValue (val, typ) + return { ["*type"] = typ, ["*value"] = val } +end + +--------------------------------------------------------------------- +-- Create the XML-RPC string used to call a method. +-- @param method String with method name. +-- @param ... Parameters to the call. +-- @return String with the XML string/document. +--------------------------------------------------------------------- +function clEncode (method, ...) + return toxml.methodCall (method, ...) +end + +--------------------------------------------------------------------- +-- Convert the method response document to a Lua table. +-- @param meth_resp String with XML document. +-- @return Boolean indicating whether the call was successful or not; +-- and a Lua object with the converted response element. +--------------------------------------------------------------------- +function clDecode (meth_resp) + local d = parse (meth_resp) + if type(d) ~= "table" then + error ("Not an XML document: "..meth_resp) + end + return x2methodResponse (d) +end + +--------------------------------------------------------------------- +-- Convert the method call (client request) document to a name and +-- a list of parameters. +-- @param request String with XML document. +-- @return String with method's name AND the table of arguments. +--------------------------------------------------------------------- +function srvDecode (request) + local d = parse (request) + if type(d) ~= "table" then + error ("Not an XML document: "..request) + end + return x2methodCall (d) +end + +--------------------------------------------------------------------- +-- Convert a table into an XML-RPC methodReponse element. +-- @param obj Lua object. +-- @param is_fault Boolean indicating wether the result should be +-- a `fault' element (default = false). +-- @return String with XML-RPC response. +--------------------------------------------------------------------- +function srvEncode (obj, is_fault) + local ok = not (is_fault or false) + return toxml.methodResponse (ok, obj) +end + +--------------------------------------------------------------------- +-- Register the methods. +-- @param tab_or_func Table or mapping function. +-- If a table is given, it can have one level of objects and then the +-- methods; +-- if a function is given, it will be used as the dispatcher. +-- The given function should return a Lua function that implements. +--------------------------------------------------------------------- +dispatch = error +function srvMethods (tab_or_func) + local t = type (tab_or_func) + if t == "function" then + dispatch = tab_or_func + elseif t == "table" then + dispatch = function (name) + local ok, _, obj, method = strfind (name, "^([^.]+)%.(.+)$") + if not ok then + return tab_or_func[name] + else + if tab_or_func[obj] and tab_or_func[obj][method] then + return function (...) + return tab_or_func[obj][method] (obj, ...) + end + else + return nil + end + end + end + else + error ("Argument is neither a table nor a function") + end +end diff --git a/root/usr/lib/lua/xmlrpc/scgi.lua b/root/usr/lib/lua/xmlrpc/scgi.lua new file mode 100644 index 0000000..d39e30e --- /dev/null +++ b/root/usr/lib/lua/xmlrpc/scgi.lua @@ -0,0 +1,76 @@ +-- Copyright 2003-2010 Kepler Project +-- Copyright 2014-2018 Sandor Balazsi +-- XML-RPC over SCGI. + +local error, tonumber, tostring, unpack = error, tonumber, tostring, unpack + +local socket= require"socket" +local string= require"string" +local xmlrpc= require"xmlrpc" + +module("xmlrpc.scgi") + +--------------------------------------------------------------------- +-- Call a remote method. +-- @param addr String with the address of the SCGI server. +-- @param port The port of the SCGI server. +-- @param method String with the name of the method to be called. +-- @return Table with the response(could be a `fault' or a `params' +-- XML-RPC element). +--------------------------------------------------------------------- +function call(addr, port, method, ...) + local request_body = xmlrpc.clEncode(method, ...) + local sock = socket.connect(addr, port) + if sock == nil then + return false, "socket connect failed" + end + sock:send(netstring(request_body)) + local err, code, headers, body = receive(sock) + + if tonumber(code) == 200 then + return xmlrpc.clDecode(body) + else + error(tostring(err or code)) + end +end + +--------------------------------------------------------------------- +-- Encode message as netstring +-- @param request_body String with the message +-- @return String with the encoded message +--------------------------------------------------------------------- +function netstring(request_body) + local null = "\0" + local content_length = "CONTENT_LENGTH" .. null .. string.len(request_body) .. null + local scgi_enable = "SCGI" .. null .. "1" .. null + local request_method = "REQUEST_METHOD" .. null .. "POST" .. null + local server_protocol = "SERVER_PROTOCOL" .. null .. "HTTP/1.1" .. null + local header = content_length .. scgi_enable .. request_method .. server_protocol + return string.len(header) .. ":" .. header .. "," .. request_body +end + +--------------------------------------------------------------------- +-- Receive and parse socket response +-- @param sock Socket instance +-- @return Headers, body and error codes +--------------------------------------------------------------------- +function receive(sock) + local line, body, err + local headers = {} + + line, err = sock:receive() + if err then return err, "500" end + while line ~= "" do + local name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) + if not(name and value) then return "malformed reponse header: " .. line, "500" end + headers[string.lower(name)] = value + + line, err = sock:receive() + if err then return err, "500" end + end + + body = sock:receive(headers["content-length"]) + local code = socket.skip(2, string.find(headers["status"], "^(%d%d%d)")) + return err, code, headers, body +end + diff --git a/root/usr/share/rpcd/acl.d/luci-app-rtorrent.json b/root/usr/share/rpcd/acl.d/luci-app-rtorrent.json new file mode 100644 index 0000000..33e3e9b --- /dev/null +++ b/root/usr/share/rpcd/acl.d/luci-app-rtorrent.json @@ -0,0 +1,11 @@ +{ + "luci-app-rtorrent": { + "description": "Grant UCI access for luci-app-rtorrent", + "read": { + "uci": [ "rtorrent" ] + }, + "write": { + "uci": [ "rtorrent" ] + } + } +}