├── src ├── etc │ ├── config │ │ └── rtorrent │ ├── luci-uploads │ │ └── rtorrent │ │ │ └── .placeholder │ └── uci-defaults │ │ └── rtorrent ├── www │ └── luci-static │ │ └── resources │ │ ├── icons │ │ ├── filetypes │ │ │ ├── 7z.png │ │ │ ├── ac3.png │ │ │ ├── avi.png │ │ │ ├── bat.png │ │ │ ├── bmp.png │ │ │ ├── c.png │ │ │ ├── cgi.png │ │ │ ├── cpp.png │ │ │ ├── dts.png │ │ │ ├── elf.png │ │ │ ├── exe.png │ │ │ ├── f4v.png │ │ │ ├── flac.png │ │ │ ├── flv.png │ │ │ ├── gif.png │ │ │ ├── gz.png │ │ │ ├── h.png │ │ │ ├── htm.png │ │ │ ├── jar.png │ │ │ ├── jpeg.png │ │ │ ├── jpg.png │ │ │ ├── lua.png │ │ │ ├── mkv.png │ │ │ ├── mov.png │ │ │ ├── mp3.png │ │ │ ├── mp4.png │ │ │ ├── nfo.png │ │ │ ├── pl.png │ │ │ ├── png.png │ │ │ ├── sh.png │ │ │ ├── sqlite.png │ │ │ ├── srt.png │ │ │ ├── wma.png │ │ │ ├── wmv.png │ │ │ ├── arj.png │ │ │ ├── bz2.png │ │ │ ├── cab.png │ │ │ ├── iso.png │ │ │ ├── rar.png │ │ │ ├── tar.png │ │ │ ├── tgz.png │ │ │ ├── zip.png │ │ │ ├── app.png │ │ │ ├── css.png │ │ │ ├── db.png │ │ │ ├── dir.png │ │ │ ├── doc.png │ │ │ ├── pdf.png │ │ │ ├── php.png │ │ │ ├── ppt.png │ │ │ ├── psd.png │ │ │ ├── txt.png │ │ │ ├── up.png │ │ │ ├── xls.png │ │ │ ├── code.png │ │ │ ├── file.png │ │ │ ├── flash.png │ │ │ ├── html.png │ │ │ ├── image.png │ │ │ ├── java.png │ │ │ ├── linux.png │ │ │ ├── music.png │ │ │ ├── script.png │ │ │ ├── video.png │ │ │ └── archive.png │ │ ├── home.png │ │ └── unknown_tracker.svg │ │ └── rtorrent.js └── usr │ └── lib │ └── lua │ ├── luci │ ├── view │ │ └── rtorrent │ │ │ ├── value.htm │ │ │ ├── map.htm │ │ │ ├── tvalue.htm │ │ │ ├── dropdown.htm │ │ │ ├── dvalue.htm │ │ │ ├── upload.htm │ │ │ ├── simpleform.htm │ │ │ ├── tblsection_main.htm │ │ │ └── tblsection.htm │ ├── model │ │ └── cbi │ │ │ └── rtorrent │ │ │ ├── rss.lua │ │ │ ├── logger.lua │ │ │ ├── settings │ │ │ ├── frontend.lua │ │ │ ├── rss.lua │ │ │ └── rtorrent.lua │ │ │ ├── string.lua │ │ │ ├── download.lua │ │ │ ├── rss-rule.lua │ │ │ ├── torrent │ │ │ ├── info.lua │ │ │ ├── chunks.lua │ │ │ ├── trackers.lua │ │ │ ├── files.lua │ │ │ └── peers.lua │ │ │ ├── add.lua │ │ │ ├── array.lua │ │ │ ├── main.lua │ │ │ └── common.lua │ └── controller │ │ └── rtorrent.lua │ ├── rtorrent.lua │ ├── xmlrpc │ ├── scgi.lua │ └── init.lua │ ├── bencode.lua │ └── rss_downloader.lua ├── control ├── conffiles ├── preinst ├── prerm ├── postinst └── control ├── .gitignore ├── doc ├── luci-app-rtorrent_license ├── lua-bencode_license └── lua-xml-rpc_license ├── Makefile └── README.md /src/etc/config/rtorrent: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /control/conffiles: -------------------------------------------------------------------------------- 1 | /etc/config/rtorrent 2 | -------------------------------------------------------------------------------- /src/etc/luci-uploads/rtorrent/.placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/7z.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/ac3.png: -------------------------------------------------------------------------------- 1 | music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/avi.png: -------------------------------------------------------------------------------- 1 | video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/bat.png: -------------------------------------------------------------------------------- 1 | script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/bmp.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/c.png: -------------------------------------------------------------------------------- 1 | code.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/cgi.png: -------------------------------------------------------------------------------- 1 | script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/cpp.png: -------------------------------------------------------------------------------- 1 | code.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/dts.png: -------------------------------------------------------------------------------- 1 | music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/elf.png: -------------------------------------------------------------------------------- 1 | linux.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/exe.png: -------------------------------------------------------------------------------- 1 | app.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/f4v.png: -------------------------------------------------------------------------------- 1 | flash.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/flac.png: -------------------------------------------------------------------------------- 1 | music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/flv.png: -------------------------------------------------------------------------------- 1 | flash.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/gif.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/gz.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/h.png: -------------------------------------------------------------------------------- 1 | code.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/htm.png: -------------------------------------------------------------------------------- 1 | html.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/jar.png: -------------------------------------------------------------------------------- 1 | java.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/jpeg.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/jpg.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/lua.png: -------------------------------------------------------------------------------- 1 | script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/mkv.png: -------------------------------------------------------------------------------- 1 | video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/mov.png: -------------------------------------------------------------------------------- 1 | video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/mp3.png: -------------------------------------------------------------------------------- 1 | music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/mp4.png: -------------------------------------------------------------------------------- 1 | video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/nfo.png: -------------------------------------------------------------------------------- 1 | txt.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/pl.png: -------------------------------------------------------------------------------- 1 | script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/png.png: -------------------------------------------------------------------------------- 1 | image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/sh.png: -------------------------------------------------------------------------------- 1 | script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/sqlite.png: -------------------------------------------------------------------------------- 1 | db.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/srt.png: -------------------------------------------------------------------------------- 1 | txt.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/wma.png: -------------------------------------------------------------------------------- 1 | music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/wmv.png: -------------------------------------------------------------------------------- 1 | video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/arj.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/bz2.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/cab.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/iso.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/rar.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/tar.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/tgz.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/zip.png: -------------------------------------------------------------------------------- 1 | archive.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ipk/ 2 | key/ 3 | todo/ 4 | temp/ 5 | *~ 6 | *.swp 7 | *.swo 8 | -------------------------------------------------------------------------------- /control/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 3 | mv /etc/config/rtorrent /etc/config/rtorrent-prev &>/dev/null 4 | exit 0 5 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/value.htm: -------------------------------------------------------------------------------- 1 | 4 | <%+cbi/value%> 5 | -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/home.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/app.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/css.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/db.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/dir.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/doc.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/pdf.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/php.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/ppt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/ppt.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/psd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/psd.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/txt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/txt.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/up.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/xls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/xls.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/code.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/file.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/flash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/flash.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/html.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/image.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/java.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/linux.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/music.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/music.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/script.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/video.png -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/filetypes/archive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wolandmaster/luci-app-rtorrent/HEAD/src/www/luci-static/resources/icons/filetypes/archive.png -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/map.htm: -------------------------------------------------------------------------------- 1 | 4 | 5 | <%+cbi/map%> 6 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/tvalue.htm: -------------------------------------------------------------------------------- 1 | 7 | <%+cbi/tvalue%> 8 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/dropdown.htm: -------------------------------------------------------------------------------- 1 | 6 | <%+cbi/dropdown%> 7 | -------------------------------------------------------------------------------- /control/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 3 | rm -fr /tmp/luci-indexcache* /tmp/luci-modulecache 4 | [ -x ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 5 | . ${IPKG_INSTROOT}/lib/functions.sh 6 | default_prerm $0 $@ 7 | -------------------------------------------------------------------------------- /control/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ "${IPKG_NO_SCRIPT}" = "1" ] && exit 0 3 | rm -fr /tmp/luci-indexcache* /tmp/luci-modulecache 4 | [ -x ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0 5 | . ${IPKG_INSTROOT}/lib/functions.sh 6 | default_postinst $0 $@ 7 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/dvalue.htm: -------------------------------------------------------------------------------- 1 | 12 | <%+cbi/dvalue%> 13 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/upload.htm: -------------------------------------------------------------------------------- 1 | 11 | <%+cbi/upload%> 12 | -------------------------------------------------------------------------------- /control/control: -------------------------------------------------------------------------------- 1 | Package: luci-app-rtorrent 2 | Version: 0.2.3 3 | Depends: libc, rtorrent-rpc, luaexpat, luasocket, luasec, luci-compat, luci-lib-httpprotoutils 4 | Source: https://github.com/wolandmaster/luci-app-rtorrent 5 | Section: luci 6 | Maintainer: Sandor Balazsi 7 | Architecture: all 8 | Description: rTorrent frontend for LuCI 9 | -------------------------------------------------------------------------------- /src/etc/uci-defaults/rtorrent: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -e "/etc/config/rtorrent-prev" ]; then 4 | mv /etc/config/rtorrent-prev /etc/config/rtorrent 5 | else 6 | touch /etc/config/rtorrent 7 | fi 8 | 9 | if ! uci get rtorrent.logging &>/dev/null; then 10 | uci set rtorrent.logging=rss \ 11 | && uci commit rtorrent 12 | fi 13 | 14 | rm -fr /tmp/luci-indexcache* /tmp/luci-modulecache 15 | 16 | exit 0 17 | -------------------------------------------------------------------------------- /doc/luci-app-rtorrent_license: -------------------------------------------------------------------------------- 1 | Copyright 2014-2021 Sandor Balazsi 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/simpleform.htm: -------------------------------------------------------------------------------- 1 | 4 | 5 | <%+cbi/simpleform%> 6 | 21 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/rss.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local build_url = require "luci.dispatcher".build_url 5 | local common = require "luci.model.cbi.rtorrent.common" 6 | 7 | local map, rss_rule, name, match, enabled 8 | 9 | map = Map("rtorrent", "RSS Downloader", common.rss_downloader_status()) 10 | map.template = "rtorrent/map" 11 | 12 | rss_rule = map:section(TypedSection, "rss-rule") 13 | rss_rule.template = "rtorrent/tblsection" 14 | rss_rule.addremove = true 15 | rss_rule.anonymous = true 16 | rss_rule.sortable = true 17 | rss_rule.extedit = build_url("admin", "rtorrent", "rss", "%s") 18 | rss_rule.create = function(self, section) 19 | local rule = TypedSection.create(self, section) 20 | self.map:set(rule, "name", "Unnamed rule") 21 | luci.http.redirect(build_url("admin", "rtorrent", "rss", rule)) 22 | end 23 | 24 | name = rss_rule:option(DummyValue, "name", "Name") 25 | name.width = "45%" 26 | name.classes = { "wrap" } 27 | 28 | match = rss_rule:option(DummyValue, "match", "Match") 29 | match.width = "54%" 30 | match.classes = { "wrap" } 31 | 32 | enabled = rss_rule:option(Flag, "enabled", "Enabled") 33 | enabled.rmempty = false 34 | enabled.width = "1%" 35 | enabled.classes = { "nowrap", "center" } 36 | 37 | return map 38 | -------------------------------------------------------------------------------- /doc/lua-bencode_license: -------------------------------------------------------------------------------- 1 | All files in the source distribution of lua-bencode may be copied under the 2 | same terms as Lua 5.0, 5.1, and 5.2. These terms are also known as the "MIT/X 3 | Consortium License". 4 | 5 | For reasons of clarity, a copy of these terms is included below. 6 | 7 | Copyright (c) 2009, 2010, 2011, 2012 by Moritz Wilhelmy 8 | Copyright (c) 2009 by Kristofer Karlsson 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of 11 | this software and associated documentation files (the "Software"), to deal in 12 | the Software without restriction, including without limitation the rights to 13 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 14 | of the Software, and to permit persons to whom the Software is furnished to do 15 | so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in all 18 | copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | SOFTWARE. 27 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/tblsection_main.htm: -------------------------------------------------------------------------------- 1 |
2 |
    3 | <% for _, tab in ipairs(self.tab_names) do 4 | local active = tab == self.selected_tab and " active" or "" 5 | local sort = self.sort ~= self.default_sort and "/" .. self.sort or "" -%> 6 |
  • 7 | <%=self.tabs[tab].title%> 8 |
  • 9 | <%- end %> 10 |
11 |
12 | <%+rtorrent/tblsection%> 13 |
14 | >
17 | " /> 18 | 19 | 38 | -------------------------------------------------------------------------------- /src/www/luci-static/resources/icons/unknown_tracker.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = luci-app-rtorrent 2 | VERSION = $(shell awk '/^Version:/ {print $$2}' control/control) 3 | ARCH = $(shell awk '/^Architecture:/ {print $$2}' control/control) 4 | IPK = $(NAME)_$(VERSION)_$(ARCH).ipk 5 | 6 | .PHONY: control 7 | 8 | all: packages-file 9 | 10 | ipk: clean control data 11 | mkdir -p ipk 12 | echo "2.0" > ipk/debian-binary 13 | cd ipk && tar czf $(IPK) control.tar.gz data.tar.gz debian-binary 14 | rm -f ipk/*.tar.gz ipk/debian-binary 15 | 16 | control: 17 | mkdir -p ipk 18 | rm -f ../ipk/control.tar.gz 19 | cd control && tar czf ../ipk/control.tar.gz * 20 | 21 | data: 22 | mkdir -p ipk 23 | cd src && tar czf ../ipk/data.tar.gz * 24 | 25 | key: 26 | mkdir -p key 27 | usign -G -s key/sign_key -p key/sign_key.pub -c "$(NAME) key" 28 | cd key && usign -F -p sign_key.pub | xargs cp sign_key.pub 29 | 30 | packages-file: ipk 31 | mkdir -p ipk 32 | cp control/control ipk/Packages 33 | echo "Filename: $(IPK)" >> ipk/Packages 34 | wc -c ipk/$(IPK) | awk '{print "Size:", $$1}' >> ipk/Packages 35 | sha256sum ipk/$(IPK) | awk '{print "SHA256sum:", $$1}' >> ipk/Packages 36 | usign -S -m ipk/Packages -s key/sign_key 37 | gzip ipk/Packages 38 | 39 | clean: 40 | rm -fr ipk 41 | 42 | test-deploy: test-remove 43 | cp -a src/usr/* /usr 44 | cp -a src/www/* /www 45 | 46 | test-remove: 47 | find src/usr src/www -type f -o -type l | sed 's/^src//' | xargs rm -f 48 | rm -fr /usr/lib/lua/luci/model/cbi/rtorrent 49 | rm -fr /usr/lib/lua/luci/view/rtorrent 50 | rm -fr /usr/lib/lua/xmlrpc 51 | rm -fr /www/luci-static/resources/icons/filetypes 52 | rm -fr /tmp/luci-indexcache* /tmp/luci-modulecache 53 | 54 | test-reinstall: 55 | opkg list-installed | grep -q $(NAME) \ 56 | && opkg --force-reinstall install $(NAME) \ 57 | || echo "$(NAME) not installed, skip reinstall" 58 | 59 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/logger.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local socket = require "socket" 5 | local nixio = require "nixio" 6 | local class = require "luci.util".class 7 | local array = require "luci.model.cbi.rtorrent.array" 8 | 9 | local levels = { 10 | ["TRACE"] = 1, ["DEBUG"] = 2, ["INFO"] = 3, ["WARN"] = 4, ["ERROR"] = 5, ["FATAL"] = 6, ["OFF"] = 7 11 | } 12 | 13 | local function timestamp() 14 | return os.date("%Y-%m-%d %H:%M:%S") .. "," .. tostring("%.3f" % socket.gettime()):match("%.(%d+)") 15 | end 16 | 17 | local function message(self, level, ...) 18 | if levels[level] >= levels[self.level] then 19 | local line = array() 20 | :append(timestamp()) 21 | :append("%-5s" % level) 22 | :append(unpack({...})) 23 | :join(" ") 24 | self.fh:lock("lock") 25 | self.fh:write(line .. "\n") 26 | self.fh:sync() 27 | self.fh:lock("ulock") 28 | end 29 | end 30 | 31 | --[[ A P I ]]-- 32 | 33 | local logger = class() 34 | 35 | function logger.__init__(self, level, target) 36 | self.level = level or "INFO" 37 | if level ~= "OFF" then 38 | self.fh = nixio.open(target or "/dev/tty", "a") 39 | end 40 | end 41 | 42 | function logger.close(self) 43 | self.fh:sync() 44 | self.fh:close() 45 | end 46 | 47 | function logger.trace(self, ...) 48 | message(self, "TRACE", ...) 49 | end 50 | 51 | function logger.debug(self, ...) 52 | message(self, "DEBUG", ...) 53 | end 54 | 55 | function logger.info(self, ...) 56 | message(self, "INFO", ...) 57 | end 58 | 59 | function logger.warn(self, ...) 60 | message(self, "WARN", ...) 61 | end 62 | 63 | function logger.error(self, ...) 64 | message(self, "ERROR", ...) 65 | end 66 | 67 | function logger.fatal(self, ...) 68 | message(self, "FATAL", ...) 69 | end 70 | 71 | return logger 72 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/controller/rtorrent.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local dm = require "luci.model.cbi.rtorrent.download" 5 | 6 | module("luci.controller.rtorrent", package.seeall) 7 | 8 | function index() 9 | entry({ "admin", "rtorrent" }, firstchild(), "Torrent", 70) 10 | entry({ "admin", "rtorrent", "main" }, 11 | form("rtorrent/main"), "List", 10).leaf = true 12 | entry({ "admin", "rtorrent", "add" }, 13 | form("rtorrent/add", { autoapply = true }), "Add", 20) 14 | entry({ "admin", "rtorrent", "rss" }, arcombine( 15 | cbi("rtorrent/rss"), cbi("rtorrent/rss-rule")), "RSS Downloader", 30).leaf = true 16 | entry({ "admin", "rtorrent", "settings" }, 17 | alias("admin", "rtorrent", "settings", "rtorrent"), "Settings", 40) 18 | 19 | -- torrent 20 | entry({ "admin", "rtorrent", "torrent", "info" }, 21 | form("rtorrent/torrent/info"), "Info", 10).leaf = true 22 | entry({ "admin", "rtorrent", "torrent", "files" }, 23 | form("rtorrent/torrent/files"), "Files", 20).leaf = true 24 | entry({ "admin", "rtorrent", "torrent", "trackers" }, 25 | form("rtorrent/torrent/trackers"), "Trackers", 30).leaf = true 26 | entry({ "admin", "rtorrent", "torrent", "peers" }, 27 | form("rtorrent/torrent/peers"), "Peers", 40).leaf = true 28 | entry({ "admin", "rtorrent", "torrent", "chunks" }, 29 | form("rtorrent/torrent/chunks"), "Chunks", 50).leaf = true 30 | 31 | -- settings 32 | entry({ "admin", "rtorrent", "settings", "rtorrent" }, 33 | form("rtorrent/settings/rtorrent"), "rTorrent", 10) 34 | entry({ "admin", "rtorrent", "settings", "frontend" }, 35 | cbi("rtorrent/settings/frontend"), "rTorrent Frontend", 20) 36 | entry({ "admin", "rtorrent", "settings", "rss" }, 37 | cbi("rtorrent/settings/rss"), "RSS Downloader", 30) 38 | 39 | -- download 40 | entry({ "admin", "rtorrent", "download" }, call("download")).leaf = true 41 | end 42 | 43 | function download() 44 | dm.download(unpack(luci.dispatcher.context.requestpath, 4)) 45 | end 46 | -------------------------------------------------------------------------------- /doc/lua-xml-rpc_license: -------------------------------------------------------------------------------- 1 | Lua XML-RPC is free software: it can be used for both academic and commercial 2 | purposes at absolutely no cost. There are no royalties or GNU-like "copyleft" 3 | restrictions. Lua XML-RPC qualifies as Open Source software. Its licenses are 4 | compatible with GPL. Lua XML-RPC is not in the public domain and the Kepler 5 | Project hold its copyright. The legal details are below. 6 | 7 | The spirit of the license is that you are free to use Lua XML-RPC for any 8 | purpose at no cost without having to ask us. The only requirement is that if 9 | you do use Lua XML-RPC, then you should give us credit by including the 10 | appropriate copyright notice somewhere in your product or its documentation. 11 | 12 | The Lua XML-RPC library is designed and implemented by Roberto Ierusalimschy, 13 | Andre Carregal and Tomas Guisasola. Tim Niemueller is the current maintainer. 14 | The implementation is not derived from licensed software. 15 | 16 | Copyright 2003-2010 Kepler Project. 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in all 26 | copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | SOFTWARE. 35 | -------------------------------------------------------------------------------- /src/www/luci-static/resources/rtorrent.js: -------------------------------------------------------------------------------- 1 | function selectElement(selector) { 2 | return new Promise(resolve => { 3 | const nodes = document.querySelectorAll(selector); 4 | if (nodes.length > 0) { 5 | return resolve(nodes); 6 | } 7 | const observer = new MutationObserver(mutations => { 8 | const nodes = document.querySelectorAll(selector); 9 | if (nodes.length > 0) { 10 | observer.disconnect(); 11 | return resolve(nodes); 12 | } 13 | }); 14 | observer.observe(document.body, { childList: true, subtree: true }); 15 | }); 16 | } 17 | 18 | function eachElement(selector, callback, scope) { 19 | selectElement(selector).then(elements => { 20 | for (let i = 0; i < elements.length; i++) { 21 | callback.call(scope, elements[i], i, elements); 22 | } 23 | }); 24 | } 25 | 26 | async function updateInvertSelection(invertSelection) { 27 | let count = 0, selected = 0; 28 | await eachElement(".tr:not(:last-child) input[type=checkbox]", checkbox => { 29 | count++; selected += checkbox.checked ? 1 : 0; 30 | }); 31 | invertSelection.checked = (selected == count); 32 | invertSelection.indeterminate = (selected > 0 && selected < count); 33 | } 34 | 35 | function toLocalTimeString(date) { 36 | date.setTime(date.getTime() - 60000 * date.getTimezoneOffset()) 37 | return date.toISOString().replace("T", " ").substring(0, 19); 38 | } 39 | 40 | function toHumanTimeString(sec) { 41 | let date = new Date(sec * 1000), str = date.getSeconds() + "s"; 42 | if (date.getUTCMinutes() > 0) { str = date.getUTCMinutes() + "m " + str; } 43 | if (date.getUTCHours() > 0) { str = date.getUTCHours() + "h " + str; } 44 | if (date.getUTCDate() > 1) { str = (date.getUTCDate() - 1) + "d " + str; } 45 | return str; 46 | } 47 | 48 | function updateNextRunTime(element, start, interval) { 49 | document.getElementById(element).innerHTML = "updating.."; 50 | setInterval(function() { 51 | let now = Date.now(); 52 | let remaining = start * 1000 > now 53 | ? start * 1000 - now 54 | : (interval * 1000) - (now - start * 1000) % (interval * 1000); 55 | document.getElementById(element).innerHTML = toLocalTimeString(new Date(now + remaining)) 56 | + " (" + toHumanTimeString(Math.floor(remaining / 1000)) + " remaining)"; 57 | }, 1000); 58 | } 59 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/settings/frontend.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local common = require "luci.model.cbi.rtorrent.common" 5 | local array = require "luci.model.cbi.rtorrent.array" 6 | require "luci.model.cbi.rtorrent.string" 7 | 8 | local map, cookies, domain, cookie 9 | 10 | function cookie_list(value) 11 | if value:blank() or value:trim():ends(";") or value:trim():starts(";") 12 | or value:match(";[^ ]") or value:match("; +") then 13 | return false 14 | end 15 | local name_value 16 | for name_value in value:gmatch("[^; ]+") do 17 | -- https://stackoverflow.com/a/1969339 18 | if not name_value:match("^[%w!#$%%&'*+-.^_`|~]+" 19 | .. "=" .. "[%w!#$%%&'()*+-./:<=>?@%[%]^_`{|}~]+$") then 20 | return false 21 | end 22 | end 23 | return true 24 | end 25 | 26 | map = Map("rtorrent", "rTorrent LuCI Frontend Settings") 27 | map.on_parse = function(self, ...) 28 | local domains = array() 29 | for _, section_id in ipairs(cookies:cfgsections()) do 30 | if domains:get(domain:formvalue(section_id)) then 31 | domains:get(domain:formvalue(section_id)):insert(section_id) 32 | else domains:set(domain:formvalue(section_id), { section_id }) end 33 | end 34 | for _, sections in domains:pairs() do 35 | if sections:size() > 1 then 36 | for _, section_id in sections:pairs() do 37 | domain:add_error(section_id, "invalid", "Same domain defined more than once!") 38 | end 39 | self.save = false 40 | end 41 | end 42 | end 43 | 44 | cookies = map:section(TypedSection, "cookies", "Cookies", 45 | "Cookies are used to download torrent files and RSS feeds from authenticated pages and trackers.") 46 | cookies.addremove = true 47 | cookies.anonymous = true 48 | 49 | domain = cookies:option(Value, "domain", "Domain") 50 | domain.rmempty = false 51 | 52 | cookie = cookies:option(TextValue, "cookie", "Cookie", 53 | 'List of name-value pairs separated by a semicolon and space: syntax.') 55 | cookie.template = "rtorrent/tvalue" 56 | cookie.rmempty = false 57 | cookie.rows = 1 58 | cookie.validate = function(self, value, section) 59 | if value and not cookie_list(value) then 60 | return nil, "The provided cookie does not satisfy cookie name-value list syntax!" 61 | end 62 | return value 63 | end 64 | 65 | return map 66 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/string.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | function string.starts(str, begin) 5 | if not str then return false end 6 | return str:sub(1, begin:len()) == begin 7 | end 8 | 9 | function string.ends(str, tail) 10 | if not str then return false end 11 | return str:sub(-tail:len()) == tail 12 | end 13 | 14 | function string.split(str, sep, limit) 15 | sep, limit = sep or "%s", limit or math.huge 16 | local tbl, part, end_index, start_index, match = {}, 0, 0 17 | repeat 18 | part = part + 1 19 | start_index, end_index, match = str:find("([^" .. sep .. "]+)", end_index + 1) 20 | if match then table.insert(tbl, match) end 21 | until not start_index or part >= limit 22 | if end_index then tbl[part] = tbl[part] .. str:sub(end_index + 1) end 23 | return tbl 24 | end 25 | 26 | function string.ucfirst(str) 27 | return (str:gsub("^%l", string.upper)) 28 | end 29 | 30 | function string.trim(str) 31 | return str:match("^()%s*$") and "" or str:match("^%s*(.*%S)") 32 | end 33 | 34 | function string.blank(str) 35 | return str:trim() == "" 36 | end 37 | 38 | function string.not_blank(str) 39 | return not str:blank() 40 | end 41 | 42 | function string.tohex(str) 43 | return (str:gsub(".", function(char) 44 | return string.format("%02X", string.byte(char)) 45 | end)) 46 | end 47 | 48 | function string.urlencode(str) 49 | return (str:gsub("([^%w_%-.])", function(char) 50 | return string.format("%%%02X", string.byte(char)) 51 | end)) 52 | end 53 | 54 | function string.urldecode(str) 55 | return (str:gsub("%%(%x%x)", function(hex) 56 | return string.char(tonumber(hex, 16)) 57 | end):gsub("+", " ")) 58 | end 59 | 60 | function string.unicode_to_html(str) 61 | return (str:gsub("\u(%x%x%x%x)", "&#x%1")) 62 | end 63 | 64 | function string.has_unprintable(str) 65 | for char in str:gmatch(".") do 66 | local byte = char:byte() 67 | if byte ~= 10 and (byte < 32 or byte > 126) then 68 | return true 69 | end 70 | end 71 | return false 72 | end 73 | 74 | function string.ellipsize(str, max) 75 | if #str <= max then return str end 76 | return table.concat({ 77 | str:sub(1, max / 2), (#str - max), str:sub(-max / 2) 78 | }, " ... ") 79 | end 80 | 81 | function string.lower_pattern(pattern) 82 | return (pattern:gsub("(%%?)(.)", function(percent, letter) 83 | if percent ~= "" or not letter:match("%a") then return percent .. letter 84 | else return letter:lower() end 85 | end)) 86 | end 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # luci-app-rtorrent 2 | rTorrent frontend for OpenWrt's LuCI web interface 3 | 4 | :new: _2021-06-24: Complete rewrite from scratch ([0.2.x](https://github.com/wolandmaster/luci-app-rtorrent/tree/0.2.x))_ 5 | 6 | :new: _202?-??-??: Complete rewrite from scratch No.2: this time in JavaScript ([0.3.x](https://github.com/wolandmaster/luci-app-rtorrent/tree/0.3.x))_ 7 | 8 | ## Features 9 | - List all torrent downloads 10 | - Add new torrent by url/magnet uri/file 11 | - Stop/start/pause/hash/delete torrents 12 | - Categorize torrents by tags 13 | - Set priority per file 14 | - Enable/disable and add trackers to torrent 15 | - Detailed peer and chunk listing 16 | - Completely LuCI based interface 17 | - OpenWrt device independent (written in lua) 18 | - Opkg package manager support 19 | - RSS feed downloader (automatically download torrents that match the specified criteria) 20 | 21 | ## Screenshots 22 | [luci-app-rtorrent 0.2.0](https://github.com/wolandmaster/luci-app-rtorrent/wiki/Screenshots) 23 | 24 | ## Install instructions 25 | 26 | ### Install rtorrent-rpc 27 | ``` 28 | opkg update 29 | opkg install rtorrent-rpc screen 30 | ``` 31 | ### Create rTorrent config file 32 | 33 | #### Minimal _/root/.rtorrent.rc_ file (don't forget to update the paths!): 34 | ``` 35 | directory = /path/to/downloads/ 36 | session = /path/to/session/ 37 | 38 | scgi_port = 127.0.0.1:6000 39 | 40 | method.set_key = event.download.erased, on_erase, "branch=d.custom5=,\"execute2={rm,-rf,--,$d.base_path=}\"" 41 | 42 | schedule2 = rss_downloader, 60, 300, ((execute.throw, /usr/lib/lua/rss_downloader.lua, --uci)) 43 | ``` 44 | 45 | ### Create _/etc/init.d/rtorrent_ autostart script 46 | ``` 47 | #!/bin/sh /etc/rc.common 48 | 49 | START=99 50 | STOP=99 51 | 52 | start() { 53 | HOME=/root screen -dmS rtorrent nice -19 rtorrent 54 | } 55 | 56 | boot() { 57 | start "$@" 58 | } 59 | 60 | stop() { 61 | killall rtorrent 62 | } 63 | ``` 64 | 65 | ### Start rtorrent 66 | ``` 67 | chmod +x /etc/init.d/rtorrent 68 | /etc/init.d/rtorrent enable 69 | /etc/init.d/rtorrent start 70 | ``` 71 | 72 | ### Install luci-app-rtorrent 73 | ``` 74 | opkg install libustream-wolfssl 75 | wget -q https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest/e1a1ba8004c4220f -O /etc/opkg/keys/e1a1ba8004c4220f 76 | echo 'src/gz luci_app_rtorrent https://github.com/wolandmaster/luci-app-rtorrent/releases/download/latest' >> /etc/opkg.conf 77 | opkg update 78 | opkg install luci-app-rtorrent 79 | ``` 80 | 81 | ### Upgrade already installed version 82 | ``` 83 | opkg update 84 | opkg upgrade luci-app-rtorrent 85 | ``` 86 | 87 | ### References 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/usr/lib/lua/rtorrent.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local assert, ipairs, tostring, string, table, unpack = assert, ipairs, tostring, string, table, unpack 5 | 6 | local fs = require "nixio.fs" 7 | local socket = require "socket" 8 | local xmlrpc = require "xmlrpc" 9 | local scgi = require "xmlrpc.scgi" 10 | 11 | local rtorrent_config_file = "/root/.rtorrent.rc" 12 | 13 | module "rtorrent" 14 | 15 | local function format(results, commands) 16 | local formatted_results = {} 17 | for _, result in ipairs(results) do 18 | local formatted = {} 19 | for i, value in ipairs(result) do 20 | formatted[commands[i]:gsub("[%.=,]", "_")] = value 21 | end 22 | table.insert(formatted_results, formatted) 23 | end 24 | return formatted_results 25 | end 26 | 27 | function call(method, ...) 28 | local address, port = ("\n" .. tostring(fs.readfile(rtorrent_config_file))) 29 | :match("\n%s*scgi_port%s*=%s*([^:]+):(%d+)") 30 | assert(address, "\n\nError: scgi port not defined in your " .. rtorrent_config_file .. " config file!\n" 31 | .. 'Please add to it, e.g.: "scgi_port = 127.0.0.1:6000".\n') 32 | local ok, res = scgi.call(address, port, method, ...) 33 | if not ok and res == "socket connect failed" then 34 | assert(ok, "\n\nFailed to connect to rtorrent: rpc port not reachable" 35 | .. " on " .. address .. ":" .. port .. "!\nPossible reasons:\n" 36 | .. "- rtorrent is not running (ps w | grep [r]torrent)\n" 37 | .. "- not the rpc version of rtorrent is installed\n") 38 | end 39 | assert(ok, string.format("\n\nXML-RPC call failed: %s!\n", tostring(res))) 40 | return res 41 | end 42 | 43 | function multicall(method_type, hash, filter, ...) 44 | local commands = {} 45 | for i, command in ipairs({...}) do 46 | if not command:match("=") then command = command .. "=" end 47 | commands[i] = method_type .. command 48 | end 49 | local method = (method_type == "d.") and "multicall2" or "multicall" 50 | return format(call(method_type .. method, hash, filter, unpack(commands)), {...}) 51 | end 52 | 53 | function batchcall(method_type, hash, ...) 54 | local methods = {} 55 | for i, command in ipairs({...}) do 56 | local params = { hash } 57 | if command:match("=") then 58 | for arg in command:gsub(".*=", ""):gmatch("[^,]+") do 59 | table.insert(params, arg) 60 | end 61 | end 62 | table.insert(methods, { 63 | methodName = method_type .. command:gsub("=.*", ""), 64 | params = xmlrpc.newTypedValue(params, "array") 65 | }) 66 | end 67 | local results = {} 68 | for i, result in ipairs(call("system.multicall", xmlrpc.newTypedValue(methods, "array"))) do 69 | results[({...})[i]:gsub("[%.=,]", "_")] = (#result == 1) and result[1] or result 70 | end 71 | return results 72 | end 73 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/download.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local io = require "io" 5 | local nixio = require "nixio" 6 | local http = require "luci.http" 7 | local rtorrent = require "rtorrent" 8 | local common = require "luci.model.cbi.rtorrent.common" 9 | local array = require "luci.model.cbi.rtorrent.array" 10 | require "luci.model.cbi.rtorrent.string" 11 | 12 | module("luci.model.cbi.rtorrent.download", package.seeall) 13 | 14 | function permitted(path) 15 | for _, path_prefix in ipairs({ "/bin/", "/boot/", "/dev/", "/etc/", "/lib/", "/overlay/", "/proc/", 16 | "/root/", "/run/", "/sbin/", "/srv/", "/sys/", "/tmp/", "/usr/", "/var/", "/www/" }) do 17 | if path:starts(path_prefix) then 18 | return false 19 | end 20 | end 21 | return true 22 | end 23 | 24 | function download(hash, ...) 25 | local download_dir = rtorrent.call("d.directory", hash) 26 | local path = nixio.fs.realpath(array({...}):insert(1, download_dir):join("/")) 27 | if not permitted(path) then 28 | http.write("

Download from this location is not permitted!

") 29 | elseif nixio.fs.stat(path, "type") == "reg" then 30 | download_file(path) 31 | elseif nixio.fs.stat(path, "type") == "dir" then 32 | download_folder(path) 33 | else 34 | http.write("

No such file or directory: " .. path .. "

") 35 | end 36 | end 37 | 38 | function download_file(file) 39 | http.header("Content-Disposition", 'attachment; filename="%s"' % nixio.fs.basename(file)) 40 | http.header("Transfer-Encoding", "chunked") 41 | http.prepare_content("application/octet-stream") 42 | pump(nixio.open(file, "r")) 43 | end 44 | 45 | function download_folder(folder) 46 | if http.getenv("HTTP_USER_AGENT"):lower():find("linux") 47 | or nixio.fs.stat("/usr/bin/zip", "type") ~= "reg" then 48 | download_as_tar(folder) 49 | else 50 | download_as_zip(folder) 51 | end 52 | end 53 | 54 | function download_as_tar(folder) 55 | http.header("Content-Disposition", 'attachment; filename="%s.tar"' % nixio.fs.basename(folder)) 56 | http.header("Transfer-Encoding", "chunked") 57 | http.prepare_content("application/x-tar") 58 | pump(io.popen('tar -cf - -C "%s" .' % folder)) 59 | end 60 | 61 | function download_as_zip(folder) 62 | http.header("Content-Disposition", 'attachment; filename="%s.zip"' % nixio.fs.basename(folder)) 63 | http.header("Transfer-Encoding", "chunked") 64 | http.prepare_content("application/zip") 65 | pump(io.popen('cd "%s" && zip -0 -r - .' % folder)) 66 | end 67 | 68 | function pump(fh) 69 | local blocksize = 2^13 --8K 70 | repeat 71 | local chunk = fh:read(blocksize) 72 | http.write(string.format("%X", chunk and #chunk or 0) .. "\r\n" .. chunk .. "\r\n") 73 | until not chunk or chunk == "" 74 | fh:close() 75 | end 76 | -------------------------------------------------------------------------------- /src/usr/lib/lua/xmlrpc/scgi.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2003-2010 Kepler Project 2 | -- Copyright 2014-2021 Sandor Balazsi 3 | -- XML-RPC over SCGI. 4 | 5 | local error, tonumber, tostring, unpack = error, tonumber, tostring, unpack 6 | 7 | local socket = require "socket" 8 | local string = require "string" 9 | local xmlrpc = require "xmlrpc" 10 | 11 | module("xmlrpc.scgi") 12 | 13 | --------------------------------------------------------------------- 14 | -- Call a remote method. 15 | -- @param addr String with the address of the SCGI server. 16 | -- @param port The port of the SCGI server. 17 | -- @param method String with the name of the method to be called. 18 | -- @return Table with the response(could be a `fault' or a `params' 19 | -- XML-RPC element). 20 | --------------------------------------------------------------------- 21 | function call(addr, port, method, ...) 22 | local request_body = xmlrpc.clEncode(method, ...) 23 | local sock = socket.connect(addr, port) 24 | if sock == nil then 25 | return false, "socket connect failed" 26 | end 27 | sock:send(netstring(request_body)) 28 | local err, code, headers, body = receive(sock) 29 | 30 | if tonumber(code) == 200 then 31 | return xmlrpc.clDecode(body) 32 | else 33 | error(tostring(err or code)) 34 | end 35 | end 36 | 37 | --------------------------------------------------------------------- 38 | -- Encode message as netstring 39 | -- @param request_body String with the message 40 | -- @return String with the encoded message 41 | --------------------------------------------------------------------- 42 | function netstring(request_body) 43 | local null = "\0" 44 | local content_length = "CONTENT_LENGTH" .. null .. string.len(request_body) .. null 45 | local scgi_enable = "SCGI" .. null .. "1" .. null 46 | local request_method = "REQUEST_METHOD" .. null .. "POST" .. null 47 | local server_protocol = "SERVER_PROTOCOL" .. null .. "HTTP/1.1" .. null 48 | local header = content_length .. scgi_enable .. request_method .. server_protocol 49 | return string.len(header) .. ":" .. header .. "," .. request_body 50 | end 51 | 52 | --------------------------------------------------------------------- 53 | -- Receive and parse socket response 54 | -- @param sock Socket instance 55 | -- @return Headers, body and error codes 56 | --------------------------------------------------------------------- 57 | function receive(sock) 58 | local line, body, err 59 | local headers = {} 60 | 61 | line, err = sock:receive() 62 | if err then return err, "500" end 63 | while line ~= "" do 64 | local name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) 65 | if not(name and value) then return "malformed reponse header: " .. line, "500" end 66 | headers[string.lower(name)] = value 67 | 68 | line, err = sock:receive() 69 | if err then return err, "500" end 70 | end 71 | 72 | body = sock:receive(headers["content-length"]) 73 | local code = socket.skip(2, string.find(headers["status"], "^(%d%d%d)")) 74 | return err, code, headers, body 75 | end 76 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/settings/rss.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local nixio = require "nixio" 5 | local common = require "luci.model.cbi.rtorrent.common" 6 | local array = require "luci.model.cbi.rtorrent.array" 7 | require "luci.model.cbi.rtorrent.string" 8 | 9 | local map, feeds, name, url, enabled, logging, rss_logging, rss_logfile, rss_loglevel 10 | 11 | map = Map("rtorrent", "RSS Downloader Settings", common.rss_downloader_status()) 12 | map.template = "rtorrent/map" 13 | map.redirect = (nixio.getenv("HTTP_REFERER") or ""):match("/admin/rtorrent/rss/cfg") 14 | and nixio.getenv("HTTP_REFERER") or luci.http.formvalue("redirect") 15 | map:section(SimpleSection).hidden = { redirect = map.redirect } 16 | map.on_parse = function(self, ...) 17 | local names = array() 18 | for _, section_id in ipairs(feeds:cfgsections()) do 19 | if names:get(name:formvalue(section_id)) then 20 | names:get(name:formvalue(section_id)):insert(section_id) 21 | else names:set(name:formvalue(section_id), { section_id }) end 22 | end 23 | for _, sections in names:pairs() do 24 | if sections:size() > 1 then 25 | for _, section_id in sections:pairs() do 26 | name:add_error(section_id, "invalid", "Same name defined more than once!") 27 | end 28 | self.save = false 29 | end 30 | end 31 | end 32 | 33 | feeds = map:section(TypedSection, "rss-feed", "Feeds") 34 | feeds.template = "rtorrent/tblsection" 35 | feeds.addremove = true 36 | feeds.anonymous = true 37 | feeds.sortable = true 38 | 39 | name = feeds:option(Value, "name", "Name") 40 | name.rmempty = false 41 | name.width = "35%" 42 | name.validate = function(self, value, section) 43 | if not value or value:blank() then return nil, "Missing RSS feed name!" 44 | elseif value:match("|") then return nil, "The name cannot contain pipe characters!" end 45 | return value 46 | end 47 | 48 | url = feeds:option(Value, "url", "RSS Feed URL") 49 | url.rmempty = false 50 | url.width = "64%" 51 | url.validate = function(self, value, section) 52 | local content, err = common.download(value:trim()) 53 | if not content then 54 | return nil, "Not able to download RSS feed: " .. err .. "!" 55 | end 56 | return value 57 | end 58 | 59 | enabled = feeds:option(Flag, "enabled", "Enabled") 60 | enabled.rmempty = false 61 | enabled.width = "1%" 62 | enabled.classes = { "center" } 63 | 64 | logging = map:section(NamedSection, "logging", "rss", "Logging") 65 | 66 | rss_logging = logging:option(Flag, "rss_logging", "Enable RSS logging") 67 | 68 | rss_logfile = logging:option(Value, "rss_logfile", "RSS Downloader logfile") 69 | rss_logfile:depends("rss_logging", 1) 70 | rss_logfile.validate = function(self, value, section) 71 | if not value or value:blank() then return nil, "Missing RSS Downloader logfile!" end 72 | local parent_folder = nixio.fs.dirname(value) 73 | if parent_folder == "." or nixio.fs.stat(parent_folder, "type") ~= "dir" then 74 | return nil, "Wrong filename, please use absolute path!" 75 | end 76 | return value 77 | end 78 | 79 | rss_loglevel = logging:option(ListValue, "rss_loglevel", "RSS Downloader loglevel") 80 | rss_loglevel:depends("rss_logging", 1) 81 | rss_loglevel.default = "INFO" 82 | rss_loglevel:value("TRACE") 83 | rss_loglevel:value("DEBUG") 84 | rss_loglevel:value("INFO") 85 | rss_loglevel:value("WARN") 86 | rss_loglevel:value("ERROR") 87 | rss_loglevel:value("FATAL") 88 | 89 | return map 90 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/rss-rule.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local rtorrent = require "rtorrent" 5 | local util = require "luci.util" 6 | local build_url = require "luci.dispatcher".build_url 7 | local common = require "luci.model.cbi.rtorrent.common" 8 | require "luci.model.cbi.rtorrent.string" 9 | 10 | local section = arg[1] 11 | 12 | local map, rss_rule, enabled, name, match, exclude, minsize, maxsize, feed, tags, destdir, autostart 13 | 14 | map = Map("rtorrent", "RSS Downloader Rule", common.rss_downloader_status()) 15 | map.template = "rtorrent/map" 16 | map.redirect = build_url("admin", "rtorrent", "rss") 17 | map.on_parse = function(self, ...) 18 | if util.instanceof(feed, DummyValue) then 19 | feed:add_error(section, "invalid", "No feed found! Please add one.") 20 | self.save = false 21 | elseif not feed:formvalue(section) then 22 | feed:add_error(section, "invalid", "At least one feed must be selected!") 23 | self.save = false 24 | end 25 | end 26 | 27 | if not map:get(section) then luci.http.redirect(map.redirect) end 28 | 29 | rss_rule = map:section(NamedSection, section, map:get(section)[".type"]) 30 | rss_rule.anonymous = true 31 | 32 | enabled = rss_rule:option(Flag, "enabled", "Enabled") 33 | enabled.rmempty = false 34 | 35 | name = rss_rule:option(Value, "name", "Name") 36 | name.rmempty = false 37 | 38 | match = rss_rule:option(TextValue, "match", "Match", 39 | 'The torrent name must match this (case-insensitive) lua pattern. ' 40 | % "https://www.lua.org/pil/20.2.html" .. "Use .* to download everything from the feed." ) 41 | match.template = "rtorrent/tvalue" 42 | match.rmempty = false 43 | match.rows = 1 44 | 45 | exclude = rss_rule:option(TextValue, "exclude", "Exclude", 46 | 'Exclude torrents that names match this (case-insensitive) lua pattern.' 47 | % "https://www.lua.org/pil/20.2.html") 48 | exclude.rows = 1 49 | 50 | minsize = rss_rule:option(Value, "minsize", "Min size (MiB):") 51 | 52 | maxsize = rss_rule:option(Value, "maxsize", "Max size (MiB):") 53 | 54 | local feeds = common.uci_sections(map.config, "rss-feed") 55 | if feeds:empty() then 56 | feed = rss_rule:option(DummyValue, "feed", "Feed") 57 | feed.template = "rtorrent/dvalue" 58 | feed.rawhtml = true 59 | feed.value = '
RSS feeds not yet added. You can do it here.
' 60 | % build_url("admin", "rtorrent", "settings", "rss") 61 | else 62 | feed = rss_rule:option(DropDown, "feed", "Feed", 'You can manage RSS feeds here.' 63 | % build_url("admin", "rtorrent", "settings", "rss")) 64 | -- TODO: warning on disabled feed 65 | feed.template = "rtorrent/dropdown" 66 | feed.multiple = true 67 | feed.display = 3 68 | feed.delimiter = "|" 69 | feed.cfgvalue = function(self, section) 70 | local value = AbstractValue.cfgvalue(self, section) 71 | return type(value) == "string" and value:split(feed.delimiter) or value 72 | end 73 | feeds:foreach(function(f) feed:value(f:get("name"), f:get("name") 74 | .. (f:get("enabled") == "0" and " (disabled)" or "")) end) 75 | end 76 | 77 | tags = rss_rule:option(Value, "tags", "Add tags") 78 | -- tags.default = "" 79 | 80 | destdir = rss_rule:option(Value, "destdir", "Download directory") 81 | destdir.default = rtorrent.call("directory.default") 82 | -- destdir.datatype = "directory" 83 | destdir.rmempty = false 84 | 85 | autostart = rss_rule:option(Flag, "autostart", "Start download") 86 | autostart.default = "1" 87 | autostart.rmempty = false 88 | 89 | return map 90 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/torrent/info.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local nixio = require "nixio" 5 | local rtorrent = require "rtorrent" 6 | local build_url = require "luci.dispatcher".build_url 7 | local common = require "luci.model.cbi.rtorrent.common" 8 | local array = require "luci.model.cbi.rtorrent.array" 9 | require "luci.model.cbi.rtorrent.string" 10 | 11 | local hash = unpack(arg) 12 | local form, infohash, url, started, finished, status, tags, comment 13 | 14 | local torrent = array(rtorrent.batchcall("d.", hash, 15 | "name", "timestamp.started", "timestamp.finished", "message", 16 | "custom1", "custom=icon", "custom=url", "custom=comment")) 17 | 18 | _G.redirect = build_url("admin", "rtorrent", "main", unpack(common.get_cookie("rtorrent-main", {}))) 19 | form = SimpleForm("rtorrent", torrent:get("name")) 20 | form.template = "rtorrent/simpleform" 21 | form.all_tabs = array():append("info", "files", "trackers", "peers", "chunks"):get() 22 | form.tab_url_postfix = function(tab) 23 | local filters = array(common.get_cookie("rtorrent-" .. tab, {})) 24 | return filters:get(1) == hash and filters:join("/") or hash 25 | end 26 | form.handle = function(self, state, data) 27 | if state == FORM_VALID then 28 | luci.http.redirect(nixio.getenv("REQUEST_URI")) 29 | end 30 | return true 31 | end 32 | 33 | infohash = form:field(DummyValue, "hash", "Hash") 34 | infohash.template = "rtorrent/dvalue" 35 | infohash.rawhtml = true 36 | infohash.value = '
%s
' % hash 37 | 38 | local torrent_url = torrent:get("custom_url"):urldecode() 39 | url = form:field(DummyValue, "url", "Torrent URL") 40 | url.template = "rtorrent/dvalue" 41 | url.rawhtml = true 42 | url.value = '
%s
' % (torrent_url:blank() 43 | and 'Unknown, added by an uploaded torrent file or magnet URI.' 44 | or '%s' % { torrent_url , torrent_url }) 45 | 46 | started = form:field(DummyValue, "started", "Download started") 47 | started.template = "rtorrent/dvalue" 48 | started.rawhtml = true 49 | started.value = '
%s
' % (torrent:get("timestamp_started") == 0 50 | and "not yet started" 51 | or os.date("!%Y-%m-%d %H:%M:%S", torrent:get("timestamp_started"))) 52 | 53 | finished = form:field(DummyValue, "finished", "Download finished") 54 | finished.template = "rtorrent/dvalue" 55 | finished.rawhtml = true 56 | finished.value = '
%s
' % (torrent:get("timestamp_finished") == 0 57 | and "not yet finished" 58 | or os.date("!%Y-%m-%d %H:%M:%S", torrent:get("timestamp_finished"))) 59 | 60 | status = form:field(DummyValue, "status", "Status") 61 | status.template = "rtorrent/dvalue" 62 | status.rawhtml = true 63 | status.value = '
%s
' % torrent:get("message") 64 | 65 | tags = form:field(Value, "tags", "Tags") 66 | tags.cfgvalue = function(self, section) return torrent:get("custom1") end 67 | tags.write = function(self, section, value) 68 | rtorrent.call("d.custom1.set", hash, value) 69 | end 70 | tags.remove = function(self, section) 71 | if self:cfgvalue(section) ~= "" then 72 | self:write(section, "") 73 | end 74 | end 75 | 76 | comment = form:field(TextValue, "comment", "Comment") 77 | comment.rows = 5 78 | comment.cfgvalue = function(self, section) 79 | return torrent:get("custom_comment"):urldecode() 80 | end 81 | comment.write = function(self, section, value) 82 | rtorrent.call("d.custom.set", hash, "comment", value:urlencode()) 83 | end 84 | comment.remove = function(self, section) 85 | if self:cfgvalue(section) ~= "" then 86 | self:write(section, "") 87 | end 88 | end 89 | 90 | return form 91 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/view/rtorrent/tblsection.htm: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 | <%+cbi/tblsection%> 25 |
26 | <% if self.pages and self.pages ~= "" then %> 27 | 28 | <% end %> 29 | 94 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/torrent/chunks.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local nixio = require "nixio" 5 | local rtorrent = require "rtorrent" 6 | local build_url = require "luci.dispatcher".build_url 7 | local common = require "luci.model.cbi.rtorrent.common" 8 | local array = require "luci.model.cbi.rtorrent.array" 9 | require "luci.model.cbi.rtorrent.string" 10 | 11 | local hash, page = unpack(arg) 12 | common.set_cookie("rtorrent-chunks", { hash, page }) 13 | 14 | page = page and tonumber(page) or 1 15 | 16 | local torrent = array(rtorrent.batchcall("d.", hash, 17 | "name", "bitfield", "chunks_seen", "size_chunks", "chunk_size", "completed_chunks", "wanted_chunks")) 18 | local bitfield, chunks, line, group, pos = array(), array(), (page - 1) * 10 + 1, 1, 1 19 | torrent:get("bitfield"):gsub("(%x%x)", function(hex) bitfield:insert(tonumber(hex, 16)) end) 20 | torrent:get("chunks_seen"):sub((line - 1) * 200 + 1, (line + 9) * 200):gsub("%x(%x)", function(seen) 21 | if pos == 1 then 22 | if group == 1 then 23 | chunks:set(line, array():set("offset", "%s" % ((line - 1) * 100))) 24 | end 25 | chunks:get(line):set(group, "") 26 | end 27 | local chunk_index = (line - 1) * 100 + (group - 1) * 10 + pos - 1 28 | local chunk_status = bitfield:get(math.floor(chunk_index / 8) + 1) 29 | local chunk_mask = nixio.bit.lshift(1, 7 - chunk_index % 8) 30 | local chunk_done = nixio.bit.band(chunk_status, chunk_mask) == chunk_mask 31 | chunks:get(line):set(group, chunks:get(line):get(group) 32 | .. (chunk_done and '%s' % seen or seen)) 33 | pos = pos + 1 34 | if pos > 10 then pos, group = 1, group + 1 end 35 | if group > 10 then group, line = 1, line + 1 end 36 | end) 37 | 38 | local form, summary, list, offset 39 | local chunks_count, chunk_size, completed_chunks, wanted_chunks, excluded_chunks, download_done 40 | 41 | _G.redirect = build_url("admin", "rtorrent", "main", unpack(common.get_cookie("rtorrent-main", {}))) 42 | form = SimpleForm("rtorrent", torrent:get("name")) 43 | form.template = "rtorrent/simpleform" 44 | form.submit = false 45 | form.reset = false 46 | form.all_tabs = array():append("info", "files", "trackers", "peers", "chunks"):get() 47 | form.tab_url_postfix = function(tab) 48 | local filters = (tab == "chunks") and array(arg) or array(common.get_cookie("rtorrent-" .. tab, {})) 49 | return filters:get(1) == hash and filters:join("/") or hash 50 | end 51 | form.handle = function(self, state, data) 52 | if state == FORM_VALID then luci.http.redirect(nixio.getenv("REQUEST_URI")) end 53 | return true 54 | end 55 | 56 | summary = form:section(Table, { [1] = { 57 | ["chunks_count"] = tostring(torrent:get("size_chunks")), 58 | ["chunk_size"] = common.human_size(torrent:get("chunk_size")), 59 | ["completed_chunks"] = tostring(torrent:get("completed_chunks")), 60 | ["wanted_chunks"] = tostring(torrent:get("wanted_chunks")), 61 | ["excluded_chunks"] = tostring(torrent:get("size_chunks") 62 | - torrent:get("completed_chunks") - torrent:get("wanted_chunks")), 63 | ["download_done"] = "%.2f%%" % math.min(100.0 * torrent:get("completed_chunks") 64 | / (torrent:get("completed_chunks") + torrent:get("wanted_chunks")), 100) 65 | } }) 66 | summary.template = "rtorrent/tblsection" 67 | summary.name = "rtorrent-chunks-summary" 68 | 69 | chunks_count = summary:option(DummyValue, "chunks_count", "Chunks count") 70 | chunks_count.classes = { "center" } 71 | 72 | chunk_size = summary:option(DummyValue, "chunk_size", "Chunk size") 73 | chunk_size.classes = { "center" } 74 | 75 | completed_chunks = summary:option(DummyValue, "completed_chunks", "Completed chunks") 76 | completed_chunks.classes = { "center" } 77 | 78 | wanted_chunks = summary:option(DummyValue, "wanted_chunks", "Wanted chunks") 79 | wanted_chunks.classes = { "center" } 80 | 81 | excluded_chunks = summary:option(DummyValue, "excluded_chunks", 82 | 'Excluded chunks') 83 | excluded_chunks.classes = { "center" } 84 | 85 | download_done = summary:option(DummyValue, "download_done", "Download done") 86 | download_done.classes = { "center" } 87 | 88 | list = form:section(Table, chunks:get()) 89 | list.template = "rtorrent/tblsection" 90 | list.name = "rtorrent-chunks" 91 | list.pages = common.pagination(math.floor(torrent:get("chunks_seen"):len() / 200) + 1, page, 92 | common.pagination_link, build_url("admin", "rtorrent", "torrent", "chunks", hash)):join() 93 | 94 | offset = list:option(DummyValue, "offset", "Offset") 95 | offset.rawhtml = true 96 | offset.width = "8%" 97 | offset.classes = { "right" } 98 | 99 | for group = 1, 10 do 100 | local column = list:option(DummyValue, group) 101 | column.rawhtml = true 102 | if group ~= 10 then column.width = "1%" end 103 | end 104 | 105 | return form 106 | -------------------------------------------------------------------------------- /src/usr/lib/lua/bencode.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2009, 2010, 2011, 2012 by Moritz Wilhelmy 2 | -- Copyright (c) 2009 by Kristofer Karlsson 3 | -- Public domain lua-module for handling bittorrent-bencoded data. 4 | -- This module includes both a recursive decoder and a recursive encoder. 5 | 6 | local sort, concat, insert = table.sort, table.concat, table.insert 7 | local pairs, ipairs, type, tonumber = pairs, ipairs, type, tonumber 8 | local sub, find = string.sub, string.find 9 | 10 | local M = {} 11 | 12 | -- helpers 13 | 14 | local function islist(t) 15 | local n = #t 16 | for k, v in pairs(t) do 17 | if type(k) ~= "number" 18 | or k % 1 ~= 0 -- integer? 19 | or k < 1 20 | or k > n 21 | then 22 | return false 23 | end 24 | end 25 | for i = 1, n do 26 | if t[i] == nil then 27 | return false 28 | end 29 | end 30 | return true 31 | end 32 | 33 | -- encoder functions 34 | 35 | local encode_rec -- encode_list/dict and encode_rec are mutually recursive... 36 | 37 | local function encode_list(t, x) 38 | 39 | insert(t, "l") 40 | 41 | for _,v in ipairs(x) do 42 | local err,ev = encode_rec(t, v); if err then return err,ev end 43 | end 44 | 45 | insert(t, "e") 46 | end 47 | 48 | local function encode_dict(t, x) 49 | insert(t, "d") 50 | -- bittorrent requires the keys to be sorted. 51 | local sortedkeys = {} 52 | for k, v in pairs(x) do 53 | if type(k) ~= "string" then 54 | return "bencoding requires dictionary keys to be strings", k 55 | end 56 | insert(sortedkeys, k) 57 | end 58 | sort(sortedkeys) 59 | 60 | for k, v in ipairs(sortedkeys) do 61 | local err,ev = encode_rec(t, v); if err then return err,ev end 62 | err,ev = encode_rec(t, x[v]); if err then return err,ev end 63 | end 64 | insert(t, "e") 65 | end 66 | 67 | local function encode_int(t, x) 68 | 69 | if x % 1 ~= 0 then return "number is not an integer", x end 70 | insert(t, "i" ) 71 | insert(t, x ) 72 | insert(t, "e" ) 73 | end 74 | 75 | local function encode_str(t, x) 76 | 77 | insert(t, #x ) 78 | insert(t, ":" ) 79 | insert(t, x ) 80 | end 81 | 82 | encode_rec = function(t, x) 83 | 84 | local typx = type(x) 85 | if typx == "string" then return encode_str (t, x) 86 | elseif typx == "number" then return encode_int (t, x) 87 | elseif typx == "table" then 88 | 89 | if islist(x) then return encode_list (t, x) 90 | else return encode_dict (t, x) 91 | end 92 | else 93 | return "type cannot be converted to an acceptable type for bencoding", typx 94 | end 95 | end 96 | 97 | -- call recursive bencoder function with empty table, stringify that table. 98 | -- this is the only encode* function visible to module users. 99 | M.encode = function (x) 100 | 101 | local t = {} 102 | local err, val = encode_rec(t,x) 103 | if not err then 104 | return concat(t) 105 | else 106 | return nil, err, val 107 | end 108 | end 109 | 110 | -- decoder functions 111 | 112 | local function decode_integer(s, index) 113 | local a, b, int = find(s, "^(%-?%d+)e", index) 114 | if not int then return nil, "not a number", nil end 115 | int = tonumber(int) 116 | if not int then return nil, "not a number", int end 117 | return int, b + 1 118 | end 119 | 120 | local function decode_list(s, index) 121 | local t = {} 122 | while sub(s, index, index) ~= "e" do 123 | local obj, ev 124 | obj, index, ev = M.decode(s, index) 125 | if not obj then return obj, index, ev end 126 | insert(t, obj) 127 | end 128 | index = index + 1 129 | return t, index 130 | end 131 | 132 | local function decode_dictionary(s, index) 133 | local t = {} 134 | while sub(s, index, index) ~= "e" do 135 | local obj1, obj2, ev 136 | 137 | obj1, index, ev = M.decode(s, index) 138 | if not obj1 then return obj1, index, ev end 139 | 140 | obj2, index, ev = M.decode(s, index) 141 | if not obj2 then return obj2, index, ev end 142 | 143 | t[obj1] = obj2 144 | end 145 | index = index + 1 146 | return t, index 147 | end 148 | 149 | local function decode_string(s, index) 150 | local a, b, len = find(s, "^([0-9]+):", index) 151 | if not len then return nil, "not a length", len end 152 | index = b + 1 153 | 154 | local v = sub(s, index, index + len - 1) 155 | if #v < tonumber(len) then return nil, "truncated string at end of input", v end 156 | index = index + len 157 | return v, index 158 | end 159 | 160 | 161 | M.decode = function (s, index) 162 | if not s then return nil, "no data", nil end 163 | index = index or 1 164 | local t = sub(s, index, index) 165 | if not t then return nil, "truncation error", nil end 166 | 167 | if t == "i" then 168 | return decode_integer(s, index + 1) 169 | elseif t == "l" then 170 | return decode_list(s, index + 1) 171 | elseif t == "d" then 172 | return decode_dictionary(s, index + 1) 173 | elseif t >= '0' and t <= '9' then 174 | return decode_string(s, index) 175 | else 176 | return nil, "invalid type", t 177 | end 178 | end 179 | 180 | return M 181 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/add.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local nixio = require "nixio" 5 | local bencode = require "bencode" 6 | local xmlrpc = require "xmlrpc" 7 | local rtorrent = require "rtorrent" 8 | local datatypes = require "luci.cbi.datatypes" 9 | local common = require "luci.model.cbi.rtorrent.common" 10 | local array = require "luci.model.cbi.rtorrent.array" 11 | require "luci.model.cbi.rtorrent.string" 12 | 13 | local torrents = array() 14 | local uploads = "/etc/luci-uploads/rtorrent" 15 | local form, uri, file, dir, tags, start 16 | common.remove_cookie("rtorrent-notifications") 17 | 18 | form = SimpleForm("rtorrent", "Add Torrent") 19 | form.template = "rtorrent/simpleform" 20 | form.submit = "Add" 21 | form.notifications = common.get_cookie("rtorrent-notifications", {}) 22 | form.handle = function(self, state, data) 23 | if state ~= FORM_NODATA and torrents:empty() then 24 | uri:add_error(1, "missing") 25 | file:add_error(1, "missing", "Either a torrent URL / magnet URI or " 26 | .. "an uploaded torrent file must be provided!") 27 | return true, FORM_INVALID 28 | end 29 | if state == FORM_VALID then 30 | for _, torrent in torrents:pairs() do 31 | torrent:set("start", data.start):set("directory", data.dir):set("tags", data.tags) 32 | common.add_to_rtorrent(torrent) 33 | table.insert(form.notifications, "Added %s" % torrent:get("name")) 34 | end 35 | file:remove(1) 36 | common.set_cookie("rtorrent-notifications", form.notifications) 37 | luci.http.redirect(nixio.getenv("REQUEST_URI")) 38 | end 39 | return true 40 | end 41 | 42 | uri = form:field(TextValue, "uri", "Torrent URL(s)
or magnet URI(s)", 43 | "All torrent URL and magnet URI should be in a separate line.") 44 | uri.template = "rtorrent/tvalue" 45 | uri.rows = 3 46 | uri.validate = function(self, value, section) 47 | local errors = array() 48 | for _, line in ipairs(value:split("\r\n")) do 49 | if "magnet:" == line:trim():sub(1, 7) then 50 | local magnet, err = common.parse_magnet(line:trim()) 51 | if not magnet then errors:insert(err) 52 | else 53 | local content, err = bencode.encode({ ["magnet-uri"] = line:trim() }) 54 | if not content then errors:insert("Failed to encode torrent: " .. err .. "!") 55 | else 56 | torrents:insert({ 57 | ["name"] = magnet:get("dn") 58 | and magnet:get("dn"):get(1) or line:trim(), 59 | ["data"] = nixio.bin.b64encode(content), 60 | ["icon"] = common.tracker_icon(common.extract_urls(magnet)) 61 | }) 62 | end 63 | end 64 | elseif "file://" == line:trim():sub(1, 7) then 65 | local filename = line:trim():sub(8) 66 | if not filename:starts("/") then filename = uploads .. "/" .. filename end 67 | local result, err = file.validate(self, filename) 68 | if not result then errors:insert(err) end 69 | elseif "http://" == line:trim():sub(1, 7) or "https://" == line:trim():sub(1, 8) then 70 | local content, err = common.download(line:trim()) 71 | if not content then 72 | errors:insert("Failed to download torrent: " .. err .. "!") 73 | else 74 | local data, err = bencode.decode(content) 75 | if not data then 76 | errors:insert("Failed to parse torrent: " .. err .. "!") 77 | else 78 | -- TODO: extract comment from torrent file 79 | torrents:insert({ 80 | ["name"] = data.info.name, 81 | ["data"] = nixio.bin.b64encode(content), 82 | ["icon"] = common.tracker_icon(array({ line:trim(), 83 | unpack(common.extract_urls(array(data)):get()) })), 84 | ["url"] = line:trim() 85 | }) 86 | end 87 | end 88 | else 89 | errors:insert("Not supported URL/URI: \"%s\"! " % line:trim() 90 | .. "Supported schemes: \"http://\", \"https://\", \"file://\", \"magnet:\".") 91 | end 92 | end 93 | if not errors:empty() then 94 | for i, err in errors:pairs() do 95 | if not errors:last(i) then self:add_error(section, err) end 96 | end 97 | return nil, errors:last() 98 | end 99 | return value 100 | end 101 | 102 | file = form:field(FileUpload, "file", "Upload torrent file") 103 | file.template = "rtorrent/upload" 104 | file.root_directory = uploads 105 | file.unsafeupload = true 106 | file.validate = function(self, value, section) 107 | if not datatypes.file(value) then 108 | return nil, "File '" .. value .. "' does not exists!" 109 | elseif not nixio.fs.access(value, "r") then 110 | return nil, "File '" .. value .. "' read permission denied!" 111 | end 112 | local content = nixio.fs.readfile(value) 113 | local data, err = bencode.decode(content) 114 | if not data then 115 | return nil, "Failed to parse torrent file '" .. value .. "': " .. err .. "!" 116 | end 117 | -- TODO: extract comment from torrent file 118 | torrents:insert({ 119 | ["name"] = data.info.name, 120 | ["data"] = nixio.bin.b64encode(content), 121 | ["icon"] = common.tracker_icon(common.extract_urls(array(data))) 122 | }) 123 | return value 124 | end 125 | 126 | dir = form:field(Value, "dir", "Download directory") 127 | dir.default = rtorrent.call("directory.default") 128 | dir.rmempty = false 129 | dir.validate = function(self, value, section) 130 | if not value then 131 | return nil, "Download directory must be provided!" 132 | elseif not datatypes.directory(value) then 133 | return nil, "Directory '" .. value .. "' does not exists!" 134 | elseif not nixio.fs.access(value, "w") then 135 | return nil, "Directory '" .. value .. "' write permission denied!" 136 | end 137 | return value 138 | end 139 | 140 | tags = form:field(Value, "tags", "Tags") 141 | 142 | start = form:field(Flag, "start", "Start now") 143 | start.default = "1" 144 | start.rmempty = false 145 | 146 | return form 147 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/array.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local array = {} 5 | 6 | setmetatable(array, { 7 | __call = function(self, init) 8 | local instance = setmetatable({}, { 9 | __type = "array" 10 | }) 11 | for name, func in pairs(self) do 12 | instance[name] = func 13 | end 14 | instance.table = init or {} 15 | return instance 16 | end 17 | }) 18 | 19 | function array.get(self, key) 20 | if key then 21 | if type(self.table[key]) == "table" 22 | and (not getmetatable(self.table[key]) or 23 | getmetatable(self.table[key]).__type ~= "array") then 24 | self.table[key] = array(self.table[key]) 25 | end 26 | return self.table[key] 27 | else 28 | local native_table = {} 29 | for key, value in pairs(self.table) do 30 | if type(value) == "table" and getmetatable(value) 31 | and getmetatable(value).__type == "array" then 32 | native_table[key] = value:get() 33 | else native_table[key] = value end 34 | end 35 | return native_table 36 | end 37 | end 38 | 39 | function array.set(self, key, value, ...) 40 | if type(value) ~= "function" then self.table[key] = value 41 | else self.table[key] = value(key, self, ...) end 42 | return self 43 | end 44 | 45 | function array.insert(self, ...) -- array.insert(self, [pos,] value) 46 | local args = {...} 47 | if #args == 0 then 48 | -- nothing to do 49 | elseif #args == 1 or type(args[1]) == "function" then 50 | if type(args[1]) ~= "function" then table.insert(self.table, args[1]) 51 | else table.insert(self.table, args[1](self, unpack(args, 2))) end 52 | elseif #args == 2 or type(args[2]) == "function" then 53 | if type(args[2]) ~= "function" then table.insert(self.table, args[1], args[2]) 54 | else table.insert(self.table, args[1], args[2](self, unpack(args, 3))) end 55 | else assert(false, "invalid arg count to 'insert'") end 56 | return self 57 | end 58 | 59 | function array.append(self, ...) 60 | for _, value in ipairs({...}) do 61 | self:insert(value) 62 | end 63 | return self 64 | end 65 | 66 | function array.increment(self, key, delta) 67 | return self:set(key, (self.table[key] or 0) + (delta or 1)) 68 | end 69 | 70 | function array.decrement(self, key, delta) 71 | return self:set(key, (self.table[key] or 0) - (delta or 1)) 72 | end 73 | 74 | function array.remove(self, pos) 75 | table.remove(self.table, pos) 76 | return self 77 | end 78 | 79 | function array.pairs(self) 80 | local function iterator(table, key) 81 | local next_key = next(table, key) 82 | return next_key, self:get(next_key) 83 | end 84 | return iterator, self.table, nil 85 | end 86 | 87 | function array.size(self) 88 | return table.getn(self.table) 89 | end 90 | 91 | function array.empty(self) 92 | return next(self.table) == nil 93 | end 94 | 95 | function array.first(self, key) 96 | local first_key, first_value = next(self.table) 97 | if key then return key == first_key 98 | else return first_value end 99 | end 100 | 101 | function array.last(self, key) 102 | local next_key, next_value = next(self.table, key) 103 | if key then return not next_value 104 | else 105 | local last_value = nil 106 | for _, value in self:pairs() do last_value = value end 107 | return last_value 108 | end 109 | end 110 | 111 | function array.clone(self) 112 | return array(self:get()) 113 | end 114 | 115 | function array.keys(self) 116 | local keys = array() 117 | for key, _ in self:pairs() do keys:insert(key) end 118 | return keys 119 | end 120 | 121 | function array.values(self) 122 | local values = array() 123 | for _, value in self:pairs() do values:insert(value) end 124 | return values 125 | end 126 | 127 | function array.join(self, separator) 128 | return table.concat(self.table, separator) 129 | end 130 | 131 | function array.sort(self, comparator, ...) 132 | local args = {...} 133 | if not comparator then 134 | table.sort(self.table) 135 | elseif type(comparator) == "function" then 136 | table.sort(self.table, function(lhs, rhs) 137 | return comparator(lhs, rhs, unpack(args)) 138 | end) 139 | else 140 | local order = args[1] 141 | table.sort(self.table, function(lhs, rhs) 142 | if order == "asc" then 143 | return lhs:get(comparator) < rhs:get(comparator) 144 | elseif order == "desc" then 145 | return lhs:get(comparator) > rhs:get(comparator) 146 | else assert(false, "invalid sort order: " .. tostring(order)) end 147 | end) 148 | end 149 | return self 150 | end 151 | 152 | function array.contains(self, query) 153 | assert(query, "invalid query to 'contains'") 154 | for key, value in self:pairs() do 155 | if value == query then return key end 156 | end 157 | return false 158 | end 159 | 160 | function array.foreach(self, func, ...) 161 | assert(type(func) == "function", "invalid func to 'foreach'") 162 | for key, value in self:pairs() do 163 | func(value, key, self, ...) 164 | end 165 | return self 166 | end 167 | 168 | function array.map(self, func, ...) 169 | assert(type(func) == "function", "invalid func to 'map'") 170 | local out = array() 171 | for key, value in self:pairs() do 172 | local out_value, out_key = func(value, key, self, ...) 173 | out:set(out_key or key, out_value) 174 | end 175 | return out 176 | end 177 | 178 | function array.filter(self, func, ...) 179 | assert(type(func) == "function", "invalid func to 'filter'") 180 | local out = array() 181 | for key, value in self:pairs() do 182 | if func(value, key, self, ...) then 183 | if type(key) == "number" then out:insert(value) 184 | else out:set(key, value) end 185 | end 186 | end 187 | return out 188 | end 189 | 190 | function array.limit(self, count, start) 191 | start = start or 0 192 | local current = 0 193 | return self:filter(function() 194 | current = current + 1 195 | return current > start and current <= start + count 196 | end) 197 | end 198 | 199 | function array.unique(self) 200 | local out = array() 201 | local seen = array() 202 | for key, value in self:pairs() do 203 | if type(key) == "number" then 204 | if not seen:contains(value) then 205 | seen:insert(value) 206 | out:insert(value) 207 | end 208 | else out:set(key, value) end 209 | end 210 | return out 211 | end 212 | 213 | function array.traverse(self, func, depth) 214 | assert(type(func) == "function", "invalid func to 'traverse'") 215 | depth = depth or 0 216 | for key, value in self:pairs() do 217 | func(value, key, self, depth) 218 | if type(value) == "table" then 219 | self.traverse(value, func, depth + 1) 220 | end 221 | end 222 | end 223 | 224 | return array 225 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/torrent/trackers.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local rtorrent = require "rtorrent" 5 | local build_url = require "luci.dispatcher".build_url 6 | local common = require "luci.model.cbi.rtorrent.common" 7 | local array = require "luci.model.cbi.rtorrent.array" 8 | require "luci.model.cbi.rtorrent.string" 9 | 10 | local compute, format, hash, sort, page = {}, {}, unpack(arg) 11 | common.set_cookie("rtorrent-trackers", { hash, sort, page }) 12 | common.remove_cookie("rtorrent-notifications") 13 | 14 | sort = sort or "status-asc" 15 | page = page and tonumber(page) or 1 16 | local sort_column, sort_order = unpack(sort:split("-")) 17 | 18 | function compute_total(tracker, index, trackers, total) 19 | total:increment("count") 20 | total:increment("latest_new_peers", tracker:get("latest_new_peers")) 21 | total:increment("latest_sum_peers", tracker:get("latest_sum_peers")) 22 | total:increment("seeds", tracker:get("seeds")) 23 | total:increment("leeches", tracker:get("leeches")) 24 | total:increment("downloaded", tracker:get("downloaded")) 25 | total:get("urls"):insert(tracker:get("url")) 26 | if trackers:last(index) then 27 | format_values(total:set(".total_row", true) 28 | :set("url", "TOTAL: %d pcs." % total:get("count")) 29 | :set("peers", compute.peers)) 30 | end 31 | end 32 | 33 | function compute_values(tracker, index, trackers, ...) 34 | for _, key in ipairs({ "index", "icon", "status", "peers", 35 | "seeds", "leeches", "downloaded", "updated", "enabled" }) do 36 | tracker:set(key, compute[key], index, trackers, ...) 37 | end 38 | end 39 | 40 | function compute.index(key, tracker, index) return index - 1 end 41 | function compute.seeds(key, tracker) return tracker:get("scrape_complete") end 42 | function compute.leeches(key, tracker) return tracker:get("scrape_incomplete") end 43 | function compute.downloaded(key, tracker) return tracker:get("scrape_downloaded") end 44 | function compute.updated(key, tracker) return tracker:get("scrape_time_last") end 45 | function compute.enabled(key, tracker) return tracker:get("is_enabled") end 46 | 47 | function compute.icon(key, tracker) 48 | return common.tracker_icon(array({ tracker:get("url") })) 49 | end 50 | 51 | function compute.status(key, tracker, index, trackers, torrent) 52 | -- 1: working, 2: updating, 3: faulty, 4: stopped, 5: inactive 53 | local status = 3 54 | if torrent:get("state") == 0 then status = 4 55 | elseif tracker:get("is_enabled") == 0 then status = 5 56 | elseif tracker:get("failed_counter") == 0 57 | and tracker:get("success_counter") == 0 then status = 2 58 | elseif tracker:get("failed_counter") == 0 then status = 1 end 59 | return status 60 | end 61 | 62 | function compute.peers(key, tracker) 63 | return tracker:get("latest_new_peers") + tracker:get("latest_sum_peers") * 1e9 64 | end 65 | 66 | function format_values(tracker, index, trackers, ...) 67 | for key, value in tracker:pairs() do 68 | tracker:set(key, format[key] 69 | and format[key](value, key, tracker, index, trackers, ...) or value) 70 | end 71 | return tracker 72 | end 73 | 74 | function format.icon(value) return '' % value end 75 | function format.seeds(value) return tostring(value) end 76 | function format.leeches(value) return tostring(value) end 77 | function format.downloaded(value) return tostring(value) end 78 | function format.updated(value) return common.human_time(os.time() - value) end 79 | function format.enabled(value) return tostring(value) end 80 | 81 | function format.status(value, key, tracker) 82 | local last_success = tracker:get("success_time_last") ~= 0 83 | and os.date("!%Y-%m-%d %H:%M:%S", tracker:get("success_time_last")) or "never succeeded" 84 | local last_failed = tracker:get("failed_time_last") ~= 0 85 | and os.date("!%Y-%m-%d %H:%M:%S", tracker:get("failed_time_last")) or "never failed" 86 | local text, color = "", "" 87 | if value == 1 then text, color = "working", "green" 88 | elseif value == 2 then text, color = "updating", "blue" 89 | elseif value == 3 then text, color = "faulty", "red" 90 | elseif value == 4 then text = "stopped" 91 | elseif value == 5 then text = "inactive" end 92 | return '
%s
' % { 93 | last_success, last_failed, color, text 94 | } 95 | end 96 | 97 | function format.peers(value) 98 | local latest_new_peers, latest_sum_peers = value % 1e9, math.floor(value / 1e9) 99 | return '
%d/%d
' % { 100 | latest_new_peers, latest_sum_peers 101 | } 102 | end 103 | 104 | local total = array():set("count", 0):set("urls", array()) 105 | local torrent = array(rtorrent.batchcall("d.", hash, "name", "state", "is_active")) 106 | local trackers = array(rtorrent.multicall("t.", hash, "", 107 | "is_enabled", "url", "latest_new_peers", "latest_sum_peers", 108 | "failed_counter", "success_counter", "success_time_last", "failed_time_last", 109 | "scrape_complete", "scrape_incomplete", "scrape_downloaded", "scrape_time_last")) 110 | :foreach(compute_values, torrent) 111 | :foreach(compute_total, total) 112 | :sort(sort_column, sort_order) 113 | :limit(10, (page - 1) * 10) 114 | :foreach(format_values) 115 | :insert(total:get("count") > 1 and total or nil) 116 | 117 | local form, list, icon, url, status, peers, seeds, leeches, downloaded, updated, enabled, add 118 | 119 | _G.redirect = build_url("admin", "rtorrent", "main", unpack(common.get_cookie("rtorrent-main", {}))) 120 | form = SimpleForm("rtorrent", torrent:get("name")) 121 | form.template = "rtorrent/simpleform" 122 | form.notifications = common.get_cookie("rtorrent-notifications", {}) 123 | form.all_tabs = array():append("info", "files", "trackers", "peers", "chunks"):get() 124 | form.tab_url_postfix = function(tab) 125 | local filters = (tab == "trackers") and array(arg) or array(common.get_cookie("rtorrent-" .. tab, {})) 126 | return filters:get(1) == hash and filters:join("/") or hash 127 | end 128 | form.handle = function(self, state, data) 129 | if state == FORM_VALID then 130 | common.set_cookie("rtorrent-notifications", form.notifications) 131 | luci.http.redirect(nixio.getenv("REQUEST_URI")) 132 | end 133 | return true 134 | end 135 | form.cancel = "Trigger tracker scrape" 136 | form.on_cancel = function() 137 | rtorrent.batchcall("d.", hash, "tracker.send_scrape=0", "save_resume") 138 | luci.http.redirect(nixio.getenv("REQUEST_URI")) 139 | end 140 | 141 | list = form:section(Table, trackers:get()) 142 | list.template = "rtorrent/tblsection" 143 | list.name = "rtorrent-trackers" 144 | list.pages = common.pagination(total:get("count") or trackers:size(), page, common.pagination_link, 145 | build_url("admin", "rtorrent", "torrent", "trackers", hash, sort)):join() 146 | list.column = function(self, class, option, title, tooltip, sort_by) 147 | return self:option(class, option, '%s' % { 148 | build_url("admin", "rtorrent", "torrent", "trackers", hash, sort_by), 149 | tooltip, sort == sort_by and ' class="active"' or "", title 150 | }) 151 | end 152 | 153 | icon = list:option(DummyValue, "icon") 154 | icon.rawhtml = true 155 | icon.width = "1%" 156 | icon.classes = { "nowrap" } 157 | 158 | url = list:column(DummyValue, "url", "Url", "Sort by url", "url-asc") 159 | url.classes = { "wrap" } 160 | 161 | status = list:column(DummyValue, "status", "Status", "Sort by tracker status", "status-asc") 162 | status.rawhtml = true 163 | status.width = "1%" 164 | status.classes = { "nowrap", "center" } 165 | 166 | peers = list:column(DummyValue, "peers", "Peers", "Sort by peers", "peers-desc") 167 | peers.rawhtml = true 168 | peers.width = "1%" 169 | peers.classes = { "nowrap", "center" } 170 | 171 | seeds = list:column(DummyValue, "seeds", "Seeds", "Sort by complete peers", "seeds-desc") 172 | seeds.classes = { "nowrap", "center" } 173 | 174 | leeches = list:column(DummyValue, "leeches", "Leeches", "Sort by incomplete peers", "leeches-desc") 175 | leeches.width = "1%" 176 | leeches.classes = { "nowrap", "center" } 177 | 178 | downloaded = list:column(DummyValue, "downloaded", "Downloaded", 179 | "Sort by number of downloads", "downloaded-desc") 180 | downloaded.width = "1%" 181 | downloaded.classes = { "nowrap", "center" } 182 | 183 | updated = list:column(DummyValue, "updated", "Updated", "Sort by last scrape time", "updated-desc") 184 | updated.rawhtml = true 185 | updated.width = "1%" 186 | updated.classes = { "nowrap", "center" } 187 | 188 | enabled = list:column(Flag, "enabled", "Enabled", "Sort by enabled state", "enabled-desc") 189 | enabled.width = "1%" 190 | enabled.classes = { "nowrap", "center" } 191 | enabled.rmempty = false 192 | enabled.write = function(self, section, value) 193 | if trackers:get(section):get("index") and value ~= trackers:get(section):get("is_enabled") then 194 | rtorrent.call("t.is_enabled.set", 195 | hash .. ":t" .. trackers:get(section):get("index"), tonumber(value)) 196 | end 197 | end 198 | 199 | add = form:field(TextValue, "add_tracker", "Add tracker(s)", "All tracker URL should be in a separate line.") 200 | add.rows = 2 201 | add.validate = function(self, value, section) 202 | local errors = array() 203 | for _, line in ipairs(value:split("\r\n")) do 204 | if not line:trim():lower():match("^%w+://[%w_-]+%.[%w_-]+") then 205 | errors:insert("Invalid URL: %s" % line:trim()) 206 | elseif total:get("urls"):contains(line:trim()) then 207 | table.insert(form.notifications, "Skipped existing tracker %s" % line:trim()) 208 | else 209 | table.insert(form.notifications, "Added tracker %s" % line:trim()) 210 | end 211 | end 212 | if not errors:empty() then 213 | form.notifications = {} 214 | for i, err in errors:pairs() do 215 | if not errors:last(i) then self:add_error(section, err) end 216 | end 217 | return nil, errors:last() 218 | end 219 | return value 220 | end 221 | add.write = function(self, section, value) 222 | local tracker_group = trackers:filter(function(value) return value:get("index") end):size() 223 | local tracker_added = false 224 | for _, line in ipairs(value:split("\r\n")) do 225 | if not total:get("urls"):contains(line:trim()) then 226 | rtorrent.call("d.tracker.insert", hash, tracker_group, line:trim()) 227 | tracker_group, tracker_added = (tracker_group + 1) % 33, true 228 | end 229 | end 230 | if tracker_added and torrent:get("state") > 0 then 231 | if torrent:get("is_active") == 0 then 232 | rtorrent.batchcall("d.", hash, "stop", "close", "start", "pause") 233 | else 234 | rtorrent.batchcall("d.", hash, "stop", "close", "start") 235 | end 236 | end 237 | end 238 | 239 | return form 240 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/torrent/files.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local nixio = require "nixio" 5 | local rtorrent = require "rtorrent" 6 | local build_url = require "luci.dispatcher".build_url 7 | local common = require "luci.model.cbi.rtorrent.common" 8 | local array = require "luci.model.cbi.rtorrent.array" 9 | require "luci.model.cbi.rtorrent.string" 10 | 11 | luci.dispatcher.context.requestpath = array(luci.dispatcher.context.requestpath):map(string.urlencode):get() 12 | 13 | local args = array():append(unpack(arg)) 14 | local compute, format, hash, sort, page, path = {}, {}, 15 | table.remove(arg, 1), table.remove(arg, 1), table.remove(arg), array(arg) 16 | common.set_cookie("rtorrent-files", array():append(hash, sort):append(unpack(path:get())):append(page):get()) 17 | 18 | sort = sort or "name-asc" 19 | page = page and tonumber(page) or 1 20 | local sort_column, sort_order = unpack(sort:split("-")) 21 | 22 | function filter_by_folders(file, index, files, folders) 23 | file:set("name", file:get("path_components"):get(path:size() + 1)) 24 | if file:get("path_components"):size() > path:size() + 1 then 25 | file:set("type", "folder") 26 | if not folders:keys():contains(file:get("name")) then 27 | folders:set(file:get("name"), index) 28 | return true 29 | else 30 | files:get(folders:get(file:get("name"))) 31 | :increment("size_bytes", file:get("size_bytes")) 32 | :increment("size_chunks", file:get("size_chunks")) 33 | :increment("completed_chunks", file:get("completed_chunks")) 34 | if file:get("priority") ~= files:get(folders:get(file:get("name"))):get("priority") then 35 | files:get(folders:get(file:get("name"))):set("priority", math.huge) 36 | end 37 | return false 38 | end 39 | else 40 | file:set("type", "file") 41 | return true 42 | end 43 | end 44 | 45 | function sort_by_folders(lhs, rhs, comparator, order) 46 | if lhs:get("type") == "folder" and rhs:get("type") ~= "folder" then 47 | return true 48 | elseif lhs:get("type") ~= "folder" and rhs:get("type") == "folder" then 49 | return false 50 | else 51 | if order == "asc" then 52 | return lhs:get(comparator) < rhs:get(comparator) 53 | elseif order == "desc" then 54 | return lhs:get(comparator) > rhs:get(comparator) 55 | else assert(false, "invalid sort order: " .. tostring(order)) end 56 | end 57 | end 58 | 59 | function compute_navigate_up() 60 | if path:empty() then return nil end 61 | return format_values(array() 62 | :set("type", "up") 63 | :set("icon", compute.icon) 64 | :set("name", "..")) 65 | end 66 | 67 | function compute_total(file, index, files, total) 68 | total:increment("count") 69 | :increment("size", file:get("size")) 70 | :increment("size_chunks", file:get("size_chunks")) 71 | :increment("completed_chunks", file:get("completed_chunks")) 72 | :increment("files", file:get("type") == "file" and 1 or 0) 73 | :increment("folders", file:get("type") == "folder" and 1 or 0) 74 | total:get("priority"):insert(file:get("priority")) 75 | if files:last(index) then 76 | format_values(total:set(".total_row", true) 77 | :set("type", "total") 78 | :set("name", "TOTAL: %d pcs." % total:get("count")) 79 | :set("done", compute.done) 80 | :set("priority", total:get("priority"):unique():size() == 1 81 | and total:get("priority"):get(1) or math.huge)) 82 | end 83 | end 84 | 85 | function compute_values(file, index, files, ...) 86 | for _, key in ipairs({ "index", "icon", "size", "done" }) do 87 | file:set(key, compute[key], index, files, ...) 88 | end 89 | end 90 | 91 | function compute.icon(key, file) 92 | local icons_path = "/luci-static/resources/icons/filetypes" 93 | if file:get("type") == "up" then return "%s/%s.png" % { icons_path, "up" } end 94 | if file:get("type") == "folder" then return "%s/%s.png" % { icons_path, "dir" } end 95 | local ext = file:get("path"):match("%.([^%.]+)$") 96 | if ext and nixio.fs.stat("/www%s/%s.png" % { icons_path, ext:lower() }, "type") then 97 | return "%s/%s.png" % { icons_path, ext:lower() } 98 | else return "%s/%s.png" % { icons_path, "file" } end 99 | end 100 | 101 | function compute.size(key, file) return file:get("size_bytes") end 102 | 103 | function compute.done(key, file) 104 | return 100.0 * file:get("completed_chunks") / file:get("size_chunks") 105 | end 106 | 107 | function format_values(file, index, files, ...) 108 | for key, value in file:pairs() do 109 | file:set(key, format[key] 110 | and format[key](value, key, file, index, files, ...) or value) 111 | end 112 | return file 113 | end 114 | 115 | function format.icon(value) return '' % value end 116 | function format.priority(value) return value == math.huge and "mixed" or tostring(value) end 117 | 118 | function format.name(value, key, file) 119 | if file:get("type") == "total" 120 | or file:get("type") == "file" and file:get("completed_chunks") ~= file:get("size_chunks") then 121 | return value 122 | end 123 | local title, href = "", array():append("admin", "rtorrent") 124 | if file:get("type") == "up" then href 125 | :append("torrent", "files", hash, sort) 126 | :append(unpack(array(path:get()):remove(path:size()):get())):insert("1") 127 | title = ' title="Navigate up one directory level"' 128 | elseif file:get("type") == "folder" then href 129 | :append("torrent", "files", hash, sort) 130 | :append(unpack(array(path:get()):insert(value):get())):insert("1") 131 | elseif file:get("type") == "file" then href 132 | :append("download", hash) 133 | :append(unpack(array(path:get()):insert(value):get())) 134 | end 135 | return '%s' % { build_url(unpack(href:map(string.urlencode):get())), title, value } 136 | end 137 | 138 | function format.size(value, key, file) 139 | return '
%s
' % { value, common.human_size(value) } 140 | end 141 | 142 | function format.done(value, key, file) 143 | return "%.1f%%" % value 144 | end 145 | 146 | local folders, total = array(), array():set("priority", array()):set("count", 0) 147 | local torrent = array(rtorrent.batchcall("d.", hash, "name", "is_multi_file")) 148 | local files = array(rtorrent.multicall("f.", hash, "/" .. array(path:get()):insert(""):join("/") .. "*", 149 | "path", "path_components", "frozen_path", "size_bytes", 150 | "size_chunks", "completed_chunks", "priority")) 151 | :filter(filter_by_folders, folders) 152 | :foreach(compute_values, torrent) 153 | :foreach(compute_total, total) 154 | :sort(sort_by_folders, sort_column, sort_order) 155 | :limit(10, (page - 1) * 10) 156 | :foreach(format_values) 157 | :insert(1, compute_navigate_up) 158 | :insert(total:get("count") > 1 and total or nil) 159 | 160 | local form, breadcrumb, list, icon, name, size, done, prio 161 | 162 | _G.redirect = build_url("admin", "rtorrent", "main", unpack(common.get_cookie("rtorrent-main", {}))) 163 | form = SimpleForm("rtorrent", torrent:get("name")) 164 | form.template = "rtorrent/simpleform" 165 | form.all_tabs = array():append("info", "files", "trackers", "peers", "chunks"):get() 166 | form.tab_url_postfix = function(tab) 167 | local filters = (tab == "files") and args or array(common.get_cookie("rtorrent-" .. tab, {})) 168 | return filters:get(1) == hash and filters:join("/") or hash 169 | end 170 | form.handle = function(self, state, data) 171 | if state == FORM_VALID then luci.http.redirect(nixio.getenv("REQUEST_URI")) end 172 | return true 173 | end 174 | if torrent:get("is_multi_file") == 1 and total:get("size_chunks") == total:get("completed_chunks") 175 | and total:get("folders") > 0 or total:get("files") > 1 then 176 | form.cancel = "Download this folder" 177 | form.on_cancel = function() 178 | luci.http.redirect(build_url(unpack(array():append("admin", "rtorrent", "download", hash) 179 | :append(unpack(path:get())):map(string.urlencode):get()))) 180 | end 181 | end 182 | 183 | if not path:empty() then 184 | breadcrumb = form:field(DummyValue, "breadcrumb") 185 | breadcrumb.rawhtml = true 186 | breadcrumb.value = array(path:get()):insert(1, ""):map(function(subpath, index, subpaths) 187 | return '%s' % { 188 | build_url("admin", "rtorrent", "torrent", "files", hash, sort, 189 | subpaths:limit(index):filter(string.not_blank):map(string.urlencode):join("/"), "1"), 190 | subpath:blank() and '' or subpath 191 | } 192 | end):join(" / ") 193 | end 194 | 195 | list = form:section(Table, files:get()) 196 | list.template = "rtorrent/tblsection" 197 | list.name = "rtorrent-files" 198 | list.pages = common.pagination(total:get("count") or files:size(), tonumber(page), common.pagination_link, 199 | build_url("admin", "rtorrent", "torrent", "files", hash, sort, path:map(string.urlencode):join("/"))):join() 200 | list.column = function(self, class, option, title, tooltip, sort_by) 201 | return self:option(class, option, '%s' % { 202 | build_url("admin", "rtorrent", "torrent", "files", 203 | hash, sort_by, path:map(string.urlencode):join("/"), "1"), 204 | tooltip, sort == sort_by and ' class="active"' or "", title 205 | }) 206 | end 207 | 208 | icon = list:option(DummyValue, "icon") 209 | icon.rawhtml = true 210 | icon.width = "1%" 211 | icon.classes = { "nowrap" } 212 | 213 | name = list:column(DummyValue, "name", "Name", "Sort by name", "name-asc") 214 | name.rawhtml = true 215 | name.classes = { "wrap" } 216 | 217 | size = list:column(DummyValue, "size", "Size", "Sort by size", "size-desc") 218 | size.rawhtml = true 219 | size.width = "1%" 220 | size.classes = { "nowrap", "center" } 221 | 222 | done = list:column(DummyValue, "done", "Done", "Sort by download done percent", "done-desc") 223 | done.rawhtml = true 224 | done.width = "10%" 225 | done.classes = { "nowrap", "center" } 226 | 227 | local all_files 228 | prio = list:column(ListValue, "priority", "Priority", "Sort by priority", "priority-desc") 229 | prio.classes = { "nowrap", "center" } 230 | prio.width = "15%" 231 | prio:value("0", "off") 232 | prio:value("1", "normal") 233 | prio:value("2", "high") 234 | prio:value("", "hidden") 235 | prio:value("mixed", "mixed") 236 | prio.write = function(self, section, value) 237 | if files:get(section):get("type") == "total" then return end 238 | if not all_files then 239 | all_files = array(rtorrent.multicall("f.", hash, "", "path_components", "priority")) 240 | end 241 | local indices = array() 242 | all_files:foreach(function(file, index) 243 | if files:get(section):get("path_components"):limit(path:size() + 1):join("/") 244 | == file:get("path_components"):limit(path:size() + 1):join("/") 245 | and file:get("priority") ~= tonumber(value) then 246 | indices:insert(index - 1) 247 | end 248 | end) 249 | for _, index in indices:pairs() do 250 | rtorrent.call("f.priority.set", hash .. ":f" .. index, tonumber(value)) 251 | rtorrent.call("d.update_priorities", hash) 252 | end 253 | end 254 | 255 | return form 256 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/torrent/peers.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local rtorrent = require "rtorrent" 5 | local json = require "luci.jsonc" 6 | local build_url = require "luci.dispatcher".build_url 7 | local common = require "luci.model.cbi.rtorrent.common" 8 | local array = require "luci.model.cbi.rtorrent.array" 9 | require "luci.model.cbi.rtorrent.string" 10 | 11 | local compute, format, hash, sort, page = {}, {}, unpack(arg) 12 | common.set_cookie("rtorrent-peers", { hash, sort, page }) 13 | 14 | local ip_location, country_flag, map = {}, {}, {} 15 | sort = sort or "down_speed-desc" 16 | page = page and tonumber(page) or 1 17 | local sort_column, sort_order = unpack(sort:split("-")) 18 | 19 | local ip_location_provider = "geoplugin_net" 20 | local country_flag_provider = "hltv_org" 21 | local map_provider = "googlemap" 22 | 23 | function ip_location.geoplugin_net(address) 24 | return array({ 25 | url = "http://www.geoplugin.net/json.gp?ip=%s" % address, 26 | fields = array({ 27 | country_code = "geoplugin_countryCode", country = "geoplugin_countryName", 28 | region = "geoplugin_region", city = "geoplugin_city", 29 | latitude = "geoplugin_latitude", longitude = "geoplugin_longitude" 30 | }) 31 | }) 32 | end 33 | 34 | function country_flag.prgmea_org(country_code) 35 | return array({ url = "http://prgmea.org/assets/falcon/images/flags/16x16/%s.png" % country_code }) 36 | end 37 | 38 | function country_flag.ip2location_com(country_code) 39 | return array({ url = "https://cdn.ip2location.com/assets/img/flags/%s.png" % country_code }) 40 | end 41 | 42 | function country_flag.hltv_org(country_code) 43 | return array({ url = "http://static.hltv.org/images/flag/%s.gif" % country_code }) 44 | end 45 | 46 | function map.googlemap(latitude, longitude, zoom) 47 | return array({ 48 | url = "https://google.com/maps/place/%s,%s/@%s,%s,%sz" % { 49 | latitude, longitude, latitude, longitude, zoom 50 | } 51 | }) 52 | end 53 | 54 | function map.openstreetmap(latitude, longitude, zoom) 55 | return array({ 56 | url = "http://www.openstreetmap.org/?mlat=%s&mlon=%s#map=%s/%s/%s/m" % { 57 | latitude, longitude, zoom, latitude, longitude 58 | } 59 | }) 60 | end 61 | 62 | function compute_total(peer, index, peers, total) 63 | total:increment("count") 64 | total:increment("down_speed", peer:get("down_speed")) 65 | total:increment("up_speed", peer:get("up_speed")) 66 | if peers:last(index) then 67 | format_values(total:set(".total_row", true) 68 | :set("location", "TOTAL: %d pcs." % total:get("count"))) 69 | end 70 | end 71 | 72 | function compute_values(peer, index, peers, ...) 73 | for _, key in ipairs({ "client", "flags", "done", "down_speed", "up_speed", "downloaded", "uploaded" }) do 74 | peer:set(key, compute[key], index, peers, ...) 75 | end 76 | end 77 | 78 | function compute.client(key, peer) return peer:get("client_version") end 79 | function compute.done(key, peer) return peer:get("completed_percent") end 80 | function compute.down_speed(key, peer) return peer:get("down_rate") end 81 | function compute.up_speed(key, peer) return peer:get("up_rate") end 82 | function compute.downloaded(key, peer) return peer:get("down_total") end 83 | function compute.uploaded(key, peer) return peer:get("up_total") end 84 | 85 | function compute.flags(key, peer) 86 | local flags = "" 87 | if peer:get("banned") == 1 then flags = flags .. "B" end 88 | if peer:get("is_incoming") == 1 then flags = flags .. "I" end 89 | if peer:get("is_encrypted") == 1 then flags = flags .. "E" 90 | elseif peer:get("is_obfuscated") == 1 then flags = flags .. "e" end 91 | if peer:get("is_snubbed") == 1 then flags = flags .. "S" end 92 | if peer:get("is_preferred") == 1 then flags = flags .. "F" 93 | elseif peer:get("is_unwanted") == 1 then flags = flags .. "f" end 94 | return flags 95 | end 96 | 97 | function compute_geolocation(peer, index, peers, ...) 98 | for _, key in ipairs({ "location", "icon" }) do 99 | peer:set(key, compute[key], index, peers, ...) 100 | end 101 | end 102 | 103 | function compute.icon(key, peer) 104 | return peer:get("country_code"):blank() and "" 105 | or country_flag[country_flag_provider](peer:get("country_code"):lower()):get("url") 106 | end 107 | 108 | function compute.location(key, peer) 109 | local location_provider = ip_location[ip_location_provider](peer:get("address")) 110 | local response, err, headers, code, status = common.download(location_provider:get("url")) 111 | local location = array(json.parse(response or "")) 112 | location_provider:get("fields"):foreach(function(field, key) 113 | peer:set(key, location:get(field) or "") 114 | end) 115 | if code == 429 then peer:set("country", "Location service rate limit reached!") 116 | elseif peer:get("country"):blank() then peer:set("country", "Unknown") end 117 | return array({ "country", "region", "city" }) 118 | :map(function(field) return peer:get(field):unicode_to_html() end) 119 | :filter(string.not_blank) 120 | :join(" / ") 121 | end 122 | 123 | function format_values(peer, index, peers, ...) 124 | for key, value in peer:pairs() do 125 | peer:set(key, format[key] 126 | and format[key](value, key, peer, index, peers, ...) or value) 127 | end 128 | return peer 129 | end 130 | 131 | function format.icon(value) return value and '' % value or "" end 132 | function format.done(value) return "%.1f%%" % value end 133 | function format.down_speed(value) return "%.2f" % (value / 1000) end 134 | function format.up_speed(value) return "%.2f" % (value / 1000) end 135 | function format.uploaded(value) return format.downloaded(value) end 136 | 137 | function format.downloaded(value) 138 | return "
%s
" % { 139 | value, value == 0 and "--" or common.human_size(value) 140 | } 141 | end 142 | 143 | function format.location(value, key, peer) 144 | if peer:get("latitude") and peer:get("latitude"):not_blank() then 145 | return '%s' % { 146 | map[map_provider](peer:get("latitude"), peer:get("longitude"), 12):get("url"), 147 | value 148 | } 149 | else return value end 150 | end 151 | 152 | function format.flags(value) 153 | local title = array() 154 | if value:match("B") then title:insert("Peer is banned") end 155 | if value:match("I") then title:insert("Peer is an incoming connection") end 156 | if value:match("E") then title:insert("Peer is using protocol encryption") 157 | elseif value:match("e") then title:insert("Peer is using header message obfuscation") end 158 | if value:match("S") then title:insert("Peer is snubbed") end 159 | if value:match("F") then title:insert("Peer is marked as preferred") 160 | elseif value:match("f") then title:insert("Peer is marked as unwanted") end 161 | return '
%s
' % { title:join(" "), value } 162 | end 163 | 164 | local total = array():set("count", 0) 165 | local torrent = array(rtorrent.batchcall("d.", hash, "name")) 166 | local peers = array(rtorrent.multicall("p.", hash, "", 167 | "address", "client_version", "banned", 168 | "is_incoming", "is_encrypted", "is_obfuscated", "is_snubbed", "is_preferred", "is_unwanted", 169 | "completed_percent", "down_rate", "up_rate", "up_total", "down_total")) 170 | :foreach(compute_values) 171 | :foreach(compute_total, total) 172 | :sort(sort_column, sort_order) 173 | :limit(10, (page - 1) * 10) 174 | :foreach(compute_geolocation) 175 | :foreach(format_values) 176 | :insert(total:get("count") > 1 and total or nil) 177 | 178 | local form, list, icon, location, address, client, flags, done, down_speed, up_speed, downloaded, uploaded 179 | 180 | _G.redirect = build_url("admin", "rtorrent", "main", unpack(common.get_cookie("rtorrent-main", {}))) 181 | form = SimpleForm("rtorrent", torrent:get("name")) 182 | form.template = "rtorrent/simpleform" 183 | form.submit = false 184 | form.reset = false 185 | form.all_tabs = array():append("info", "files", "trackers", "peers", "chunks"):get() 186 | form.tab_url_postfix = function(tab) 187 | local filters = (tab == "peers") and array(arg) or array(common.get_cookie("rtorrent-" .. tab, {})) 188 | return filters:get(1) == hash and filters:join("/") or hash 189 | end 190 | form.handle = function(self, state, data) 191 | if state == FORM_VALID then luci.http.redirect(nixio.getenv("REQUEST_URI")) end 192 | return true 193 | end 194 | 195 | list = form:section(Table, peers:get()) 196 | list.template = "rtorrent/tblsection" 197 | list.name = "rtorrent-peers" 198 | list.pages = common.pagination(total:get("count") or peers:size(), page, common.pagination_link, 199 | build_url("admin", "rtorrent", "torrent", "peers", hash, sort)):join() 200 | list.column = function(self, class, option, title, tooltip, sort_by) 201 | return self:option(class, option, '%s' % { 202 | build_url("admin", "rtorrent", "torrent", "peers", hash, sort_by), 203 | tooltip, sort == sort_by and ' class="active"' or "", title 204 | }) 205 | end 206 | 207 | icon = list:option(DummyValue, "icon") 208 | icon.rawhtml = true 209 | icon.width = "1%" 210 | icon.classes = { "nowrap" } 211 | 212 | location = list:option(DummyValue, "location", 213 | 'Location') 214 | location.rawhtml = true 215 | location.classes = { "wrap" } 216 | 217 | address = list:option(DummyValue, "address", "Address") 218 | address.rawhtml = true 219 | address.width = "1%" 220 | address.classes = { "nowrap", "center" } 221 | 222 | client = list:column(DummyValue, "client", "Client", "Sort by client version", "client-asc") 223 | client.width = "1%" 224 | client.classes = { "nowrap", "center" } 225 | 226 | flags = list:column(DummyValue, "flags", "Flags", "Sort by peer flags", "flags-asc") 227 | flags.rawhtml = true 228 | flags.width = "1%" 229 | flags.classes = { "nowrap", "center" } 230 | 231 | done = list:column(DummyValue, "done", "Done", "Sort by peer completed data percent", "done-desc") 232 | done.width = "1%" 233 | done.classes = { "nowrap", "center" } 234 | 235 | down_speed = list:column(DummyValue, "down_speed", "Down
Speed", 236 | "Sort by download speed (kB/s)", "down_speed-desc") 237 | down_speed.width = "1%" 238 | down_speed.classes = { "nowrap", "center" } 239 | 240 | up_speed = list:column(DummyValue, "up_speed", "Up
Speed", 241 | "Sort by upload speed (kB/s)", "up_speed-desc") 242 | up_speed.width = "1%" 243 | up_speed.classes = { "nowrap", "center" } 244 | 245 | downloaded = list:column(DummyValue, "downloaded", "Down-
loaded", 246 | "Sort by total downloaded from peer", "downloaded-desc") 247 | downloaded.rawhtml = true 248 | downloaded.width = "1%" 249 | downloaded.classes = { "nowrap", "center" } 250 | 251 | uploaded = list:column(DummyValue, "uploaded", "Up-
loaded", 252 | "Sort by total uploaded to peer", "uploaded-desc") 253 | uploaded.rawhtml = true 254 | uploaded.width = "1%" 255 | uploaded.classes = { "nowrap", "center" } 256 | 257 | return form 258 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/settings/rtorrent.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local rtorrent = require "rtorrent" 5 | local common = require "luci.model.cbi.rtorrent.common" 6 | local array = require "luci.model.cbi.rtorrent.array" 7 | local build_url = require "luci.dispatcher".build_url 8 | 9 | local throttle = array(rtorrent.batchcall("throttle.", "", 10 | "global_down.max_rate", "global_up.max_rate", "max_downloads.global", "max_uploads.global", 11 | "max_downloads", "max_uploads", "min_peers.normal", "max_peers.normal", 12 | "min_peers.seed", "max_peers.seed")) 13 | local trackers = array(rtorrent.batchcall("trackers.", "", "numwant", "use_udp")) 14 | local network = array(rtorrent.batchcall("network.", "", 15 | "http.max_open", "max_open_files", "max_open_sockets", "xmlrpc.size_limit")) 16 | 17 | local form, bandwidth_limits, download_limit, upload_limit 18 | local global_limits, max_downloads_global, max_uploads_global 19 | local torrent_limits, max_downloads, max_uploads, min_peers, max_peers, min_seeds, max_seeds, peers_numwant 20 | local network_limits, max_http_requests, max_open_files, max_open_sockets, max_xmlrpc_size 21 | 22 | form = SimpleForm("rtorrent", "rTorrent Settings", 23 | "The below settings are change the running rTorrent instance only!
" 24 | .. "If you want to make it permanent, please change them in " 25 | .. "the /root/.rtorrent.rc config file.") 26 | form.handle = function(self, state, data) 27 | if state == FORM_VALID then luci.http.redirect(nixio.getenv("REQUEST_URI")) end 28 | return true 29 | end 30 | 31 | bandwidth_limits = form:section(SimpleSection, "Bandwidth limits") 32 | 33 | download_limit = bandwidth_limits:option(Value, "global_down_max_rate_kb", "Download limit (KiB/sec)", 34 | "Global download rate (0: unlimited).
Related .rtorrent.rc config line: " 35 | .. "throttle.global_down.max_rate.set_kb.") 36 | download_limit.template = "rtorrent/value" 37 | download_limit.rmempty = false 38 | download_limit.default = throttle:get("global_down_max_rate") / 1024 39 | download_limit.datatype = "uinteger" 40 | download_limit.write = function(self, section, value) 41 | if value ~= tostring(download_limit.default) then 42 | rtorrent.call("throttle.global_down.max_rate.set_kb", "", value) 43 | end 44 | end 45 | 46 | upload_limit = bandwidth_limits:option(Value, "global_up_max_rate_kb", "Upload limit (KiB/sec)", 47 | "Global upload rate (0: unlimited).
Related .rtorrent.rc config line: " 48 | .. "throttle.global_up.max_rate.set_kb.") 49 | upload_limit.template = "rtorrent/value" 50 | upload_limit.rmempty = false 51 | upload_limit.default = throttle:get("global_up_max_rate") / 1024 52 | upload_limit.datatype = "uinteger" 53 | upload_limit.write = function(self, section, value) 54 | if value ~= tostring(upload_limit.default) then 55 | rtorrent.call("throttle.global_up.max_rate.set_kb", "", value) 56 | end 57 | end 58 | 59 | global_limits = form:section(SimpleSection, "Global limits") 60 | 61 | max_downloads_global = global_limits:option(Value, "max_downloads_global", "Download slots", 62 | "Maximum number of simultaneous downloads (0: unlimited).
Related .rtorrent.rc config line: " 63 | .. "throttle.max_downloads.global.set.") 64 | max_downloads_global.rmempty = false 65 | max_downloads_global.default = throttle:get("max_downloads_global") 66 | max_downloads_global.datatype = "uinteger" 67 | max_downloads_global.write = function(self, section, value) 68 | if value ~= tostring(max_downloads_global.default) then 69 | rtorrent.call("throttle.max_downloads.global.set", "", value) 70 | end 71 | end 72 | 73 | max_uploads_global = global_limits:option(Value, "max_uploads_global", "Upload slots", 74 | "Maximum number of simultaneous uploads (0: unlimited).
Related .rtorrent.rc config line: " 75 | .. "throttle.max_uploads.global.set.") 76 | max_uploads_global.rmempty = false 77 | max_uploads_global.default = throttle:get("max_uploads_global") 78 | max_uploads_global.datatype = "uinteger" 79 | max_uploads_global.write = function(self, section, value) 80 | if value ~= tostring(max_uploads_global.default) then 81 | rtorrent.call("throttle.max_uploads.global.set", "", value) 82 | end 83 | end 84 | 85 | torrent_limits = form:section(SimpleSection, "Torrent limits") 86 | 87 | max_downloads = torrent_limits:option(Value, "max_downloads", "Maximum downloads", 88 | "Maximum number of simultanious downloads per torrent (0: unlimited).
" 89 | .. "Related .rtorrent.rc config line: throttle.max_downloads.set.") 90 | max_downloads.rmempty = false 91 | max_downloads.default = throttle:get("max_downloads") 92 | max_downloads.datatype = "uinteger" 93 | max_downloads.write = function(self, section, value) 94 | if value ~= tostring(max_downloads.default) then 95 | rtorrent.call("throttle.max_downloads.set", "", value) 96 | end 97 | end 98 | 99 | max_uploads = torrent_limits:option(Value, "max_uploads", "Maximum uploads", 100 | "Maximum number of simultanious uploads per torrent (0: unlimited).
" 101 | .. "Related .rtorrent.rc config line: throttle.max_uploads.set.") 102 | max_uploads.rmempty = false 103 | max_uploads.default = throttle:get("max_uploads") 104 | max_uploads.datatype = "uinteger" 105 | max_uploads.write = function(self, section, value) 106 | if value ~= tostring(max_uploads.default) then 107 | rtorrent.call("throttle.max_uploads.set", "", value) 108 | end 109 | end 110 | 111 | min_peers = torrent_limits:option(Value, "min_peers", "Minimum peers", 112 | "Minimum number of peers to connect to per torrent.
" 113 | .. "Related .rtorrent.rc config line: throttle.min_peers.normal.set.") 114 | min_peers.rmempty = false 115 | min_peers.default = throttle:get("min_peers_normal") 116 | min_peers.datatype = "uinteger" 117 | min_peers.write = function(self, section, value) 118 | if value ~= tostring(min_peers.default) then 119 | rtorrent.call("throttle.min_peers.normal.set", "", value) 120 | end 121 | end 122 | 123 | max_peers = torrent_limits:option(Value, "max_peers", "Maximum peers", 124 | "Maximum number of peers to connect to per torrent.
" 125 | .. "Related .rtorrent.rc config line: throttle.max_peers.normal.set.") 126 | max_peers.rmempty = false 127 | max_peers.default = throttle:get("max_peers_normal") 128 | max_peers.datatype = "uinteger" 129 | max_peers.write = function(self, section, value) 130 | if value ~= tostring(max_peers.default) then 131 | rtorrent.call("throttle.max_peers.normal.set", "", value) 132 | end 133 | end 134 | 135 | min_seeds = torrent_limits:option(Value, "min_seeds", "Minimum seeds", 136 | "Minimum number of seeds for completed torrents (-1: same as peers).
" 137 | .. "Related .rtorrent.rc config line: throttle.min_peers.seed.set.") 138 | min_seeds.rmempty = false 139 | min_seeds.default = throttle:get("min_peers_seed") 140 | min_seeds.datatype = "integer" 141 | min_seeds.write = function(self, section, value) 142 | if value ~= tostring(min_seeds.default) then 143 | rtorrent.call("throttle.min_peers.seed.set", "", value) 144 | end 145 | end 146 | 147 | max_seeds = torrent_limits:option(Value, "max_seeds", "Maximum seeds", 148 | "Maximum number of seeds for completed torrents (-1: same as peers).
" 149 | .. "Related .rtorrent.rc config line: throttle.max_peers.seed.set.") 150 | max_seeds.rmempty = false 151 | max_seeds.default = throttle:get("max_peers_seed") 152 | max_seeds.datatype = "integer" 153 | max_seeds.write = function(self, section, value) 154 | if value ~= tostring(max_seeds.default) then 155 | rtorrent.call("throttle.max_peers.seed.set", "", value) 156 | end 157 | end 158 | 159 | peers_numwant = torrent_limits:option(Value, "peers_numwant", "Wished peers", 160 | "Wished number of peers (-1: disable feature).
" 161 | .. "Related .rtorrent.rc config line: trackers.numwant.set.") 162 | peers_numwant.rmempty = false 163 | peers_numwant.default = trackers:get("numwant") 164 | peers_numwant.datatype = "integer" 165 | peers_numwant.write = function(self, section, value) 166 | if value ~= tostring(peers_numwant.default) then 167 | rtorrent.call("trackers.numwant.set", "", value) 168 | end 169 | end 170 | 171 | network_limits = form:section(SimpleSection, "Network limits") 172 | 173 | max_http_requests = network_limits:option(Value, "max_http_requests", "Maximum http requests", 174 | "Maximum number of simultaneous HTTP request (used by announce / scrape requests).
" 175 | .. "Related .rtorrent.rc config line: network.http.max_open.set.") 176 | max_http_requests.rmempty = false 177 | max_http_requests.default = network:get("http_max_open") 178 | max_http_requests.datatype = "uinteger" 179 | max_http_requests.write = function(self, section, value) 180 | if value ~= tostring(max_http_requests.default) then 181 | rtorrent.call("network.http.max_open.set", "", value) 182 | end 183 | end 184 | 185 | max_open_files = network_limits:option(Value, "max_open_files", "Maximum open files", 186 | "Maximum number of open files rTorrent can keep open.
" 187 | .. "Related .rtorrent.rc config line: network.max_open_files.set.") 188 | max_open_files.rmempty = false 189 | max_open_files.default = network:get("max_open_files") 190 | max_open_files.datatype = "uinteger" 191 | max_open_files.write = function(self, section, value) 192 | if value ~= tostring(max_open_files.default) then 193 | rtorrent.call("network.max_open_files.set", "", value) 194 | end 195 | end 196 | 197 | max_open_sockets = network_limits:option(Value, "max_open_sockets", "Maximum open sockets", 198 | "Maximum number of connections rTorrent can accept / make (sockets).
" 199 | .. "Related .rtorrent.rc config line: network.max_open_sockets.set.") 200 | max_open_sockets.rmempty = false 201 | max_open_sockets.default = network:get("max_open_sockets") 202 | max_open_sockets.datatype = "uinteger" 203 | max_open_sockets.write = function(self, section, value) 204 | if value ~= tostring(max_open_sockets.default) then 205 | rtorrent.call("network.max_open_sockets.set", "", value) 206 | end 207 | end 208 | 209 | max_xmlrpc_size = network_limits:option(Value, "max_xmlrpc_size", "Maximum XML-RPC size", 210 | "Maximum size of any XML-RPC requests in bytes.
" 211 | .. "Human-readable forms such as 2M are also allowed (for 2 MiB, i.e. 2097152 bytes).
" 212 | .. "Related .rtorrent.rc config line: network.xmlrpc.size_limit.set.") 213 | max_xmlrpc_size.rmempty = false 214 | max_xmlrpc_size.default = network:get("xmlrpc_size_limit") 215 | max_xmlrpc_size.write = function(self, section, value) 216 | if value ~= tostring(max_xmlrpc_size.default) then 217 | rtorrent.call("network.xmlrpc.size_limit.set", "", value) 218 | end 219 | end 220 | 221 | return form 222 | -------------------------------------------------------------------------------- /src/usr/lib/lua/rss_downloader.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | -- Copyright 2014-2021 Sandor Balazsi 3 | -- Licensed to the public under the GNU General Public License. 4 | 5 | local nixio = require "nixio" 6 | local xml = require "lxp.lom" 7 | local bencode = require "bencode" 8 | local date = require "luci.http.date" 9 | local uci = require "luci.model.uci".cursor() 10 | local logger = require "luci.model.cbi.rtorrent.logger" 11 | local common = require "luci.model.cbi.rtorrent.common" 12 | local array = require "luci.model.cbi.rtorrent.array" 13 | require "luci.model.cbi.rtorrent.string" 14 | 15 | local log 16 | local aria2_timeout_seconds = 60 17 | 18 | function next_tag(xml, tag, index) 19 | index = index or 1 20 | if not xml then return nil, index end 21 | while xml[index] do 22 | if type(xml[index]) == "table" and xml[index].tag == tag then 23 | return xml[index], index 24 | end 25 | index = index + 1 26 | end 27 | end 28 | 29 | function prev_tag(xml, tag, index) 30 | if not xml then return nil, index end 31 | index = index or #xml 32 | while xml[index] do 33 | if type(xml[index]) == "table" and xml[index].tag == tag then 34 | return xml[index], index 35 | end 36 | index = index - 1 37 | end 38 | end 39 | 40 | function fix_date(date) 41 | return date:gsub("(%a+%s+)(%d%d)(%s+%d+)", function(month, year, hour) 42 | return month .. "20" .. year .. hour 43 | end) 44 | end 45 | 46 | function rss_feeds() 47 | return common.uci_sections("rtorrent", "rss-feed") 48 | :filter(function(feed) return feed:get("enabled") == "1" end) 49 | end 50 | 51 | function rss_rules() 52 | return common.uci_sections("rtorrent", "rss-rule") 53 | :filter(function(rule) return rule:get("enabled") == "1" end) 54 | end 55 | 56 | function fetch_feed(url) 57 | local content, err = common.download(url) 58 | if not content then log:error('Failed to download RSS feed "%s": %s' % { url, err }) 59 | else 60 | local rss, err = xml.parse(content) 61 | if not rss then log:error('Failed to parse RSS feed "%s": %s' % { url, err }) 62 | else return rss end 63 | end 64 | end 65 | 66 | function rss_item_url(item) 67 | local enclosure, url = next_tag(item, "enclosure") 68 | if enclosure and enclosure.attr.url then return enclosure.attr.url end 69 | local link = next_tag(item, "link") 70 | if link and link[1] then return link[1] end 71 | log:error("Failed to obtain download link of RSS item") 72 | return nil 73 | end 74 | 75 | function torrent_size(url) 76 | local content, code, err 77 | if "file://" == url:sub(1, 7) then content, code, err = nixio.fs.readfile(url:sub(8)) 78 | else content, err = common.download(url) end 79 | if not content then return log:error('Failed to download torrent "%s": %s' % { url, err }) end 80 | local torrent, err = bencode.decode(content) 81 | if not torrent or not torrent.info then 82 | return log:error('Failed to parse torrent "%s": %s' % { url, err }) 83 | end 84 | local size = 0 85 | array(torrent.info):traverse(function(value, key) 86 | if key == "length" then size = size + value end 87 | end) 88 | if size == 0 then 89 | local sha1_size = 20 90 | local piece_length = torrent.info["piece length"] 91 | local piece_count = torrent.info.pieces:len() / sha1_size 92 | -- note: when the last piece is shorter (not a full piece length) 93 | -- then the calculated size is bigger (with piece length - last piece length) 94 | size = piece_length * piece_count 95 | end 96 | if size > 0 then return size / 1024 / 1024 end 97 | end 98 | 99 | function magnet_size(uri) 100 | uri = uri:gsub("&", "&"):gsub("^ 0 then return size end 125 | end 126 | 127 | function filter_by_feed(rule, feed) 128 | local match_feed = array(rule:get("feed"):split("|")):contains(feed:get("name")) and true 129 | log:trace('Matching by feed "%s": %s' % { rule:get("feed"), tostring(match_feed) }) 130 | return match_feed 131 | end 132 | 133 | function filter_by_title(rule, title) 134 | local match_title = title:lower():find(rule:get("match"):lower_pattern()) and true 135 | log:trace('Matching by title "%s": %s' % { rule:get("match"), tostring(match_title) }) 136 | local match_exclude = not rule:get("exclude") or not title:lower():find(rule:get("exclude"):lower_pattern()) 137 | log:trace('Matching by exclude "%s": %s' % { tostring(rule:get("exclude")), tostring(match_exclude) }) 138 | return match_title and match_exclude 139 | end 140 | 141 | function filter_by_size(rule, item) 142 | local match_min_size, match_max_size = true, true 143 | if rule:get("minsize") or rule:get("maxsize") then 144 | local enclosure, size = next_tag(item, "enclosure") 145 | if enclosure then 146 | size = enclosure.attr and enclosure.attr.length 147 | and tonumber(enclosure.attr.length) / 1024 / 1024 148 | if size then log:trace("Size from enclosure length: %.2f" % size) end 149 | end 150 | if not size then 151 | local url = rss_item_url(item) 152 | if url and url:match("magnet:?") then size = magnet_size(url) 153 | elseif url then size = torrent_size(url) end 154 | if size then log:trace("Size from torrent/magnet parse: %.2f" % size) end 155 | end 156 | if not size then 157 | log:error("Failed to detect size of RSS item") -- TODO: print item xml as string 158 | if rule:get("minsize") then match_min_size = false end 159 | if rule:get("maxsize") then match_max_size = false end 160 | else 161 | if rule:get("minsize") then match_min_size = size >= tonumber(rule:get("minsize")) end 162 | if rule:get("maxsize") then match_max_size = size <= tonumber(rule:get("maxsize")) end 163 | end 164 | end 165 | log:trace('Matching by min size ">= %s": %s' % { tostring(rule:get("minsize")), tostring(match_min_size) }) 166 | log:trace('Matching by max size "<= %s": %s' % { tostring(rule:get("maxsize")), tostring(match_max_size) }) 167 | return match_min_size and match_max_size 168 | end 169 | 170 | function add_to_rtorrent(rule, item, title) 171 | log:info('Matched RSS rule "%s" by title "%s", adding to rtorrent' % { rule:get("name"), title }) 172 | local url = rss_item_url(item) 173 | if not url then 174 | log:error("Failed to detect link of RSS item") -- TODO: print item xml as string 175 | else 176 | log:debug("Link of matched item: %s" % url) 177 | local data, err, icon 178 | if url:match("magnet:?") then 179 | data, err = bencode.encode({ ["magnet-uri"] = url }) 180 | if not data then 181 | return log:error('Failed to encode torrent with magnet uri "%s": %s' % { url, err }) 182 | end 183 | local magnet, err = common.parse_magnet(url) 184 | if not magnet then 185 | return log:error('Failed to parse magnet uri "%s": %s', { url, err }) 186 | end 187 | icon = common.tracker_icon(common.extract_urls(magnet)) 188 | else 189 | data, err = common.download(url) 190 | if not data then 191 | return log:error('Failed to download torrent "%s": %s' % { url, err }) 192 | end 193 | local torrent, err = bencode.decode(data) 194 | if not torrent then 195 | return log:error('Failed to parse torrent "%s": %s' % { url, err }) 196 | end 197 | icon = common.tracker_icon(array({ url, unpack(common.extract_urls(array(torrent)):get()) })) 198 | end 199 | common.add_to_rtorrent(array() 200 | :set("data", nixio.bin.b64encode(data)) 201 | :set("start", rule:get("autostart")) 202 | :set("directory", rule:get("destdir")) 203 | :set("tags", rule:get("tags")) 204 | :set("icon", icon) 205 | :set("url", not url:match("magnet:?") and url)) 206 | end 207 | end 208 | 209 | function process_item(rules, feed, item) 210 | local title = next_tag(item, "title")[1] 211 | log:debug("Title: %s " % title) 212 | for _, rule in rules:pairs() do 213 | log:trace("Rule: %s" % rule:get("name")) 214 | if filter_by_feed(rule, feed) 215 | and filter_by_title(rule, title) 216 | and filter_by_size(rule, item) then 217 | add_to_rtorrent(rule, item, title) 218 | end 219 | end 220 | end 221 | 222 | --[[ M A I N ]]-- 223 | if #arg == 0 then 224 | io.stderr:write("Usage:\n %s [options]\n\nOptions:\n" % arg[0] 225 | .. " -l, --level Log level\n" 226 | .. " -o, --output Log file. Default: /dev/tty\n" 227 | .. " -u, --uci Read log level and log file from UCI\n") 228 | os.exit(1) 229 | end 230 | 231 | local level, target, i = "INFO", "/dev/tty", 1 232 | while i <= #arg do 233 | if arg[i] == "-l" or arg[i] == "--level" then level, i = arg[i + 1], i + 1 234 | elseif arg[i] == "-o" or arg[i] == "--output" then target, i = arg[i + 1], i + 1 235 | elseif arg[i] == "-u" or arg[i] == "--uci" then 236 | level = uci:get("rtorrent", "logging", "rss_loglevel") or "OFF" 237 | target = uci:get("rtorrent", "logging", "rss_logfile") or "/dev/null" 238 | else 239 | io.stderr:write("Error: invalid option: %s\n" % arg[i]) 240 | os.exit(1) 241 | end 242 | i = i + 1 243 | end 244 | log = logger(level, target) 245 | 246 | local rules = rss_rules() 247 | for _, feed in rss_feeds():pairs() do 248 | log:debug('Processing RSS feed: %s' % feed:get("name")) 249 | local lasthash, lastupdate = 0, 0 250 | if feed:get("lastupdate") then 251 | lasthash, lastupdate = unpack(array(feed:get("lastupdate"):split("@")) 252 | :map(function(value) return tonumber(value) end):get()) 253 | end 254 | local rss = fetch_feed(feed:get("url")) 255 | if rss then 256 | local channel = next_tag(rss, "channel") 257 | local item, index = prev_tag(channel, "item") 258 | while item do 259 | local pubdate = date.to_unix(fix_date(next_tag(item, "pubDate")[1])) 260 | if pubdate >= lastupdate then 261 | local hash = math.abs(nixio.bin.crc32(next_tag(item, "title")[1])) 262 | if hash ~= lasthash then 263 | process_item(rules, feed, item) 264 | lasthash, lastupdate = hash, pubdate 265 | uci:set("rtorrent", feed:get(".name"), "lastupdate", hash .. "@" .. pubdate) 266 | uci:save("rtorrent") 267 | uci:commit("rtorrent") 268 | end 269 | end 270 | item, index = prev_tag(channel, "item", index - 1) 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/main.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | -- Custom fields: 5 | -- d.custom1: tags (space delimited) 6 | -- d.custom5: when "1": delete files from disk on erase 7 | -- d.custom.icon: tracker favicon url 8 | -- d.custom.url: url of torrent (if applicable) 9 | -- d.custom.comment: torrent comment (urlencoded) 10 | 11 | local nixio = require "nixio" 12 | local rtorrent = require "rtorrent" 13 | local common = require "luci.model.cbi.rtorrent.common" 14 | local array = require "luci.model.cbi.rtorrent.array" 15 | local build_url = require "luci.dispatcher".build_url 16 | require "luci.model.cbi.rtorrent.string" 17 | 18 | local compute, format, action, tab, sort, page = {}, {}, {}, unpack(arg) 19 | common.set_cookie("rtorrent-main", { tab, sort, page }) 20 | 21 | tab = tab or "all" 22 | local default_sort = "name-asc" 23 | sort = sort or default_sort 24 | page = page and tonumber(page) or 1 25 | local sort_column, sort_order = unpack(sort:split("-")) 26 | 27 | function compute_values(torrent, index, torrents, ...) 28 | for _, key in ipairs({ "icon", "size", "total_wanted_chunks", "done", "status", 29 | "seeder", "leecher", "down_speed", "up_speed", "eta", "tags" }) do 30 | torrent:set(key, compute[key], index, torrents, ...) 31 | end 32 | return torrent 33 | end 34 | 35 | function compute.icon(key, torrent) return torrent:get("custom_icon") end 36 | function compute.size(key, torrent) return torrent:get("size_bytes") end 37 | function compute.seeder(key, torrent) return torrent:get("peers_complete") end 38 | function compute.leecher(key, torrent) return torrent:get("peers_accounted") end 39 | function compute.down_speed(key, torrent) return torrent:get("down_rate") end 40 | function compute.up_speed(key, torrent) return torrent:get("up_rate") end 41 | 42 | function compute.total_wanted_chunks(key, torrent) 43 | return torrent:get("wanted_chunks") + torrent:get("completed_chunks") 44 | end 45 | 46 | function compute.done(key, torrent) 47 | if torrent:get("total_wanted_chunks") == torrent:get("size_chunks") then 48 | return 100.0 * torrent:get("bytes_done") / torrent:get("size_bytes") 49 | else 50 | return math.min(100.0 * torrent:get("completed_chunks") / torrent:get("total_wanted_chunks"), 100) 51 | end 52 | end 53 | 54 | function compute.status(key, torrent) 55 | -- 1: down, 2: stop, 3: pause, 4: hash, 5: seed, 6: unknown 56 | if torrent:get("hashing") > 0 then return 4 57 | elseif torrent:get("state") == 0 then return 2 58 | elseif torrent:get("state") > 0 then 59 | if torrent:get("is_active") == 0 then return 3 60 | elseif torrent:get("wanted_chunks") > 0 then return 1 61 | else return 5 end 62 | else return 6 end 63 | end 64 | 65 | function compute.eta(key, torrent) 66 | -- 0: already done, math.huge: infinite 67 | if torrent:get("wanted_chunks") == 0 then return 0 68 | elseif torrent:get("down_rate") > 0 then 69 | if torrent:get("total_wanted_chunks") == torrent:get("size_chunks") then 70 | return (torrent:get("size_bytes") - torrent:get("bytes_done")) / torrent:get("down_rate") 71 | else 72 | return torrent:get("wanted_chunks") * torrent:get("chunk_size") / torrent:get("down_rate") 73 | end 74 | else return math.huge end 75 | end 76 | 77 | function compute.tags(key, torrent) 78 | local tags = array(torrent:get("custom1"):split()):insert("all") 79 | if torrent:get("wanted_chunks") > 0 then tags:insert("incomplete") end 80 | return tags:unique():join(" ") 81 | end 82 | 83 | function compute_tabs(torrent, index, torrents, tabs, tab) 84 | if index == 1 then 85 | local torrent_tags = array(torrents:map(function(torrent) 86 | return torrent:get("tags") end):join(" "):split()):unique():sort() 87 | if torrent_tags:contains("incomplete") then tabs:insert("incomplete") end 88 | torrent_tags:foreach(function(tag) tabs:insert(tag) end) 89 | tabs:insert(tab) 90 | end 91 | end 92 | 93 | function compute_total(torrent, index, torrents, total) 94 | total:increment("count") 95 | total:increment("size", torrent:get("size")) 96 | total:increment("down_speed", torrent:get("down_speed")) 97 | total:increment("up_speed", torrent:get("up_speed")) 98 | if torrents:last(index) then 99 | format_values(total:set(".total_row", true) 100 | :set("name", "TOTAL: %d pcs." % total:get("count"))) 101 | end 102 | end 103 | 104 | function format_values(torrent, index, torrents, ...) 105 | for key, value in torrent:pairs() do 106 | torrent:set(key, format[key] 107 | and format[key](value, key, torrent, index, torrents, ...) or value) 108 | end 109 | return torrent 110 | end 111 | 112 | function format.done(value) return "%.1f%%" % value end 113 | function format.seeder(value) return tostring(value) end 114 | function format.leecher(value) return tostring(value) end 115 | function format.down_speed(value) return "%.2f" % (value / 1000) end 116 | function format.up_speed(value) return "%.2f" % (value / 1000) end 117 | 118 | function format.icon(value) 119 | return '' % { 120 | value, common.get_domain(value) 121 | } 122 | end 123 | 124 | function format.name(value, key, torrent) 125 | if torrent:get("hash") then 126 | local url = build_url("admin", "rtorrent", "torrent", "info", torrent:get("hash")) 127 | return '%s' % { url, value } 128 | else 129 | return value 130 | end 131 | end 132 | 133 | function format.size(value) 134 | return '
%s
' % { value, common.human_size(value) } 135 | end 136 | 137 | function format.status(value) 138 | local text, color = "", "" 139 | if value == 1 then text, color = "down", ' class="green"' 140 | elseif value == 2 then text, color = "stop", ' class="red"' 141 | elseif value == 3 then text, color = "pause", ' class="orange"' 142 | elseif value == 4 then text, color = "hash", ' class="green"' 143 | elseif value == 5 then text, color = "seed", ' class="blue"' 144 | elseif value == 6 then text, color = "unknown", "" end 145 | return '%s' % { color, text } 146 | end 147 | 148 | function format.ratio(value, key, torrent) 149 | return '
%.2f
' % { 150 | common.human_size(torrent:get("up_total")), 151 | value < 1000 and "red" or "green", 152 | value / 1000 153 | } 154 | end 155 | 156 | function format.eta(value, key, torrent) 157 | local download_started = tonumber(torrent:get("timestamp_started")) ~= 0 158 | and os.date("!%Y-%m-%d %H:%M:%S", torrent:get("timestamp_started")) 159 | or "not yet started" 160 | local download_finished = tonumber(torrent:get("timestamp_finished")) ~= 0 161 | and os.date("!%Y-%m-%d %H:%M:%S", torrent:get("timestamp_finished")) 162 | or "not yet finished" 163 | local text, color = "", "" 164 | if value == 0 then text, color = "--", "" 165 | elseif value == math.huge then text, color = "∞", ' class="red"' 166 | else text, color = common.human_time(value), "" end 167 | return '
%s
' % { 168 | download_started, download_finished, color, text 169 | } 170 | end 171 | 172 | function action.start(hash) 173 | local status = rtorrent.batchcall("d.", hash, "state", "is_active") 174 | if status.state == 0 then rtorrent.call("d.start", hash) 175 | elseif status.is_active == 0 then rtorrent.call("d.resume", hash) end 176 | end 177 | 178 | function action.pause(hash) rtorrent.batchcall("d.", hash, "start", "pause") end 179 | function action.stop(hash) rtorrent.batchcall("d.", hash, "stop", "close") end 180 | function action.hash(hash) rtorrent.call("d.check_hash", hash) end 181 | function action.remove(hash) rtorrent.batchcall("d.", hash, "close", "erase") end 182 | function action.purge(hash) rtorrent.batchcall("d.", hash, "custom5.set=1", "close", "erase") end 183 | 184 | function filter_by_tab(torrent, index, torrents, tab) 185 | return string.find(" " .. torrent:get("tags") .. " ", " " .. tab .. " ") 186 | end 187 | 188 | local tabs, checked, total = array({ "all" }), array(), array():set("count", 0) 189 | local torrents = array(rtorrent.multicall("d.", "", "default", 190 | "hash", "name", "hashing", "state", "is_active", "complete", 191 | "size_bytes", "bytes_done", "size_chunks", "wanted_chunks", "completed_chunks", "chunk_size", 192 | "peers_accounted", "peers_complete", "down.rate", "up.rate", "ratio", "up.total", 193 | "timestamp.started", "timestamp.finished", "custom1", "custom=icon")) 194 | :foreach(compute_values) 195 | :foreach(compute_tabs, tabs, tab) 196 | :filter(filter_by_tab, tab) 197 | :foreach(compute_total, total) 198 | :sort(sort_column, sort_order) 199 | :limit(10, (page - 1) * 10) 200 | :foreach(format_values) 201 | :insert(total:get("count") > 1 and total or nil) 202 | 203 | local form, list, icon, name, size, done, status, seeder, leecher, down_speed, up_speed, ratio, eta, check 204 | 205 | form = SimpleForm("rtorrent", "Torrent List") 206 | form.submit = false 207 | form.reset = false 208 | form.handle = function(self, state, data) 209 | if state == FORM_VALID then 210 | checked:foreach(function(hash) action[self:formvalue("cbi.action")](hash) end) 211 | luci.http.redirect(nixio.getenv("REQUEST_URI")) 212 | end 213 | return true 214 | end 215 | 216 | list = form:section(Table, torrents:get()) 217 | list.template = "rtorrent/tblsection_main" 218 | list.name = "rtorrent-torrents" 219 | list.pages = common.pagination(total:get("count") or torrents:size(), tonumber(page), 220 | common.pagination_link, build_url("admin", "rtorrent", "main", tab, sort)):join() 221 | list.column = function(self, class, option, title, tooltip, sort_by) 222 | return self:option(class, option, '%s' % { 223 | build_url("admin", "rtorrent", "main", tab, sort_by), 224 | tooltip, sort == sort_by and ' class="active"' or "", title 225 | }) 226 | end 227 | list.action = function(self, key, value, classes) 228 | self.actions[key] = value 229 | self.action_classes[key] = "btn cbi-button important " .. classes 230 | table.insert(self.action_order, key) 231 | end 232 | list.root_path = build_url("admin", "rtorrent", "main") 233 | tabs:unique():foreach(function(tab) list:tab(tab, tab:ucfirst()) end) 234 | list.selected_tab = tabs:contains(tab) and tab or "all" 235 | list.default_sort = default_sort 236 | list.sort = sort 237 | list.actions, list.action_classes, list.action_order = {}, {}, {} 238 | list:action("start", "Start", "cbi-button-save") 239 | list:action("pause", "Pause", "cbi-button-apply") 240 | list:action("stop", "Stop", "cbi-button-apply") 241 | list:action("hash", "Check hash", "cbi-button-apply") 242 | list:action("remove", "Remove", "cbi-button-negative") 243 | list:action("purge", "Remove and delete from disk", "cbi-button-negative") 244 | 245 | icon = list:option(DummyValue, "icon") 246 | icon.rawhtml = true 247 | icon.width = "1%" 248 | icon.classes = { "nowrap" } 249 | 250 | name = list:column(DummyValue, "name", "Name", "Sort by name", "name-asc") 251 | name.rawhtml = true 252 | name.classes = { "wrap" } 253 | 254 | size = list:column(DummyValue, "size", "Size", "Sort by total size", "size-desc") 255 | size.rawhtml = true 256 | size.width = "1%" 257 | size.classes = { "nowrap", "center" } 258 | 259 | done = list:column(DummyValue, "done", "Done", "Sort by download done percent", "done-desc") 260 | done.rawhtml = true 261 | done.width = "1%" 262 | done.classes = { "nowrap", "center" } 263 | 264 | status = list:column(DummyValue, "status", "Status", "Sort by status", "status-asc") 265 | status.rawhtml = true 266 | status.width = "1%" 267 | status.classes = { "nowrap", "center" } 268 | 269 | seeder = list:column(DummyValue, "seeder", "▼", "Sort by seeder count", "seeder-desc") 270 | seeder.rawhtml = true 271 | seeder.width = "1%" 272 | seeder.classes = { "nowrap", "center" } 273 | 274 | leecher = list:column(DummyValue, "leecher", "▲", "Sort by leecher count", "leecher-desc") 275 | leecher.rawhtml = true 276 | leecher.width = "1%" 277 | leecher.classes = { "nowrap", "center" } 278 | 279 | down_speed = list:column(DummyValue, "down_speed", "Down
Speed", 280 | "Sort by download speed (kB/s)", "down_speed-desc") 281 | down_speed.rawhtml = true 282 | down_speed.width = "1%" 283 | down_speed.classes = { "nowrap", "center" } 284 | 285 | up_speed = list:column(DummyValue, "up_speed", "Up
Speed", 286 | "Sort by upload speed (kB/s)", "up_speed-desc") 287 | up_speed.rawhtml = true 288 | up_speed.width = "1%" 289 | up_speed.classes = { "nowrap", "center" } 290 | 291 | ratio = list:column(DummyValue, "ratio", "Ratio", "Sort by download/upload ratio", "ratio-desc") 292 | ratio.rawhtml = true 293 | ratio.width = "1%" 294 | ratio.classes = { "nowrap", "center" } 295 | 296 | eta = list:column(DummyValue, "eta", "ETA", "Sort by Estimated Time of Arrival", "eta-desc") 297 | eta.rawhtml = true 298 | eta.width = "1%" 299 | eta.classes = { "nowrap", "center" } 300 | 301 | check = list:option(Flag, "check") 302 | check.width = "1%" 303 | check.classes = { "nowrap", "center" } 304 | check.write = function(self, section, value) 305 | if torrents:get(section):get("hash") then 306 | checked:insert(torrents:get(section):get("hash")) 307 | end 308 | end 309 | 310 | return form 311 | -------------------------------------------------------------------------------- /src/usr/lib/lua/luci/model/cbi/rtorrent/common.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2014-2021 Sandor Balazsi 2 | -- Licensed to the public under the GNU General Public License. 3 | 4 | local socket = require "socket" 5 | local url = require "socket.url" 6 | local http = require "socket.http" 7 | local https = require "ssl.https" 8 | local ltn12 = require "ltn12" 9 | local nixio = require "nixio" 10 | local util = require "luci.util" 11 | local lhttp = require "luci.http" 12 | local uci = require "luci.model.uci".cursor() 13 | local build_url = require "luci.dispatcher".build_url 14 | local datatypes = require "luci.cbi.datatypes" 15 | local xmlrpc = require "xmlrpc" 16 | local rtorrent = require "rtorrent" 17 | local array = require "luci.model.cbi.rtorrent.array" 18 | require "luci.model.cbi.rtorrent.string" 19 | 20 | local string, table, os, math, unpack = string, table, os, math, unpack 21 | local type, ipairs, tostring, tonumber = type, ipairs, tostring, tonumber 22 | local getmetatable = getmetatable 23 | 24 | module "luci.model.cbi.rtorrent.common" 25 | 26 | function uci_sections(config, stype, name) 27 | local sections = array() 28 | uci:foreach(config, stype, function(section) 29 | if not name or section[".name"] == name then 30 | sections:insert(section) 31 | end 32 | end) 33 | return name and sections:get(1) or sections 34 | end 35 | 36 | function uci_list_add(config, stype, name, option, value) 37 | if not uci:get(config, name) then 38 | uci:set(config, name, stype) 39 | uci:commit(config) 40 | end 41 | local current_values = array(uci:get(config, name, option)) 42 | uci:set(config, name, option, current_values:insert(value):get()) 43 | uci:commit(config) 44 | end 45 | 46 | function human_size(bytes) 47 | local symbol = {[0]="B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"} 48 | local exp = bytes > 0 and math.floor(math.log(bytes) / math.log(1024)) or 0 49 | local value = bytes / math.pow(1024, exp) 50 | local acc = bytes > 0 and 2 - math.floor(math.log10(value)) or 2 51 | if acc < 0 then acc = 0 end 52 | return string.format("%." .. acc .. "f " .. symbol[exp], value) 53 | end 54 | 55 | function human_time(sec) 56 | local t = os.date("!*t", sec) 57 | if t["day"] > 25 then return "∞" 58 | elseif t["day"] > 1 then 59 | return string.format("%dd
%dh %dm", t["day"] - 1, t["hour"], t["min"]) 60 | elseif t["hour"] > 0 then 61 | return string.format("%dh
%dm %ds", t["hour"], t["min"], t["sec"]) 62 | elseif t["min"] > 0 then 63 | return string.format("%dm %ds", t["min"], t["sec"]) 64 | else return string.format("%ds", t["sec"]) end 65 | end 66 | 67 | function parse_magnet(uri) 68 | if not uri:starts("magnet:?") then return nil, "Not a valid magnet URI!" end 69 | local magnet = array() 70 | for key, value in uri:sub(9):gmatch("([^&=]+)=([^&=]+)") do 71 | key = key:gsub("%.%d+$", "") 72 | if not magnet:get(key) then magnet:set(key, array()) end 73 | magnet:get(key):insert(value:urldecode()) 74 | end 75 | if not magnet:keys():contains("xt") 76 | or not magnet:get("xt"):get(1):starts("urn:btih:") then 77 | return nil, "Magnet URI's BitTorrent info hash URN missing!" 78 | end 79 | return magnet 80 | end 81 | 82 | function get_domain(url, sub_domain) 83 | local domain = url:match("^%w+://([^/:]+)") or "" 84 | return sub_domain == true or datatypes.ipaddr(domain) and domain or domain:match("(%w+%.%w+)$") or "" 85 | end 86 | 87 | function tracker_icon(urls) 88 | for _, domain in urls:map(get_domain):filter(string.not_blank):unique():pairs() do 89 | local favicon = "http://" .. domain .. "/favicon.ico" 90 | if array(uci:get("rtorrent", "icon", "has")):contains(domain) then 91 | return favicon 92 | elseif not array(uci:get("rtorrent", "icon", "not")):contains(domain) then 93 | local icon, err, headers = download(favicon, 2) 94 | if icon and headers:get("content-type"):starts("image/") and #icon > 0 then 95 | uci_list_add("rtorrent", "frontend", "icon", "has", domain) 96 | return favicon 97 | else 98 | uci_list_add("rtorrent", "frontend", "icon", "not", domain) 99 | end 100 | end 101 | end 102 | return "/luci-static/resources/icons/unknown_tracker.svg" 103 | end 104 | 105 | function extract_urls(data) 106 | local urls = array() 107 | data:traverse(function(value) 108 | if type(value) == "string" and value:match("^%w+://%w+%.%w+") then 109 | urls:insert(value) 110 | end 111 | end) 112 | return urls:unique() 113 | end 114 | 115 | function add_to_rtorrent(torrent) 116 | local params = array() 117 | params:insert(torrent:get("start") == "1" and "load.raw_start" or "load.raw") 118 | params:insert("") -- target 119 | params:insert(xmlrpc.newTypedValue(torrent:get("data"), "base64")) 120 | params:insert('d.directory.set="%s"' % torrent:get("directory")) 121 | if torrent:get("tags") then params:insert('d.custom1.set="%s"' % torrent:get("tags")) end 122 | if torrent:get("icon") then params:insert('d.custom.set=icon, "%s"' % torrent:get("icon")) end 123 | if torrent:get("url") then params:insert('d.custom.set=url, "%s"' % torrent:get("url"):urlencode()) end 124 | rtorrent.call(unpack(params:get())) 125 | end 126 | 127 | function download(url, timeout) 128 | local proto = url:starts("https://") and https or http 129 | proto.TIMEOUT = timeout or 5 130 | local response_chunks = {} 131 | local request_headers = { 132 | ["Referer"] = "https://www.google.com", 133 | ["User-Agent"] = "unknown" 134 | } 135 | local cookie_section = uci_sections("rtorrent", "cookies") 136 | :filter(function(section) return get_domain(url, true) == section.domain end) 137 | :first() 138 | if cookie_section then request_headers["Cookie"] = cookie_section.cookie end 139 | local body, code, headers, status = proto.request({ 140 | url = url, 141 | method = "GET", 142 | headers = request_headers, 143 | redirect = (proto.PORT == 80) and true or nil, 144 | sink = ltn12.sink.table(response_chunks) 145 | }) 146 | if not body then return nil, code, array(headers), code, status end 147 | if code == 301 or code == 302 then return download(headers["location"]) 148 | elseif code == 200 then 149 | return table.concat(response_chunks), nil, array(headers), code, status 150 | else return nil, status, array(headers), code, status end 151 | end 152 | 153 | function pagination_link(page, current_page, last_page, root_path) 154 | local active = (page == current_page) and ' class="active"' or "" 155 | local text 156 | if page == "previous" then text, page = "<", math.max(current_page - 1, 1) 157 | elseif page == "next" then text, page = ">", math.min(current_page + 1, last_page) 158 | elseif page == "left-ellipsis" then text, page = "…", math.max(current_page - 10, 1) 159 | elseif page == "right-ellipsis" then text, page = "…", math.min(current_page + 10, last_page) 160 | else text = tostring(page) end 161 | return '%s' % { root_path, page, active, text } 162 | end 163 | 164 | function pagination(count, current_page, button_builder, ...) 165 | local pages = array() 166 | local last_page = math.floor(count / 10) + (count % 10 == 0 and 0 or 1) 167 | if last_page < 2 then return pages end 168 | current_page = current_page > last_page and last_page or current_page 169 | pages:insert(button_builder("previous", current_page, last_page, ...)) 170 | pages:insert(button_builder(1, current_page, last_page, ...)) 171 | pages:insert(last_page > 9 and current_page > 7 172 | and button_builder("left-ellipsis", current_page, last_page, ...) 173 | or button_builder(2, current_page, last_page, ...)) 174 | for column = 3, 9 do 175 | if last_page >= column then 176 | if last_page < 10 or (column < 8 and current_page < 8) then 177 | pages:insert(button_builder(column, current_page, last_page, ...)) 178 | elseif column == 8 then 179 | if current_page < 8 or (last_page > 14 and current_page < last_page - 6) then 180 | pages:insert(button_builder("right-ellipsis", current_page, last_page, ...)) 181 | else pages:insert(button_builder(last_page - 1, current_page, last_page, ...)) end 182 | elseif column == 9 then 183 | pages:insert(button_builder(last_page, current_page, last_page, ...)) 184 | elseif current_page > last_page - 7 then 185 | pages:insert(button_builder(last_page + column - 9, current_page, last_page, ...)) 186 | else pages:insert(button_builder(current_page + column - 5, current_page, last_page, ...)) end 187 | end 188 | end 189 | pages:insert(button_builder("next", current_page, last_page, ...)) 190 | return pages 191 | end 192 | 193 | function system_uptime() 194 | return nixio.fs.readfile("/proc/uptime"):split()[1] 195 | end 196 | 197 | function process_cmdline(pid) 198 | return array(nixio.fs.readfile("/proc/%d/cmdline" % pid):split("%z")) 199 | end 200 | 201 | function process_env(pid) 202 | local env = array() 203 | for _, entry in ipairs(nixio.fs.readfile("/proc/%d/environ" % pid):split("%z")) do 204 | env:set(unpack(entry:split("=", 2))) 205 | end 206 | return env 207 | end 208 | 209 | -- https://man7.org/linux/man-pages/man5/proc.5.html 210 | function process_stat(pid) 211 | local stat = nixio.fs.readfile("/proc/%d/stat" % pid) 212 | local fields, end_index, value = array() 213 | _, end_index, value = stat:find("(%d+)"); fields:set("pid", tonumber(value)) 214 | _, end_index, fields.table["comm"] = stat:find("%(([^)]+)%)", end_index + 2) 215 | _, end_index, fields.table["state"] = stat:find("(%a)", end_index + 2) 216 | for _, key in ipairs({ "ppid", "pgrp", "session", "tty_nr", "tpgid", "flags", "minflt", "cminflt", 217 | "majflt", "cmajflt", "utime", "stime", "cutime", "cstime", "priority", "nice", 218 | "num_threads", "itrealvalue", "starttime", "vsize", "rss", "rsslim", "startcode", 219 | "endcode", "startstack", "kstkesp", "kstkeip", "signal", "blocked", "sigignore", 220 | "sigcatch", "wchan", "nswap", "cnswap", "exit_signal", "processor", "rt_priority", 221 | "policy", "delayacct_blkio_ticks", "guest_time", "cguest_time", "start_data", 222 | "end_data", "start_brk", "arg_start", "arg_end", "env_start", "env_end", "exit_code" }) do 223 | _, end_index, value = stat:find("([-%d]+)", end_index + 2) 224 | fields:set(key, tonumber(value)) 225 | end 226 | return fields 227 | end 228 | 229 | function process_start(pid) 230 | local clk_tck = 100 -- TODO: find out value from sysconf(_SC_CLK_TCK) 231 | local process_start_time = process_stat(pid):get("starttime") / clk_tck 232 | local current_time = socket.gettime() 233 | return math.floor(current_time - system_uptime() + process_start_time) 234 | end 235 | 236 | function rtorrent_config(rtorrent_pid) 237 | rtorrent_pid = rtorrent_pid or rtorrent.call("system.pid") 238 | local rtorrent_config_file = process_env(rtorrent_pid):get("HOME") .. "/.rtorrent.rc" 239 | for _, arg in process_cmdline(rtorrent_pid):pairs() do 240 | if arg:starts("import=") then rtorrent_config_file = arg:sub(8) end 241 | end 242 | return array(nixio.fs.stat(rtorrent_config_file, "type") == "reg" 243 | and nixio.fs.readfile(rtorrent_config_file):split("\n") or {}) 244 | end 245 | 246 | function rtorrent_schedule_parse(schedule) 247 | local time = schedule:split(":") 248 | if #time == 1 then 249 | return time[1] 250 | elseif #time == 2 then 251 | return time[1] * 60 + time[2] 252 | elseif #time == 3 then 253 | return time[1] * 60 * 60 + time[2] * 60 + time[3] 254 | elseif #time == 4 then 255 | return time[1] * 24 * 60 * 60 + time[2] * 60 * 60 + time[3] * 60 + time[4] 256 | end 257 | end 258 | 259 | function rtorrent_schedule_start(rtorrent_start, second) 260 | local time, now = os.date("*t", rtorrent_start), socket.gettime() 261 | local start = rtorrent_start - time.hour * 60 * 60 - time.min * 60 - time.sec + second 262 | if now - rtorrent_start < 24 * 60 * 60 and start < rtorrent_start then start = start + 24 * 60 * 60 end 263 | return start 264 | end 265 | 266 | function rss_downloader_status() 267 | local rtorrent_pid, schedule = rtorrent.call("system.pid") 268 | for _, line in rtorrent_config(rtorrent_pid):pairs() do 269 | if line:match("^%s*schedule.*execute.*/usr/lib/lua/rss_downloader.lua") then 270 | schedule = line:split(",") 271 | end 272 | end 273 | if schedule then 274 | local rtorrent_start = process_start(rtorrent_pid) 275 | local start, start_text 276 | if schedule[2]:match(":") then 277 | start = rtorrent_schedule_start(rtorrent_start, rtorrent_schedule_parse(schedule[2])) 278 | start_text = "" .. schedule[2] .. " start time" 279 | else 280 | start = rtorrent_start + schedule[2] 281 | start_text = "" .. schedule[2] .. " seconds initial delay" 282 | end 283 | local interval = rtorrent_schedule_parse(schedule[3]) 284 | local interval_text = "" .. schedule[3] .. "" .. (schedule[3]:match(":") and "" or " seconds") 285 | return "RSS Downloader is scheduled by rTorrent with %s and %s interval.
" 286 | % { start_text, interval_text } 287 | .. 'The next fetch of RSS feed(s) will be at .' 288 | .. '' 289 | % { start, interval } 290 | else 291 | return 'Warning! RSS Downloader not scheduled by rTorrent! ' 292 | .. 'Please add a schedule2 line to your rTorrent config file (/root/.rtorrent.rc).
' 295 | .. "For example, to trigger it every 300 seconds, " 296 | .. "with an initial delay of 60 seconds after rTorrent startup:
schedule2 = " 297 | .. "rss_downloader, 60, 300, ((execute.throw, /usr/lib/lua/rss_downloader.lua, --uci))" 298 | end 299 | end 300 | 301 | function set_cookie(name, data, attributes) 302 | attributes = attributes or "" 303 | lhttp.header("Set-Cookie", "%s=%s; Path=%s; SameSite=Strict%s" % { 304 | name, data and util.serialize_data(data):urlencode() or "", build_url("admin", "rtorrent"), attributes 305 | }) 306 | end 307 | 308 | function get_cookie(name, default) 309 | local cookie = lhttp.getcookie(name) 310 | return cookie and util.restore_data(cookie) or default 311 | end 312 | 313 | function remove_cookie(name) 314 | set_cookie(name, nil, "; Max-Age=0") 315 | end 316 | -------------------------------------------------------------------------------- /src/usr/lib/lua/xmlrpc/init.lua: -------------------------------------------------------------------------------- 1 | -- Copyright 2003-2010 Kepler Project 2 | -- XML-RPC implementation for Lua. 3 | 4 | local lxp = require "lxp" 5 | local lom = require "lxp.lom" 6 | 7 | local assert, error, ipairs, pairs, select, type, tonumber, unpack = assert, error, ipairs, pairs, select, type, tonumber, unpack 8 | local format, gsub, strfind, strsub = string.format, string.gsub, string.find, string.sub 9 | local concat, tinsert = table.concat, table.insert 10 | local ceil = math.ceil 11 | local parse = lom.parse 12 | 13 | module (...) 14 | 15 | _COPYRIGHT = "Copyright (C) 2003-2014 Kepler Project" 16 | _DESCRIPTION = "LuaXMLRPC is a library to make remote procedure calls using XML-RPC" 17 | _PKGNAME = "LuaXMLRPC" 18 | _VERSION_MAJOR = 1 19 | _VERSION_MINOR = 2 20 | _VERSION_MICRO = 2 21 | _VERSION = _VERSION_MAJOR .. "." .. _VERSION_MINOR .. "." .. _VERSION_MICRO 22 | 23 | --------------------------------------------------------------------- 24 | -- XML-RPC Parser 25 | --------------------------------------------------------------------- 26 | 27 | --------------------------------------------------------------------- 28 | local function trim (s) 29 | return (type(s) == "string" and gsub (s, "^%s*(.-)%s*$", "%1")) 30 | end 31 | 32 | --------------------------------------------------------------------- 33 | local function is_space (s) 34 | return type(s) == "string" and trim(s) == "" 35 | end 36 | 37 | --------------------------------------------------------------------- 38 | -- Get next non-space element from tab starting from index i. 39 | -- @param tab Table. 40 | -- @param i Numeric index. 41 | -- @return Object and its position on table; nil and an invalid index 42 | -- when there is no more elements. 43 | --------------------------------------------------------------------- 44 | function next_nonspace (tab, i) 45 | if not i then i = 1 end 46 | while is_space (tab[i]) do i = i+1 end 47 | return tab[i], i 48 | end 49 | 50 | --------------------------------------------------------------------- 51 | -- Get next element of tab with the given tag starting from index i. 52 | -- @param tab Table. 53 | -- @param tag String with the name of the tag. 54 | -- @param i Numeric index. 55 | -- @return Object and its position on table; nil and an invalid index 56 | -- when there is no more elements. 57 | --------------------------------------------------------------------- 58 | local function next_tag (tab, tag, i) 59 | if not i then i = 1 end 60 | while tab[i] do 61 | if type (tab[i]) == "table" and tab[i].tag == tag then 62 | return tab[i], i 63 | end 64 | i = i + 1 65 | end 66 | return nil, i 67 | end 68 | 69 | --------------------------------------------------------------------- 70 | local function x2number (tab) 71 | if tab.tag == "int" or tab.tag == "i4" or tab.tag == "i8" or tab.tag == "double" then 72 | return tonumber (next_nonspace (tab, 1), 10) 73 | end 74 | end 75 | 76 | --------------------------------------------------------------------- 77 | local function x2boolean (tab) 78 | if tab.tag == "boolean" then 79 | local v = next_nonspace (tab, 1) 80 | return v == true or v == "true" or tonumber (v) == 1 or false 81 | end 82 | end 83 | 84 | --------------------------------------------------------------------- 85 | local function x2string (tab) 86 | return tab.tag == "string" and (tab[1] or "") 87 | end 88 | 89 | --------------------------------------------------------------------- 90 | local function x2date (tab) 91 | return tab.tag == "dateTime.iso8601" and next_nonspace (tab, 1) 92 | end 93 | 94 | --------------------------------------------------------------------- 95 | local function x2base64 (tab) 96 | return tab.tag == "base64" and next_nonspace (tab, 1) 97 | end 98 | 99 | --------------------------------------------------------------------- 100 | local function x2name (tab) 101 | return tab.tag == "name" and next_nonspace (tab, 1) 102 | end 103 | 104 | local x2value 105 | 106 | --------------------------------------------------------------------- 107 | -- Disassemble a member object in its name and value parts. 108 | -- @param tab Table with a DOM representation. 109 | -- @return String (name) and Object (value). 110 | -- @see x2name, x2value. 111 | --------------------------------------------------------------------- 112 | local function x2member (tab) 113 | return 114 | x2name (next_tag(tab,"name")), 115 | x2value (next_tag(tab,"value")) 116 | end 117 | 118 | --------------------------------------------------------------------- 119 | -- Disassemble a struct object into a Lua table. 120 | -- @param tab Table with DOM representation. 121 | -- @return Table with "name = value" pairs. 122 | --------------------------------------------------------------------- 123 | local function x2struct (tab) 124 | if tab.tag == "struct" then 125 | local res = {} 126 | for i = 1, #tab do 127 | if not is_space (tab[i]) then 128 | local name, val = x2member (tab[i]) 129 | res[name] = val 130 | end 131 | end 132 | return res 133 | end 134 | end 135 | 136 | --------------------------------------------------------------------- 137 | -- Disassemble an array object into a Lua table. 138 | -- @param tab Table with DOM representation. 139 | -- @return Table. 140 | --------------------------------------------------------------------- 141 | local function x2array (tab) 142 | if tab.tag == "array" then 143 | local d = next_tag (tab, "data") 144 | local res = {} 145 | for i = 1, #d do 146 | if not is_space (d[i]) then 147 | tinsert (res, x2value (d[i])) 148 | end 149 | end 150 | return res 151 | end 152 | end 153 | 154 | --------------------------------------------------------------------- 155 | local xmlrpc_types = { 156 | int = x2number, 157 | i4 = x2number, 158 | i8 = x2number, 159 | boolean = x2boolean, 160 | string = x2string, 161 | double = x2number, 162 | ["dateTime.iso8601"] = x2date, 163 | base64 = x2base64, 164 | struct = x2struct, 165 | array = x2array, 166 | } 167 | 168 | local x2param, x2fault 169 | 170 | --------------------------------------------------------------------- 171 | -- Disassemble a methodResponse into a Lua object. 172 | -- @param tab Table with DOM representation. 173 | -- @return Boolean (indicating wether the response was successful) 174 | -- and (a Lua object representing the return values OR the fault 175 | -- string and the fault code). 176 | --------------------------------------------------------------------- 177 | local function x2methodResponse (tab) 178 | assert (type(tab) == "table", "Not a table") 179 | assert (tab.tag == "methodResponse", 180 | "Not a `methodResponse' tag: "..tab.tag) 181 | local t = next_nonspace (tab, 1) 182 | if t.tag == "params" then 183 | return true, unpack (x2param (t)) 184 | elseif t.tag == "fault" then 185 | local f = x2fault (t) 186 | return false, f.faultString, f.faultCode 187 | else 188 | error ("Couldn't find a nor a element") 189 | end 190 | end 191 | 192 | --------------------------------------------------------------------- 193 | -- Disassemble a value element into a Lua object. 194 | -- @param tab Table with DOM representation. 195 | -- @return Object. 196 | --------------------------------------------------------------------- 197 | x2value = function (tab) 198 | local t = tab.tag 199 | assert (t == "value", "Not a `value' tag: "..t) 200 | local n = next_nonspace (tab) 201 | if type(n) == "string" or type(n) == "number" then 202 | return n 203 | elseif type (n) == "table" then 204 | local t = n.tag 205 | local get = xmlrpc_types[t] 206 | if not get then error ("Invalid <"..t.."> element") end 207 | return get (next_nonspace (tab)) 208 | elseif type(n) == "nil" then 209 | -- the next best thing is to assume it's an empty string 210 | return "" 211 | 212 | end 213 | end 214 | 215 | --------------------------------------------------------------------- 216 | -- Disassemble a fault element into a Lua object. 217 | -- @param tab Table with DOM representation. 218 | -- @return Object. 219 | --------------------------------------------------------------------- 220 | x2fault = function (tab) 221 | assert (tab.tag == "fault", "Not a `fault' tag: "..tab.tag) 222 | return x2value (next_nonspace (tab)) 223 | end 224 | 225 | --------------------------------------------------------------------- 226 | -- Disassemble a param element into a Lua object. 227 | -- Ignore white spaces between elements. 228 | -- @param tab Table with DOM representation. 229 | -- @return Object. 230 | --------------------------------------------------------------------- 231 | x2param = function (tab) 232 | assert (tab.tag == "params", "Not a `params' tag") 233 | local res = {} 234 | local p, i = next_nonspace (tab, 1) 235 | while p do 236 | if p.tag == "param" then 237 | tinsert (res, x2value (next_tag (p, "value"))) 238 | end 239 | p, i = next_nonspace (tab, i+1) 240 | end 241 | return res 242 | end 243 | 244 | --------------------------------------------------------------------- 245 | -- Disassemble a methodName element into a Lua object. 246 | -- @param tab Table with DOM representation. 247 | -- @return Object. 248 | --------------------------------------------------------------------- 249 | local function x2methodName (tab) 250 | assert (tab.tag == "methodName", "Not a `methodName' tag: "..tab.tag) 251 | return (next_nonspace (tab, 1)) 252 | end 253 | 254 | --------------------------------------------------------------------- 255 | -- Disassemble a methodCall element into its name and a list of parameters. 256 | -- @param tab Table with DOM representation. 257 | -- @return Object. 258 | --------------------------------------------------------------------- 259 | local function x2methodCall (tab) 260 | assert (tab.tag == "methodCall", "Not a `methodCall' tag: "..tab.tag) 261 | return 262 | x2methodName (next_tag (tab,"methodName")), 263 | x2param (next_tag (tab,"params")) 264 | end 265 | 266 | --------------------------------------------------------------------- 267 | -- End of XML-RPC Parser 268 | --------------------------------------------------------------------- 269 | 270 | --------------------------------------------------------------------- 271 | -- Convert a Lua Object into an XML-RPC string. 272 | --------------------------------------------------------------------- 273 | 274 | --------------------------------------------------------------------- 275 | local formats = { 276 | boolean = "%d", 277 | number = "%d", 278 | string = "%s", 279 | base64 = "%s", 280 | 281 | array = "\n%s\n", 282 | double = "%s", 283 | int = "%s", 284 | struct = "%s", 285 | 286 | member = "%s%s", 287 | value = "%s", 288 | 289 | param = "%s", 290 | 291 | params = [[ 292 | 293 | %s 294 | ]], 295 | 296 | fault = [[ 297 | 298 | %s 299 | ]], 300 | 301 | methodCall = [[ 302 | 303 | 304 | %s 305 | %s 306 | 307 | ]], 308 | 309 | methodResponse = [[ 310 | 311 | 312 | %s 313 | ]], 314 | } 315 | formats.table = formats.struct 316 | 317 | local toxml = {} 318 | toxml.double = function (v,t) return format (formats.double, v) end 319 | toxml.int = function (v,t) return format (formats.int, v) end 320 | toxml.string = function (v,t) return format (formats.string, v) end 321 | toxml.base64 = function (v,t) return format (formats.base64, v) end 322 | 323 | --------------------------------------------------------------------- 324 | -- Build a XML-RPC representation of a boolean. 325 | -- @param v Object. 326 | -- @return String. 327 | --------------------------------------------------------------------- 328 | function toxml.boolean (v) 329 | local n = (v and 1) or 0 330 | return format (formats.boolean, n) 331 | end 332 | 333 | --------------------------------------------------------------------- 334 | -- Build a XML-RPC representation of a number. 335 | -- @param v Object. 336 | -- @param t Object representing the XML-RPC type of the value. 337 | -- @return String. 338 | --------------------------------------------------------------------- 339 | function toxml.number (v, t) 340 | local tt = (type(t) == "table") and t["*type"] 341 | if tt == "int" or tt == "i4" or tt == "i8" then 342 | return toxml.int (v, t) 343 | elseif tt == "double" then 344 | return toxml.double (v, t) 345 | elseif v == ceil(v) then 346 | return toxml.int (v, t) 347 | else 348 | return toxml.double (v, t) 349 | end 350 | end 351 | 352 | --------------------------------------------------------------------- 353 | -- @param typ Object representing a type. 354 | -- @return Function that generate an XML element of the given type. 355 | -- The object could be a string (as usual in Lua) or a table with 356 | -- a field named "type" that should be a string with the XML-RPC 357 | -- type name. 358 | --------------------------------------------------------------------- 359 | local function format_func (typ) 360 | if type (typ) == "table" then 361 | return toxml[typ.type] 362 | else 363 | return toxml[typ] 364 | end 365 | end 366 | 367 | --------------------------------------------------------------------- 368 | -- @param val Object representing an array of values. 369 | -- @param typ Object representing the type of the value. 370 | -- @return String representing the equivalent XML-RPC value. 371 | --------------------------------------------------------------------- 372 | function toxml.array (val, typ) 373 | local ret = {} 374 | local et = typ.elemtype 375 | local f = format_func (et) 376 | for i,v in ipairs (val) do 377 | if et and et ~= "array" then 378 | tinsert (ret, format (formats.value, f (v, et))) 379 | else 380 | local ct,cv = type_val(v) 381 | local cf = format_func(ct) 382 | tinsert (ret, format (formats.value, cf(cv, ct))) 383 | end 384 | 385 | end 386 | return format (formats.array, concat (ret, '\n')) 387 | end 388 | 389 | --------------------------------------------------------------------- 390 | --------------------------------------------------------------------- 391 | function toxml.struct (val, typ) 392 | local ret = {} 393 | if type (typ) == "table" then 394 | for n,t in pairs (typ.elemtype) do 395 | local f = format_func (t) 396 | tinsert (ret, format (formats.member, n, f (val[n], t))) 397 | end 398 | else 399 | for i, v in pairs (val) do 400 | tinsert (ret, toxml.member (i, v)) 401 | end 402 | end 403 | return format (formats.struct, concat (ret)) 404 | end 405 | 406 | toxml.table = toxml.struct 407 | 408 | --------------------------------------------------------------------- 409 | --------------------------------------------------------------------- 410 | function toxml.member (n, v) 411 | return format (formats.member, n, toxml.value (v)) 412 | end 413 | 414 | --------------------------------------------------------------------- 415 | -- Get type and value of object. 416 | --------------------------------------------------------------------- 417 | function type_val (obj) 418 | local t = type (obj) 419 | local v = obj 420 | if t == "table" then 421 | t = obj["*type"] or "table" 422 | v = obj["*value"] or obj 423 | end 424 | return t, v 425 | end 426 | 427 | --------------------------------------------------------------------- 428 | -- Convert a Lua object to a XML-RPC object (plain string). 429 | --------------------------------------------------------------------- 430 | function toxml.value (obj) 431 | local to, val = type_val (obj) 432 | if type(to) == "table" then 433 | return format (formats.value, toxml[to.type] (val, to)) 434 | else 435 | -- primitive (not structured) types. 436 | --return format (formats[to], val) 437 | return format (formats.value, toxml[to] (val, to)) 438 | end 439 | end 440 | 441 | --------------------------------------------------------------------- 442 | -- @param ... List of parameters. 443 | -- @return String representing the `params' XML-RPC element. 444 | --------------------------------------------------------------------- 445 | function toxml.params (...) 446 | local params_list = {} 447 | for i = 1, select ("#", ...) do 448 | params_list[i] = format (formats.param, toxml.value (select (i, ...))) 449 | end 450 | return format (formats.params, concat (params_list, '\n ')) 451 | end 452 | 453 | --------------------------------------------------------------------- 454 | -- @param method String with method's name. 455 | -- @param ... List of parameters. 456 | -- @return String representing the `methodCall' XML-RPC element. 457 | --------------------------------------------------------------------- 458 | function toxml.methodCall (method, ...) 459 | local idx = strfind (method, "[^A-Za-z_.:/0-9]") 460 | if idx then 461 | error (format ("Invalid character `%s'", strsub (method, idx, idx))) 462 | end 463 | return format (formats.methodCall, method, toxml.params (...)) 464 | end 465 | 466 | --------------------------------------------------------------------- 467 | -- @param err String with error message. 468 | -- @return String representing the `fault' XML-RPC element. 469 | --------------------------------------------------------------------- 470 | function toxml.fault (err) 471 | local code 472 | local message = err 473 | if type (err) == "table" then 474 | code = err.code 475 | message = err.message 476 | end 477 | return format (formats.fault, toxml.value { 478 | faultCode = { ["*type"] = "int", ["*value"] = code or err.faultCode or 1 }, 479 | faultString = message or err.faultString or "fatal error", 480 | }) 481 | end 482 | 483 | --------------------------------------------------------------------- 484 | -- @param ok Boolean indicating if the response was correct or a 485 | -- fault one. 486 | -- @param params Object containing the response contents. 487 | -- @return String representing the `methodResponse' XML-RPC element. 488 | --------------------------------------------------------------------- 489 | function toxml.methodResponse (ok, params) 490 | local resp 491 | if ok then 492 | resp = toxml.params (params) 493 | else 494 | resp = toxml.fault (params) 495 | end 496 | return format (formats.methodResponse, resp) 497 | end 498 | 499 | --------------------------------------------------------------------- 500 | -- End of converter from Lua to XML-RPC. 501 | --------------------------------------------------------------------- 502 | 503 | 504 | --------------------------------------------------------------------- 505 | -- Create a representation of an array with the given element type. 506 | --------------------------------------------------------------------- 507 | function newArray (elemtype) 508 | return { type = "array", elemtype = elemtype, } 509 | end 510 | 511 | --------------------------------------------------------------------- 512 | -- Create a representation of a structure with the given members. 513 | --------------------------------------------------------------------- 514 | function newStruct (members) 515 | return { type = "struct", elemtype = members, } 516 | end 517 | 518 | --------------------------------------------------------------------- 519 | -- Create a representation of a value according to a type. 520 | -- @param val Any Lua value. 521 | -- @param typ A XML-RPC type. 522 | --------------------------------------------------------------------- 523 | function newTypedValue (val, typ) 524 | return { ["*type"] = typ, ["*value"] = val } 525 | end 526 | 527 | --------------------------------------------------------------------- 528 | -- Create the XML-RPC string used to call a method. 529 | -- @param method String with method name. 530 | -- @param ... Parameters to the call. 531 | -- @return String with the XML string/document. 532 | --------------------------------------------------------------------- 533 | function clEncode (method, ...) 534 | return toxml.methodCall (method, ...) 535 | end 536 | 537 | --------------------------------------------------------------------- 538 | -- Convert the method response document to a Lua table. 539 | -- @param meth_resp String with XML document. 540 | -- @return Boolean indicating whether the call was successful or not; 541 | -- and a Lua object with the converted response element. 542 | --------------------------------------------------------------------- 543 | function clDecode (meth_resp) 544 | local d = parse (meth_resp) 545 | if type(d) ~= "table" then 546 | error ("Not an XML document: "..meth_resp) 547 | end 548 | return x2methodResponse (d) 549 | end 550 | 551 | --------------------------------------------------------------------- 552 | -- Convert the method call (client request) document to a name and 553 | -- a list of parameters. 554 | -- @param request String with XML document. 555 | -- @return String with method's name AND the table of arguments. 556 | --------------------------------------------------------------------- 557 | function srvDecode (request) 558 | local d = parse (request) 559 | if type(d) ~= "table" then 560 | error ("Not an XML document: "..request) 561 | end 562 | return x2methodCall (d) 563 | end 564 | 565 | --------------------------------------------------------------------- 566 | -- Convert a table into an XML-RPC methodReponse element. 567 | -- @param obj Lua object. 568 | -- @param is_fault Boolean indicating wether the result should be 569 | -- a `fault' element (default = false). 570 | -- @return String with XML-RPC response. 571 | --------------------------------------------------------------------- 572 | function srvEncode (obj, is_fault) 573 | local ok = not (is_fault or false) 574 | return toxml.methodResponse (ok, obj) 575 | end 576 | 577 | --------------------------------------------------------------------- 578 | -- Register the methods. 579 | -- @param tab_or_func Table or mapping function. 580 | -- If a table is given, it can have one level of objects and then the 581 | -- methods; 582 | -- if a function is given, it will be used as the dispatcher. 583 | -- The given function should return a Lua function that implements. 584 | --------------------------------------------------------------------- 585 | dispatch = error 586 | function srvMethods (tab_or_func) 587 | local t = type (tab_or_func) 588 | if t == "function" then 589 | dispatch = tab_or_func 590 | elseif t == "table" then 591 | dispatch = function (name) 592 | local ok, _, obj, method = strfind (name, "^([^.]+)%.(.+)$") 593 | if not ok then 594 | return tab_or_func[name] 595 | else 596 | if tab_or_func[obj] and tab_or_func[obj][method] then 597 | return function (...) 598 | return tab_or_func[obj][method] (obj, ...) 599 | end 600 | else 601 | return nil 602 | end 603 | end 604 | end 605 | else 606 | error ("Argument is neither a table nor a function") 607 | end 608 | end 609 | --------------------------------------------------------------------------------