mirror of
https://git.deluge-torrent.org/deluge
synced 2025-08-04 07:28:39 +00:00
sync webui 112 and translate
This commit is contained in:
parent
019ea1f601
commit
cd2f45da92
22 changed files with 810 additions and 332 deletions
|
@ -29,7 +29,7 @@
|
||||||
# this exception statement from your version. If you delete this exception
|
# this exception statement from your version. If you delete this exception
|
||||||
|
|
||||||
plugin_name = _("Web User Interface")
|
plugin_name = _("Web User Interface")
|
||||||
plugin_author = _("Martijn Voncken")
|
plugin_author = "Martijn Voncken"
|
||||||
plugin_version = "rev."
|
plugin_version = "rev."
|
||||||
plugin_description = _("""A Web based User Interface
|
plugin_description = _("""A Web based User Interface
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ There is support for multiple templates, but just one is included.
|
||||||
|
|
||||||
Other contributors:
|
Other contributors:
|
||||||
*somedude : template enhancements.
|
*somedude : template enhancements.
|
||||||
|
*markybob : stability : synced with his changes in deluge-svn.
|
||||||
""")
|
""")
|
||||||
|
|
||||||
import deluge.common
|
import deluge.common
|
||||||
|
@ -80,9 +80,10 @@ class plugin_WebUi(object):
|
||||||
self.web_server = None
|
self.web_server = None
|
||||||
if not deluge.common.windows_check():
|
if not deluge.common.windows_check():
|
||||||
import commands
|
import commands
|
||||||
status = commands.getstatusoutput('ps x |grep -v grep |grep run_webserver')
|
status = commands.getstatusoutput(
|
||||||
|
'ps x |grep -v grep |grep run_webserver')
|
||||||
if status[0] == 0:
|
if status[0] == 0:
|
||||||
os.kill(status[1].split()[0], 9)
|
os.kill(int(status[1].split()[0]), 9)
|
||||||
time.sleep(1) #safe time to wait for kill to finish.
|
time.sleep(1) #safe time to wait for kill to finish.
|
||||||
self.config_file = deluge.common.CONFIG_DIR + "/webui.conf"
|
self.config_file = deluge.common.CONFIG_DIR + "/webui.conf"
|
||||||
self.config = deluge.pref.Preferences(self.config_file, False)
|
self.config = deluge.pref.Preferences(self.config_file, False)
|
||||||
|
@ -95,9 +96,6 @@ class plugin_WebUi(object):
|
||||||
if not self.config.get('port'): #ugly way to detect new config file.
|
if not self.config.get('port'): #ugly way to detect new config file.
|
||||||
#set default values:
|
#set default values:
|
||||||
self.config.set("port", 8112)
|
self.config.set("port", 8112)
|
||||||
#future->use deluge-core setting for download_dir (if it is set)
|
|
||||||
self.config.set("download_dir", os.path.expanduser("~"))
|
|
||||||
self.config.set("torrent_dir", os.path.expanduser("~"))
|
|
||||||
self.config.set("button_style", 2)
|
self.config.set("button_style", 2)
|
||||||
self.config.set("auto_refresh", False)
|
self.config.set("auto_refresh", False)
|
||||||
self.config.set("auto_refresh_secs", 4)
|
self.config.set("auto_refresh_secs", 4)
|
||||||
|
@ -112,10 +110,8 @@ class plugin_WebUi(object):
|
||||||
self.config.set("cache_templates", True)
|
self.config.set("cache_templates", True)
|
||||||
|
|
||||||
if deluge.common.windows_check():
|
if deluge.common.windows_check():
|
||||||
if self.config.get("run_in_thread") == None:
|
|
||||||
self.config.set("run_in_thread", True)
|
self.config.set("run_in_thread", True)
|
||||||
else:
|
else:
|
||||||
if self.config.get("run_in_thread") == None:
|
|
||||||
self.config.set("run_in_thread", False)
|
self.config.set("run_in_thread", False)
|
||||||
|
|
||||||
self.dbus_manager = get_dbus_manager(deluge_core, deluge_interface,
|
self.dbus_manager = get_dbus_manager(deluge_core, deluge_interface,
|
||||||
|
@ -141,8 +137,8 @@ class plugin_WebUi(object):
|
||||||
self.kill_server()
|
self.kill_server()
|
||||||
|
|
||||||
if self.config.get("run_in_thread"):
|
if self.config.get("run_in_thread"):
|
||||||
print 'start Webui(inside gtk)..'
|
print 'Start Webui(inside gtk)..'
|
||||||
webserver_common.init() #reload changed config.
|
webserver_common.init_gtk_05() #reload changed config.
|
||||||
from deluge_webserver import WebServer #only import in threaded mode
|
from deluge_webserver import WebServer #only import in threaded mode
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,11 +146,9 @@ class plugin_WebUi(object):
|
||||||
self.web_server.start_gtk()
|
self.web_server.start_gtk()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print 'start Webui(in process)..'
|
print 'Start Webui(in process)..'
|
||||||
path = os.path.dirname(__file__)
|
server_bin = os.path.dirname(__file__) + '/run_webserver'
|
||||||
server_bin = path + '/run_webserver'
|
self.proc = Popen((server_bin,'env=0.5'))
|
||||||
port = str(self.config.get('port'))
|
|
||||||
self.proc = Popen((server_bin, port),cwd=path)
|
|
||||||
|
|
||||||
def kill_server(self):
|
def kill_server(self):
|
||||||
if self.web_server:
|
if self.web_server:
|
||||||
|
@ -187,7 +181,8 @@ class ConfigDialog(gtk.Dialog):
|
||||||
template_path = os.path.join(os.path.dirname(__file__), 'templates')
|
template_path = os.path.join(os.path.dirname(__file__), 'templates')
|
||||||
self.templates = [dirname for dirname
|
self.templates = [dirname for dirname
|
||||||
in os.listdir(template_path)
|
in os.listdir(template_path)
|
||||||
if os.path.isdir(os.path.join(template_path, dirname))]
|
if os.path.isdir(os.path.join(template_path, dirname))
|
||||||
|
and not dirname.startswith('.')]
|
||||||
|
|
||||||
self.port = self.add_widget(_('Port Number'), gtk.SpinButton())
|
self.port = self.add_widget(_('Port Number'), gtk.SpinButton())
|
||||||
self.pwd1 = self.add_widget(_('New Password'), gtk.Entry())
|
self.pwd1 = self.add_widget(_('New Password'), gtk.Entry())
|
||||||
|
@ -195,16 +190,11 @@ class ConfigDialog(gtk.Dialog):
|
||||||
self.template = self.add_widget(_('Template'), gtk.combo_box_new_text())
|
self.template = self.add_widget(_('Template'), gtk.combo_box_new_text())
|
||||||
self.button_style = self.add_widget(_('Button Style'),
|
self.button_style = self.add_widget(_('Button Style'),
|
||||||
gtk.combo_box_new_text())
|
gtk.combo_box_new_text())
|
||||||
self.download_dir = self.add_widget(_('Download Directory'),
|
|
||||||
gtk.FileChooserButton(_('Download Directory')))
|
|
||||||
self.torrent_dir = self.add_widget(_('Torrent Directory'),
|
|
||||||
gtk.FileChooserButton(_('Torrent Directory')))
|
|
||||||
self.cache_templates = self.add_widget(_('Cache Templates'),
|
self.cache_templates = self.add_widget(_('Cache Templates'),
|
||||||
gtk.CheckButton())
|
gtk.CheckButton())
|
||||||
self.run_in_thread = self.add_widget(_('Run inside GTK'), gtk.CheckButton())
|
#self.share_downloads = self.add_widget(_('Share Download Directory'),
|
||||||
|
# gtk.CheckButton())
|
||||||
|
|
||||||
self.download_dir.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
||||||
self.torrent_dir.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
||||||
self.port.set_range(80, 65536)
|
self.port.set_range(80, 65536)
|
||||||
self.port.set_increments(1, 10)
|
self.port.set_increments(1, 10)
|
||||||
self.pwd1.set_visibility(False)
|
self.pwd1.set_visibility(False)
|
||||||
|
@ -218,23 +208,16 @@ class ConfigDialog(gtk.Dialog):
|
||||||
|
|
||||||
for item in [_('Text and image'), _('Image Only'), _('Text Only')]:
|
for item in [_('Text and image'), _('Image Only'), _('Text Only')]:
|
||||||
self.button_style.append_text(item)
|
self.button_style.append_text(item)
|
||||||
if not self.config.get("button_style"):
|
if self.config.get("button_style") == None:
|
||||||
self.config.set("button_style", 2)
|
self.config.set("button_style", 2)
|
||||||
|
|
||||||
self.port.set_value(int(self.config.get("port")))
|
self.port.set_value(int(self.config.get("port")))
|
||||||
self.template.set_active(
|
self.template.set_active(
|
||||||
self.templates.index(self.config.get("template")))
|
self.templates.index(self.config.get("template")))
|
||||||
self.button_style.set_active(self.config.get("button_style"))
|
self.button_style.set_active(self.config.get("button_style"))
|
||||||
|
#self.share_downloads.set_active(
|
||||||
|
# bool(self.config.get("share_downloads")))
|
||||||
|
|
||||||
self.torrent_dir.set_filename(self.config.get("torrent_dir"))
|
|
||||||
self.download_dir.set_filename(self.config.get("download_dir"))
|
|
||||||
|
|
||||||
if deluge.common.windows_check():
|
|
||||||
self.run_in_thread.set_active(True)
|
|
||||||
self.run_in_thread.set_sensitive(False)
|
|
||||||
else:
|
|
||||||
self.run_in_thread.set_active(False)
|
|
||||||
self.run_in_thread.set_sensitive(False)
|
|
||||||
self.cache_templates.set_active(self.config.get("cache_templates"))
|
self.cache_templates.set_active(self.config.get("cache_templates"))
|
||||||
|
|
||||||
self.vbox.pack_start(self.vb, True, True, 0)
|
self.vbox.pack_start(self.vb, True, True, 0)
|
||||||
|
@ -270,9 +253,7 @@ class ConfigDialog(gtk.Dialog):
|
||||||
self.config.set("port", int(self.port.get_value()))
|
self.config.set("port", int(self.port.get_value()))
|
||||||
self.config.set("template", self.template.get_active_text())
|
self.config.set("template", self.template.get_active_text())
|
||||||
self.config.set("button_style", self.button_style.get_active())
|
self.config.set("button_style", self.button_style.get_active())
|
||||||
self.config.set("torrent_dir", self.torrent_dir.get_filename())
|
|
||||||
self.config.set("download_dir",self.download_dir.get_filename())
|
|
||||||
self.config.set("cache_templates", self.cache_templates.get_active())
|
self.config.set("cache_templates", self.cache_templates.get_active())
|
||||||
self.config.set("run_in_thread", self.run_in_thread.get_active())
|
#self.config.set("share_downloads", self.share_downloads.get_active())
|
||||||
self.config.save(self.plugin.config_file)
|
self.config.save(self.plugin.config_file)
|
||||||
self.plugin.start_server() #restarts server
|
self.plugin.start_server() #restarts server
|
||||||
|
|
|
@ -68,7 +68,7 @@ class DbusManager(dbus.service.Object):
|
||||||
|
|
||||||
@dbus.service.method(dbus_interface=dbus_interface,
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
in_signature="",out_signature="as")
|
in_signature="",out_signature="as")
|
||||||
def get_torrent_state(self):
|
def get_session_state(self):
|
||||||
"""Returns a list of torrent_ids in the session.
|
"""Returns a list of torrent_ids in the session.
|
||||||
same as 0.6, but returns type "as" instead of a pickle
|
same as 0.6, but returns type "as" instead of a pickle
|
||||||
"""
|
"""
|
||||||
|
@ -129,16 +129,18 @@ class DbusManager(dbus.service.Object):
|
||||||
return status_subset
|
return status_subset
|
||||||
|
|
||||||
@dbus.service.method(dbus_interface=dbus_interface,
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
in_signature="s",out_signature="")
|
in_signature="as",out_signature="")
|
||||||
def pause_torrent(self, torrent_id):
|
def pause_torrent(self, torrents):
|
||||||
"""same as 0.6 interface"""
|
"""same as 0.6 interface"""
|
||||||
|
for torrent_id in torrents:
|
||||||
torrent_id = int(torrent_id)
|
torrent_id = int(torrent_id)
|
||||||
self.core.set_user_pause(torrent_id,True)
|
self.core.set_user_pause(torrent_id,True)
|
||||||
|
|
||||||
@dbus.service.method(dbus_interface=dbus_interface,
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
in_signature="s", out_signature="")
|
in_signature="as", out_signature="")
|
||||||
def resume_torrent(self, torrent_id):
|
def resume_torrent(self, torrents):
|
||||||
"""same as 0.6 interface"""
|
"""same as 0.6 interface"""
|
||||||
|
for torrent_id in torrents:
|
||||||
torrent_id = int(torrent_id)
|
torrent_id = int(torrent_id)
|
||||||
self.core.set_user_pause(torrent_id,False)
|
self.core.set_user_pause(torrent_id,False)
|
||||||
|
|
||||||
|
@ -157,7 +159,6 @@ class DbusManager(dbus.service.Object):
|
||||||
@dbus.service.method(dbus_interface=dbus_interface,
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
in_signature="s", out_signature="b")
|
in_signature="s", out_signature="b")
|
||||||
def add_torrent_url(self, url):
|
def add_torrent_url(self, url):
|
||||||
"""not available in deluge 0.6 interface"""
|
|
||||||
filename = fetch_url(url)
|
filename = fetch_url(url)
|
||||||
self._add_torrent(filename)
|
self._add_torrent(filename)
|
||||||
return True
|
return True
|
||||||
|
@ -182,8 +183,8 @@ class DbusManager(dbus.service.Object):
|
||||||
#name = fillename without directory
|
#name = fillename without directory
|
||||||
name = name.replace('\\','/')
|
name = name.replace('\\','/')
|
||||||
name = 'deluge_' + str(random.random()) + '_' + name.split('/')[-1]
|
name = 'deluge_' + str(random.random()) + '_' + name.split('/')[-1]
|
||||||
|
filename = os.path.join(self.core.config.get("default_download_path"), name)
|
||||||
|
|
||||||
filename = os.path.join(self.config.get("torrent_dir"),name)
|
|
||||||
filecontent = base64.b64decode(filecontent_b64)
|
filecontent = base64.b64decode(filecontent_b64)
|
||||||
f = open(filename,"wb") #no with statement, that's py 2.5+
|
f = open(filename,"wb") #no with statement, that's py 2.5+
|
||||||
f.write(filecontent)
|
f.write(filecontent)
|
||||||
|
@ -192,11 +193,42 @@ class DbusManager(dbus.service.Object):
|
||||||
self._add_torrent(filename)
|
self._add_torrent(filename)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
|
in_signature="", out_signature="a{sv}")
|
||||||
|
def get_config(self):
|
||||||
|
return self.core.config.mapping
|
||||||
|
|
||||||
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
|
in_signature="s", out_signature="v")
|
||||||
|
def get_config_value(self,key):
|
||||||
|
return self.core.config.mapping[pythonize(key)] #ugly!
|
||||||
|
|
||||||
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
|
in_signature="a{sv}", out_signature="")
|
||||||
|
def set_config(self, config):
|
||||||
|
"""Set the config with values from dictionary"""
|
||||||
|
config = deluge.common.pythonize(config)
|
||||||
|
# Load all the values into the configuration
|
||||||
|
for key in self.core.config.keys():
|
||||||
|
self.core.config[key] = config[key]
|
||||||
|
self.core.apply_prefs()
|
||||||
|
|
||||||
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
|
in_signature="", out_signature="v")
|
||||||
|
def get_download_rate(self):
|
||||||
|
return self.core.get_state()['download_rate']
|
||||||
|
|
||||||
|
@dbus.service.method(dbus_interface=dbus_interface,
|
||||||
|
in_signature="", out_signature="v")
|
||||||
|
def get_upload_rate(self):
|
||||||
|
return self.core.get_state()['upload_rate']
|
||||||
|
|
||||||
|
|
||||||
#internal
|
#internal
|
||||||
def _add_torrent(self, filename):
|
def _add_torrent(self, filename):
|
||||||
#dbus types break pickle, again.....
|
|
||||||
filename = unicode(filename)
|
filename = unicode(filename)
|
||||||
target = self.config.get("download_dir")
|
target = self.core.config.get("default_download_path")
|
||||||
|
|
||||||
torrent_id = self.core.add_torrent(filename, target,
|
torrent_id = self.core.add_torrent(filename, target,
|
||||||
self.interface.config.get("use_compact_storage"))
|
self.interface.config.get("use_compact_storage"))
|
||||||
|
|
|
@ -31,67 +31,15 @@
|
||||||
# this exception statement from your version. If you delete this exception
|
# this exception statement from your version. If you delete this exception
|
||||||
# statement from all source files in the program, then also delete it here.
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
|
||||||
|
|
||||||
from webserver_common import TORRENT_KEYS, STATE_MESSAGES
|
|
||||||
import webserver_common as ws
|
import webserver_common as ws
|
||||||
from webserver_framework import *
|
from webserver_framework import *
|
||||||
|
|
||||||
|
|
||||||
import webpy022 as web
|
import webpy022 as web
|
||||||
from webpy022.http import seeother, url
|
from webpy022.http import seeother, url
|
||||||
from webpy022.utils import Storage
|
|
||||||
|
|
||||||
from md5 import md5
|
|
||||||
import base64
|
import base64
|
||||||
from deluge.common import fsize
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
import os
|
||||||
#utils:
|
|
||||||
def check_pwd(pwd):
|
|
||||||
m = md5()
|
|
||||||
m.update(ws.config.get('pwd_salt'))
|
|
||||||
m.update(pwd)
|
|
||||||
return (m.digest() == ws.config.get('pwd_md5'))
|
|
||||||
|
|
||||||
def get_torrent_status(torrent_id):
|
|
||||||
"""
|
|
||||||
helper method.
|
|
||||||
enhance ws.proxy.get_torrent_status with some extra data
|
|
||||||
"""
|
|
||||||
status = ws.proxy.get_torrent_status(torrent_id,TORRENT_KEYS)
|
|
||||||
status["id"] = torrent_id
|
|
||||||
|
|
||||||
#for naming the status-images
|
|
||||||
status["calc_state_str"] = "downloading"
|
|
||||||
if status["paused"]:
|
|
||||||
status["calc_state_str"] = "inactive"
|
|
||||||
elif status["is_seed"]:
|
|
||||||
status["calc_state_str"] = "seeding"
|
|
||||||
|
|
||||||
#action for torrent_pause
|
|
||||||
if status["calc_state_str"] == "inactive":
|
|
||||||
status["action"] = "start"
|
|
||||||
else:
|
|
||||||
status["action"] = "stop"
|
|
||||||
|
|
||||||
if status["paused"]:
|
|
||||||
status["message"] = _("Paused %s%%") % status['progress']
|
|
||||||
else:
|
|
||||||
status["message"] = "%s %i%%" % (STATE_MESSAGES[status["state"]]
|
|
||||||
, status['progress'])
|
|
||||||
|
|
||||||
#add some pre-calculated values
|
|
||||||
status.update({
|
|
||||||
"calc_total_downloaded" : (fsize(status["total_done"])
|
|
||||||
+ " (" + fsize(status["total_download"]) + ")"),
|
|
||||||
"calc_total_uploaded": (fsize(status['uploaded_memory']
|
|
||||||
+ status["total_payload_upload"]) + " ("
|
|
||||||
+ fsize(status["total_upload"]) + ")"),
|
|
||||||
})
|
|
||||||
|
|
||||||
return Storage(status) #Storage for easy templating.
|
|
||||||
|
|
||||||
#/utils
|
|
||||||
|
|
||||||
#routing:
|
#routing:
|
||||||
urls = (
|
urls = (
|
||||||
|
@ -107,15 +55,21 @@ urls = (
|
||||||
"/resume_all(.*)", "resume_all",
|
"/resume_all(.*)", "resume_all",
|
||||||
"/refresh/set(.*)", "refresh_set",
|
"/refresh/set(.*)", "refresh_set",
|
||||||
"/refresh/(.*)", "refresh",
|
"/refresh/(.*)", "refresh",
|
||||||
|
"/config(.*)","config",
|
||||||
"/home(.*)", "home",
|
"/home(.*)", "home",
|
||||||
"/about(.*)", "about",
|
"/about(.*)", "about",
|
||||||
|
"/logout(.*)", "logout",
|
||||||
#default-pages
|
#default-pages
|
||||||
"/", "login",
|
"/", "home",
|
||||||
"", "login",
|
"", "home",
|
||||||
#remote-api:
|
#remote-api:
|
||||||
"/remote/torrent/add(.*)", "remote_torrent_add"
|
"/remote/torrent/add(.*)", "remote_torrent_add",
|
||||||
)
|
#static:
|
||||||
|
"/static/(.*)","static",
|
||||||
|
"/template/static/(.*)","template_static",
|
||||||
|
#"/downloads/(.*)","downloads" disabled until it can handle large downloads.
|
||||||
|
|
||||||
|
)
|
||||||
#/routing
|
#/routing
|
||||||
|
|
||||||
#pages:
|
#pages:
|
||||||
|
@ -133,15 +87,10 @@ class login:
|
||||||
start_session()
|
start_session()
|
||||||
do_redirect()
|
do_redirect()
|
||||||
elif vars.redir:
|
elif vars.redir:
|
||||||
seeother(url('/login',error=1,redir=vars.redir))
|
seeother(url('/login',error=1, redir=vars.redir))
|
||||||
else:
|
else:
|
||||||
seeother('/login?error=1')
|
seeother('/login?error=1')
|
||||||
|
|
||||||
class home:
|
|
||||||
@check_session
|
|
||||||
def GET(self, name):
|
|
||||||
do_redirect()
|
|
||||||
|
|
||||||
class index:
|
class index:
|
||||||
"page containing the torrent list."
|
"page containing the torrent list."
|
||||||
@auto_refreshed
|
@auto_refreshed
|
||||||
|
@ -150,7 +99,7 @@ class index:
|
||||||
vars = web.input(sort=None, order=None)
|
vars = web.input(sort=None, order=None)
|
||||||
|
|
||||||
status_rows = [get_torrent_status(torrent_id)
|
status_rows = [get_torrent_status(torrent_id)
|
||||||
for torrent_id in ws.proxy.get_torrent_state()]
|
for torrent_id in ws.proxy.get_session_state()]
|
||||||
|
|
||||||
#sorting:
|
#sorting:
|
||||||
if vars.sort:
|
if vars.sort:
|
||||||
|
@ -164,21 +113,19 @@ class index:
|
||||||
return ws.render.index(status_rows)
|
return ws.render.index(status_rows)
|
||||||
|
|
||||||
class torrent_info:
|
class torrent_info:
|
||||||
"torrent details"
|
|
||||||
@auto_refreshed
|
@auto_refreshed
|
||||||
@deluge_page
|
@deluge_page
|
||||||
def GET(self, torrent_id):
|
def GET(self, torrent_id):
|
||||||
return ws.render.torrent_info(get_torrent_status(torrent_id))
|
return ws.render.torrent_info(get_torrent_status(torrent_id))
|
||||||
|
|
||||||
class torrent_pause:
|
class torrent_pause:
|
||||||
"start/stop a torrent"
|
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, name):
|
||||||
vars = web.input(stop = None, start = None, redir = None)
|
vars = web.input(stop = None, start = None, redir = None)
|
||||||
if vars.stop:
|
if vars.stop:
|
||||||
ws.proxy.pause_torrent(vars.stop)
|
ws.proxy.pause_torrent([vars.stop])
|
||||||
elif vars.start:
|
elif vars.start:
|
||||||
ws.proxy.resume_torrent(vars.start)
|
ws.proxy.resume_torrent([vars.start])
|
||||||
|
|
||||||
do_redirect()
|
do_redirect()
|
||||||
|
|
||||||
|
@ -194,7 +141,7 @@ class torrent_add:
|
||||||
if vars.url and vars.torrent.filename:
|
if vars.url and vars.torrent.filename:
|
||||||
error_page(_("Choose an url or a torrent, not both."))
|
error_page(_("Choose an url or a torrent, not both."))
|
||||||
if vars.url:
|
if vars.url:
|
||||||
ws.proxy.add_torrent_url(vars.url)
|
ws.proxy.add_torrent_url(vars.url )
|
||||||
do_redirect()
|
do_redirect()
|
||||||
elif vars.torrent.filename:
|
elif vars.torrent.filename:
|
||||||
data = vars.torrent.file.read()
|
data = vars.torrent.file.read()
|
||||||
|
@ -227,8 +174,7 @@ class torrent_delete:
|
||||||
return ws.render.torrent_delete(get_torrent_status(torrent_id))
|
return ws.render.torrent_delete(get_torrent_status(torrent_id))
|
||||||
|
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, torrent_id):
|
||||||
torrent_id = name
|
|
||||||
vars = web.input(data_also = None, torrent_also = None)
|
vars = web.input(data_also = None, torrent_also = None)
|
||||||
data_also = bool(vars.data_also)
|
data_also = bool(vars.data_also)
|
||||||
torrent_also = bool(vars.torrent_also)
|
torrent_also = bool(vars.torrent_also)
|
||||||
|
@ -238,30 +184,26 @@ class torrent_delete:
|
||||||
|
|
||||||
class torrent_queue_up:
|
class torrent_queue_up:
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, torrent_id):
|
||||||
torrent_id = name
|
|
||||||
ws.proxy.queue_up(torrent_id)
|
ws.proxy.queue_up(torrent_id)
|
||||||
do_redirect()
|
do_redirect()
|
||||||
|
|
||||||
class torrent_queue_down:
|
class torrent_queue_down:
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, torrent_id):
|
||||||
torrent_id = name
|
|
||||||
ws.proxy.queue_down(torrent_id)
|
ws.proxy.queue_down(torrent_id)
|
||||||
do_redirect()
|
do_redirect()
|
||||||
|
|
||||||
class pause_all:
|
class pause_all:
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, name):
|
||||||
for torrent_id in ws.proxy.get_torrent_state():
|
ws.proxy.pause_torrent(ws.proxy.get_session_state())
|
||||||
ws.proxy.pause_torrent(torrent_id)
|
|
||||||
do_redirect()
|
do_redirect()
|
||||||
|
|
||||||
class resume_all:
|
class resume_all:
|
||||||
@check_session
|
@check_session
|
||||||
def POST(self, name):
|
def POST(self, name):
|
||||||
for torrent_id in ws.proxy.get_torrent_state():
|
ws.proxy.resume_torrent(ws.proxy.get_session_state())
|
||||||
ws.proxy.resume_torrent(torrent_id)
|
|
||||||
do_redirect()
|
do_redirect()
|
||||||
|
|
||||||
class refresh:
|
class refresh:
|
||||||
|
@ -287,13 +229,61 @@ class refresh_set:
|
||||||
else:
|
else:
|
||||||
error_page(_('refresh must be > 0'))
|
error_page(_('refresh must be > 0'))
|
||||||
|
|
||||||
|
class config:
|
||||||
|
"""core config
|
||||||
|
TODO:good validation.
|
||||||
|
"""
|
||||||
|
cfg_form = web.form.Form(
|
||||||
|
web.form.Dropdown('max_download', ws.SPEED_VALUES,
|
||||||
|
description=_('Download Speed Limit'),
|
||||||
|
post='%s Kib/sec' % ws.proxy.get_config_value('max_download_speed')
|
||||||
|
)
|
||||||
|
,web.form.Dropdown('max_upload', ws.SPEED_VALUES,
|
||||||
|
description=_('Upload Speed Limit'),
|
||||||
|
post='%s Kib/sec' % ws.proxy.get_config_value('max_upload_speed')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@deluge_page
|
||||||
|
def GET(self, name):
|
||||||
|
return ws.render.config(self.cfg_form())
|
||||||
|
|
||||||
|
def POST(self, name):
|
||||||
|
vars = web.input(max_download=None, max_upload=None)
|
||||||
|
|
||||||
|
#self.config.set("max_download_speed", float(str_bwdown))
|
||||||
|
raise NotImplementedError('todo')
|
||||||
|
|
||||||
|
class home:
|
||||||
|
@check_session
|
||||||
|
def GET(self, name):
|
||||||
|
do_redirect()
|
||||||
|
|
||||||
class about:
|
class about:
|
||||||
@deluge_page_noauth
|
@deluge_page_noauth
|
||||||
def GET(self, name):
|
def GET(self, name):
|
||||||
return ws.render.about()
|
return ws.render.about()
|
||||||
|
|
||||||
#/pages
|
class logout:
|
||||||
|
def POST(self, name):
|
||||||
|
end_session()
|
||||||
|
seeother('/login')
|
||||||
|
|
||||||
|
class static(static_handler):
|
||||||
|
base_dir = os.path.join(os.path.dirname(__file__),'static')
|
||||||
|
|
||||||
|
class template_static(static_handler):
|
||||||
|
def get_base_dir(self):
|
||||||
|
return os.path.join(os.path.dirname(__file__),
|
||||||
|
'templates/%s/static' % ws.config.get('template'))
|
||||||
|
|
||||||
|
class downloads(static_handler):
|
||||||
|
def GET(self, name):
|
||||||
|
self.base_dir = ws.proxy.get_config_value('default_download_path')
|
||||||
|
if not ws.config.get('share_downloads'):
|
||||||
|
raise Exception('Access to downloads is forbidden.')
|
||||||
|
return static_handler.GET(self, name)
|
||||||
|
#/pages
|
||||||
|
|
||||||
def WebServer():
|
def WebServer():
|
||||||
return create_webserver(urls, globals())
|
return create_webserver(urls, globals())
|
||||||
|
@ -307,4 +297,3 @@ def run():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
87
|
112
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
from deluge_webserver import *
|
import deluge_webserver
|
||||||
web.run(urls, globals())
|
deluge_webserver.run()
|
||||||
|
|
10
plugins/WebUi/scripts/add_torrent_to_deluge_webui
Executable file
10
plugins/WebUi/scripts/add_torrent_to_deluge_webui
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
pwd=deluge
|
||||||
|
url=http://localhost:8112
|
||||||
|
|
||||||
|
for arg in "$@"
|
||||||
|
do
|
||||||
|
curl -F torrent=@"$arg" -F pwd=$pwd $url/remote/torrent/add
|
||||||
|
done
|
||||||
|
|
BIN
plugins/WebUi/static/images/tango/system-log-out.png
Normal file
BIN
plugins/WebUi/static/images/tango/system-log-out.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 799 B |
136
plugins/WebUi/static_handler.py
Normal file
136
plugins/WebUi/static_handler.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
#(c) Martijn Voncken, mvoncken@gmail.com
|
||||||
|
#Same Licence as web.py 0.22 ->Public Domain
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
static fileserving for web.py
|
||||||
|
without the need for wsgi wrapper magic.
|
||||||
|
"""
|
||||||
|
import webpy022 as web
|
||||||
|
from webpy022.http import seeother, url
|
||||||
|
|
||||||
|
import posixpath
|
||||||
|
import urlparse
|
||||||
|
import urllib
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import datetime
|
||||||
|
import cgi
|
||||||
|
from StringIO import StringIO
|
||||||
|
mimetypes.init() # try to read system mime.types
|
||||||
|
|
||||||
|
class static_handler:
|
||||||
|
"""
|
||||||
|
mostly c&p from SimpleHttpServer
|
||||||
|
serves relative from start location
|
||||||
|
"""
|
||||||
|
base_dir = './'
|
||||||
|
extensions_map = mimetypes.types_map
|
||||||
|
|
||||||
|
def get_base_dir(self):
|
||||||
|
#override this if you have a config that changes the base dir at runtime
|
||||||
|
#deluge on windows :(
|
||||||
|
return self.base_dir
|
||||||
|
|
||||||
|
def GET(self, path):
|
||||||
|
path = self.translate_path(path)
|
||||||
|
if os.path.isdir(path):
|
||||||
|
if not path.endswith('/'):
|
||||||
|
path += "/"
|
||||||
|
return self.list_directory(path)
|
||||||
|
|
||||||
|
ctype = self.guess_type(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = open(path, 'rb')
|
||||||
|
except IOError:
|
||||||
|
raise Exception('file not found:%s' % path)
|
||||||
|
#web.header("404", "File not found")
|
||||||
|
#return
|
||||||
|
web.header("Content-type", ctype)
|
||||||
|
fs = os.fstat(f.fileno())
|
||||||
|
web.header("Content-Length", str(fs[6]))
|
||||||
|
web.lastmodified(datetime.datetime.fromtimestamp(fs.st_mtime))
|
||||||
|
print f.read()
|
||||||
|
|
||||||
|
def translate_path(self, path):
|
||||||
|
"""Translate a /-separated PATH to the local filename syntax.
|
||||||
|
|
||||||
|
Components that mean special things to the local file system
|
||||||
|
(e.g. drive or directory names) are ignored. (XXX They should
|
||||||
|
probably be diagnosed.)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# abandon query parameters
|
||||||
|
path = urlparse.urlparse(path)[2]
|
||||||
|
path = posixpath.normpath(urllib.unquote(path))
|
||||||
|
words = path.split('/')
|
||||||
|
words = filter(None, words)
|
||||||
|
path = self.get_base_dir()
|
||||||
|
for word in words:
|
||||||
|
drive, word = os.path.splitdrive(word)
|
||||||
|
head, word = os.path.split(word)
|
||||||
|
if word in (os.curdir, os.pardir): continue
|
||||||
|
path = os.path.join(path, word)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def guess_type(self, path):
|
||||||
|
base, ext = posixpath.splitext(path)
|
||||||
|
if ext in self.extensions_map:
|
||||||
|
return self.extensions_map[ext]
|
||||||
|
ext = ext.lower()
|
||||||
|
if ext in self.extensions_map:
|
||||||
|
return self.extensions_map[ext]
|
||||||
|
else:
|
||||||
|
return 'application/octet-stream'
|
||||||
|
|
||||||
|
|
||||||
|
def list_directory(self, path):
|
||||||
|
"""Helper to produce a directory listing (absent index.html).
|
||||||
|
|
||||||
|
Return value is either a file object, or None (indicating an
|
||||||
|
error). In either case, the headers are sent, making the
|
||||||
|
interface the same as for send_head().
|
||||||
|
#TODO ->use web.py +template!
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
list = os.listdir(path)
|
||||||
|
except os.error:
|
||||||
|
web.header('404', "No permission to list directory")
|
||||||
|
return None
|
||||||
|
list.sort(key=lambda a: a.lower())
|
||||||
|
f = StringIO()
|
||||||
|
displaypath = cgi.escape(urllib.unquote(path))
|
||||||
|
f.write("<title>Directory listing for %s</title>\n" % displaypath)
|
||||||
|
f.write("<h2>Directory listing for %s</h2>\n" % displaypath)
|
||||||
|
f.write("<hr>\n<ul>\n")
|
||||||
|
for name in list:
|
||||||
|
fullname = os.path.join(path, name)
|
||||||
|
displayname = linkname = name
|
||||||
|
# Append / for directories or @ for symbolic links
|
||||||
|
if os.path.isdir(fullname):
|
||||||
|
displayname = name + "/"
|
||||||
|
linkname = name + "/"
|
||||||
|
if os.path.islink(fullname):
|
||||||
|
displayname = name + "@"
|
||||||
|
# Note: a link to a directory displays with @ and links with /
|
||||||
|
f.write('<li><a href="%s">%s</a>\n'
|
||||||
|
% (urllib.quote(linkname), cgi.escape(displayname)))
|
||||||
|
f.write("</ul>\n<hr>\n")
|
||||||
|
length = f.tell()
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
web.header("Content-type", "text/html")
|
||||||
|
web.header("Content-Length", str(length))
|
||||||
|
print f.read()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#example:
|
||||||
|
class usr_static(static_handler):
|
||||||
|
base_dir = os.path.expanduser('~')
|
||||||
|
|
||||||
|
urls = ('/relative/(.*)','static_handler',
|
||||||
|
'/(.*)','usr_static')
|
||||||
|
|
||||||
|
web.run(urls,globals())
|
|
@ -7,6 +7,7 @@ $:render.header(_('About'))
|
||||||
<li><a href="http://deluge-torrent.org">Deluge</a></li>
|
<li><a href="http://deluge-torrent.org">Deluge</a></li>
|
||||||
<li><a href="http://forum.deluge-torrent.org/viewtopic.php?f=9&t=425">
|
<li><a href="http://forum.deluge-torrent.org/viewtopic.php?f=9&t=425">
|
||||||
WebUi forum Thread</a>
|
WebUi forum Thread</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
<li><a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GPL v2
|
<li><a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GPL v2
|
||||||
</a></li>
|
</a></li>
|
||||||
|
@ -18,6 +19,7 @@ $:render.header(_('About'))
|
||||||
<ul>
|
<ul>
|
||||||
<li>Martijn Voncken</li>
|
<li>Martijn Voncken</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Template</h3>
|
<h3>Template</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Martijn Voncken</li>
|
<li>Martijn Voncken</li>
|
||||||
|
@ -27,6 +29,7 @@ $:render.header(_('About'))
|
||||||
<ul>
|
<ul>
|
||||||
<li>Zach Tibbitts</li>
|
<li>Zach Tibbitts</li>
|
||||||
<li>Alon Zakai</li>
|
<li>Alon Zakai</li>
|
||||||
|
|
||||||
<li>Alon Zakai</li>
|
<li>Alon Zakai</li>
|
||||||
<li>Marcos Pinto</li>
|
<li>Marcos Pinto</li>
|
||||||
<li>Andrew Resch</li>
|
<li>Andrew Resch</li>
|
||||||
|
|
10
plugins/WebUi/templates/deluge/config.html
Normal file
10
plugins/WebUi/templates/deluge/config.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
$def with (form)
|
||||||
|
$:render.header(_('Config'))
|
||||||
|
|
||||||
|
<div class="error">Not Implemented!</div>
|
||||||
|
<form method="POST">
|
||||||
|
$:form.render()
|
||||||
|
<input type="submit" value="$_('Apply')"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
$:render.footer()
|
|
@ -51,8 +51,10 @@ $for torrent in torrent_list:
|
||||||
$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png')
|
$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png')
|
||||||
$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/media-playback-pause.png')
|
$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/media-playback-pause.png')
|
||||||
$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/media-playback-start.png')
|
$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/media-playback-start.png')
|
||||||
|
$:render.part_button('POST', '/logout', _('Logout'), 'tango/system-log-out.png')
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
$:render.part_refresh()
|
$:part_stats()
|
||||||
|
|
||||||
$:render.footer()
|
$:render.footer()
|
||||||
|
|
||||||
|
|
|
@ -2,21 +2,25 @@ $def with (method, url, title, image='')
|
||||||
<div class="deluge_button">
|
<div class="deluge_button">
|
||||||
<form method="$method" action="$url" class="deluge_button">
|
<form method="$method" action="$url" class="deluge_button">
|
||||||
<input type="hidden" name="redir" value="$self_url()">
|
<input type="hidden" name="redir" value="$self_url()">
|
||||||
<button type="submit" class="deluge_button">
|
|
||||||
$if (get_config('button_style') == 0):
|
$if (get_config('button_style') == 0):
|
||||||
|
<button type="submit" class="deluge_button" alt="$title">
|
||||||
$title
|
$title
|
||||||
$if image:
|
$if image:
|
||||||
<image src="/static/images/$image" class="button" alt="$title"/>
|
<image src="/static/images/$image" class="button" alt="$title"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
$if (get_config('button_style') == 1):
|
$if (get_config('button_style') == 1):
|
||||||
$if image:
|
$if image:
|
||||||
<image src="/static/images/$image" class="button" alt="$title"/>
|
<input type="image" image src="/static/images/$image" class="img_button" alt="$title"/>
|
||||||
$else:
|
$else:
|
||||||
|
<button type="submit" class="deluge_button" alt="$title">
|
||||||
$title
|
$title
|
||||||
|
</button>
|
||||||
|
|
||||||
$if (get_config('button_style') == 2):
|
$if (get_config('button_style') == 2):
|
||||||
|
<button type="submit" class="deluge_button" alt="$title">
|
||||||
$title
|
$title
|
||||||
|
</button>
|
||||||
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
30
plugins/WebUi/templates/deluge/part_stats.html
Normal file
30
plugins/WebUi/templates/deluge/part_stats.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
$def with (stats)
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel" id='refresh_panel'>
|
||||||
|
|
||||||
|
$_('Auto refresh:')
|
||||||
|
$if getcookie('auto_refresh') == '1':
|
||||||
|
($getcookie('auto_refresh_secs')) $_('seconds')
|
||||||
|
$:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png')
|
||||||
|
$:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png')
|
||||||
|
$else:
|
||||||
|
$_('Off')
|
||||||
|
$:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png')
|
||||||
|
$#end
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel" id='refresh_panel'>
|
||||||
|
<a href='/config'>
|
||||||
|
$_('Down Speed') : $stats.download_rate ($stats.max_download)
|
||||||
|
|
||||||
|
$_('Up Speed') : $stats.upload_rate ($stats.max_upload)
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
(<a href='/about'>$_('About')</a>)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,7 @@ $:render.part_button('GET', '/torrent/delete/' + str(torrent.id), _('Remove'), '
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
<!--
|
||||||
[<a onclick="javascript:toggle_dump()">$_('Debug:Data Dump')</a>]
|
[<a onclick="javascript:toggle_dump()">$_('Debug:Data Dump')</a>]
|
||||||
|
|
||||||
<pre style="background-color:white;color:black;display:none" id="data_dump">
|
<pre style="background-color:white;color:black;display:none" id="data_dump">
|
||||||
|
@ -102,6 +103,8 @@ function toggle_dump(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
-->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -114,6 +117,6 @@ $:render.part_button('POST', '/torrent/queue/up/' + str(torrent.id), _('Queue Up
|
||||||
$:render.part_button('POST', '/torrent/queue/down/' + str(torrent.id), _('Queue Down'), 'tango/down.png')
|
$:render.part_button('POST', '/torrent/queue/down/' + str(torrent.id), _('Queue Down'), 'tango/down.png')
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
$:render.part_refresh()
|
$:part_stats()
|
||||||
|
|
||||||
$:render.footer()
|
$:render.footer()
|
||||||
|
|
3
plugins/WebUi/templates/example/footer.html
Normal file
3
plugins/WebUi/templates/example/footer.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
12
plugins/WebUi/templates/example/header.html
Normal file
12
plugins/WebUi/templates/example/header.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
$def with (title)
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Deluge(example) : $title</title>
|
||||||
|
<link rel="icon" href="/static/images/deluge_icon.gif" type="image/gif" />
|
||||||
|
<link rel="shortcut icon" href="/static/images/deluge_icon.gif" type="image/gif" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img src="/template/static/example.png">
|
||||||
|
<a href=/home>[HOME]</a>
|
||||||
|
|
||||||
|
<h1>$title</h1>
|
42
plugins/WebUi/templates/example/index.html
Normal file
42
plugins/WebUi/templates/example/index.html
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
$def with (torrent_list)
|
||||||
|
$:render.header(_('Torrent list'))
|
||||||
|
|
||||||
|
<form action="/torrent/pause" method="POST">
|
||||||
|
<table class="torrent_list" border=1>
|
||||||
|
<tr>
|
||||||
|
$:(sort_head('calc_state_str', 'S'))
|
||||||
|
$:(sort_head('queue_pos', '#'))
|
||||||
|
$:(sort_head('name', _('Name')))
|
||||||
|
$:(sort_head('progress', _('Progress')))
|
||||||
|
</tr>
|
||||||
|
$#4-space indentation is mandatory for for-loops in templetor!
|
||||||
|
$for torrent in torrent_list:
|
||||||
|
<tr>
|
||||||
|
<td><input type="image"
|
||||||
|
src="/static/images/$(torrent.calc_state_str)16.png"
|
||||||
|
name="$torrent.action" value="$torrent.id">
|
||||||
|
</td>
|
||||||
|
<td>$torrent.queue_pos</td>
|
||||||
|
<td style="width:100px; overflow:hidden;white-space: nowrap">
|
||||||
|
<a href="/torrent/info/$torrent.id">$(crop(torrent.name, 40))</a></td>
|
||||||
|
<td class="progress_bar">
|
||||||
|
<div class="progress_bar_outer">
|
||||||
|
<div class="progress_bar" style="width:$(torrent.progress)%">
|
||||||
|
$torrent.message
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="panel" bgcolor="5555AA">
|
||||||
|
$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png')
|
||||||
|
$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/media-playback-pause.png')
|
||||||
|
$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/media-playback-start.png')
|
||||||
|
$:render.part_button('POST', '/logout', _('Logout'), 'tango/system-log-out.png')
|
||||||
|
</div>
|
||||||
|
|
||||||
|
$:render.footer()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
revision-id: mvoncken@gmail.com-20070930083408-sv8mo0mi1rbjnfvk
|
revision-id: mvoncken@gmail.com-20070930083408-sv8mo0mi1rbjnfvk
|
||||||
date: 2007-10-23 15:10:08 +0200
|
date: 2007-10-23 15:10:08 +0200
|
||||||
build-date: 2007-10-23 15:34:50 +0200
|
build-date: 2007-10-23 15:34:50 +0200
|
||||||
revno: 87
|
revno: 112
|
||||||
branch-nick: WebUi
|
branch-nick: WebUi
|
||||||
|
|
|
@ -29,40 +29,96 @@
|
||||||
# this exception statement from your version. If you delete this exception
|
# this exception statement from your version. If you delete this exception
|
||||||
# statement from all source files in the program, then also delete it here.
|
# statement from all source files in the program, then also delete it here.
|
||||||
|
|
||||||
|
"""
|
||||||
|
initializes config,render and proxy.
|
||||||
|
contains all hacks to support running in process0.5 ,run inside-gtk0.5 and
|
||||||
|
run in process0.6
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import deluge
|
import deluge
|
||||||
from deluge.common import INSTALL_PREFIX
|
|
||||||
import random
|
import random
|
||||||
import pickle
|
import pickle
|
||||||
|
import sys
|
||||||
from webpy022 import template
|
from webpy022 import template
|
||||||
|
|
||||||
random.seed()
|
random.seed()
|
||||||
|
path = os.path.dirname(__file__)
|
||||||
|
|
||||||
config_file = deluge.common.CONFIG_DIR + "/webui.conf"
|
try:
|
||||||
|
_('translate something')
|
||||||
|
except:
|
||||||
|
import gettext
|
||||||
|
gettext.install('~/') #no translations :(
|
||||||
|
|
||||||
#a bit hacky way of detecting i'm in the deluge gui or in a process :(
|
try:
|
||||||
if not hasattr(deluge,'pref'):
|
config_dir = deluge.common.CONFIG_DIR
|
||||||
|
except:
|
||||||
|
config_dir = os.path.expanduser("~/.config/deluge")
|
||||||
|
|
||||||
|
config_file = os.path.join(config_dir,'webui.conf')
|
||||||
|
session_file = os.path.join(config_dir,'webui.sessions')
|
||||||
|
|
||||||
|
|
||||||
|
class subclassed_render(object):
|
||||||
|
"""
|
||||||
|
try to use the html template in configured dir.
|
||||||
|
not available : use template in /deluge/
|
||||||
|
"""
|
||||||
|
def __init__(self, template_dirname, cache=False):
|
||||||
|
self.base_template = template.render(
|
||||||
|
os.path.join(path, 'templates/deluge/'),
|
||||||
|
cache=cache)
|
||||||
|
|
||||||
|
self.sub_template = template.render(
|
||||||
|
os.path.join(path, 'templates/%s/' % template_dirname),
|
||||||
|
cache=cache)
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
if hasattr(self.sub_template, attr):
|
||||||
|
return getattr(self.sub_template, attr)
|
||||||
|
else:
|
||||||
|
return getattr(self.base_template, attr)
|
||||||
|
|
||||||
|
def init_process():
|
||||||
|
globals()['config'] = pickle.load(open(config_file))
|
||||||
|
globals()['render'] = subclassed_render(config.get('template'),
|
||||||
|
config.get('cache_templates'))
|
||||||
|
|
||||||
|
def init_06():
|
||||||
|
import deluge.ui.client as proxy
|
||||||
|
proxy.set_core_uri('http://localhost:58846') #How to configure this?
|
||||||
|
|
||||||
|
init_process()
|
||||||
|
globals()['proxy'] = proxy
|
||||||
|
|
||||||
|
def init_05():
|
||||||
import dbus
|
import dbus
|
||||||
|
init_process()
|
||||||
bus = dbus.SessionBus()
|
bus = dbus.SessionBus()
|
||||||
proxy = bus.get_object("org.deluge_torrent.dbusplugin"
|
proxy = bus.get_object("org.deluge_torrent.dbusplugin"
|
||||||
, "/org/deluge_torrent/DelugeDbusPlugin")
|
, "/org/deluge_torrent/DelugeDbusPlugin")
|
||||||
config = pickle.load(open(config_file))
|
globals()['proxy'] = proxy
|
||||||
render = template.render('templates/%s/' % config.get('template'),
|
|
||||||
cache=config.get('cache_templates'))
|
|
||||||
|
|
||||||
def init():
|
def init_gtk_05():
|
||||||
#appy possibly changed config-vars, only called in when runing inside gtk.
|
#appy possibly changed config-vars, only called in when runing inside gtk.
|
||||||
path = os.path.dirname(__file__)
|
|
||||||
from dbus_interface import get_dbus_manager
|
from dbus_interface import get_dbus_manager
|
||||||
globals()['proxy'] = get_dbus_manager()
|
globals()['proxy'] = get_dbus_manager()
|
||||||
globals()['config'] = deluge.pref.Preferences(config_file, False)
|
globals()['config'] = deluge.pref.Preferences(config_file, False)
|
||||||
globals()['render'] = template.render(os.path.join(path, 'templates/%s/' %
|
globals()['render'] = subclassed_render(config.get('template'),
|
||||||
config.get('template')), cache=config.get('cache_templates'))
|
config.get('cache_templates'))
|
||||||
|
|
||||||
|
|
||||||
|
#hacks to determine environment, TODO: clean up.
|
||||||
|
if 'env=0.5' in sys.argv:
|
||||||
|
init_05()
|
||||||
|
elif not hasattr(deluge, 'common'):
|
||||||
|
init_06()
|
||||||
|
elif not hasattr(deluge,'pref'):
|
||||||
|
init_05()
|
||||||
|
|
||||||
|
|
||||||
REVNO = '0.56.stable.' + open(os.path.join(os.path.dirname(__file__),'revno')).read()
|
#constants
|
||||||
|
REVNO = open(os.path.join(os.path.dirname(__file__),'revno')).read()
|
||||||
VERSION = open(os.path.join(os.path.dirname(__file__),'version')).read()
|
VERSION = open(os.path.join(os.path.dirname(__file__),'version')).read()
|
||||||
|
|
||||||
TORRENT_KEYS = ['distributed_copies', 'download_payload_rate',
|
TORRENT_KEYS = ['distributed_copies', 'download_payload_rate',
|
||||||
|
@ -82,3 +138,30 @@ STATE_MESSAGES = (_("Queued"),
|
||||||
_("Finished"),
|
_("Finished"),
|
||||||
_("Seeding"),
|
_("Seeding"),
|
||||||
_("Allocating"))
|
_("Allocating"))
|
||||||
|
|
||||||
|
SPEED_VALUES = [
|
||||||
|
(-1, 'Unlimited'),
|
||||||
|
(5, '5.0 Kib/sec'),
|
||||||
|
(10, '10.0 Kib/sec'),
|
||||||
|
(15, '15.0 Kib/sec'),
|
||||||
|
(25, '25.0 Kib/sec'),
|
||||||
|
(30, '30.0 Kib/sec'),
|
||||||
|
(50, '50.0 Kib/sec'),
|
||||||
|
(80, '80.0 Kib/sec'),
|
||||||
|
(300, '300.0 Kib/sec'),
|
||||||
|
(500, '500.0 Kib/sec')
|
||||||
|
]
|
||||||
|
|
||||||
|
COOKIE_DEFAULTS = {
|
||||||
|
'auto_refresh_secs':'10'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
SESSIONS = pickle.load(open(session_file))
|
||||||
|
except:
|
||||||
|
SESSIONS = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -45,15 +45,21 @@ import webpy022 as web
|
||||||
from webpy022.webapi import cookies, setcookie as w_setcookie
|
from webpy022.webapi import cookies, setcookie as w_setcookie
|
||||||
from webpy022.http import seeother, url
|
from webpy022.http import seeother, url
|
||||||
from webpy022 import template,changequery as self_url
|
from webpy022 import template,changequery as self_url
|
||||||
|
from webpy022.utils import Storage
|
||||||
|
from static_handler import static_handler
|
||||||
|
|
||||||
|
from deluge.common import fsize,fspeed
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
import random
|
import random
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
import datetime
|
||||||
|
import pickle
|
||||||
|
from md5 import md5
|
||||||
|
|
||||||
from deluge import common
|
from deluge import common
|
||||||
from webserver_common import REVNO, VERSION
|
from webserver_common import REVNO, VERSION, COOKIE_DEFAULTS
|
||||||
import webserver_common as ws
|
import webserver_common as ws
|
||||||
|
|
||||||
from debugerror import deluge_debugerror
|
from debugerror import deluge_debugerror
|
||||||
|
|
||||||
#init:
|
#init:
|
||||||
|
@ -65,14 +71,22 @@ def setcookie(key, val):
|
||||||
"""add 30 days expires header for persistent cookies"""
|
"""add 30 days expires header for persistent cookies"""
|
||||||
return w_setcookie(key, val , expires=2592000)
|
return w_setcookie(key, val , expires=2592000)
|
||||||
|
|
||||||
SESSIONS = [] #dumb sessions.
|
#really simple sessions, to bad i had to implement them myself.
|
||||||
def start_session():
|
def start_session():
|
||||||
session_id = str(random.random())
|
session_id = str(random.random())
|
||||||
SESSIONS.append(session_id)
|
ws.SESSIONS.append(session_id)
|
||||||
|
if len(ws.SESSIONS) > 20: #save max 20 sessions?
|
||||||
|
ws.SESSIONS = ws.SESSIONS[-20:]
|
||||||
|
#not thread safe! , but a verry rare bug.
|
||||||
|
pickle.dump(ws.SESSIONS, open(ws.session_file,'wb'))
|
||||||
setcookie("session_id", session_id)
|
setcookie("session_id", session_id)
|
||||||
|
|
||||||
if getcookie('auto_refresh_secs') == None:
|
def end_session():
|
||||||
setcookie('auto_refresh_secs','10')
|
session_id = getcookie("session_id")
|
||||||
|
if session_id in ws.SESSIONS:
|
||||||
|
ws.SESSIONS.remove(session_id)
|
||||||
|
#not thread safe! , but a verry rare bug.
|
||||||
|
pickle.dump(ws.SESSIONS, open(ws.session_file,'wb'))
|
||||||
|
|
||||||
def do_redirect():
|
def do_redirect():
|
||||||
"""for redirects after a POST"""
|
"""for redirects after a POST"""
|
||||||
|
@ -92,7 +106,6 @@ def error_page(error):
|
||||||
print ws.render.error(error)
|
print ws.render.error(error)
|
||||||
|
|
||||||
def getcookie(key, default=None):
|
def getcookie(key, default=None):
|
||||||
COOKIE_DEFAULTS = {'auto_refresh_secs':'10'}
|
|
||||||
key = str(key).strip()
|
key = str(key).strip()
|
||||||
ck = cookies()
|
ck = cookies()
|
||||||
val = ck.get(key, default)
|
val = ck.get(key, default)
|
||||||
|
@ -120,9 +133,8 @@ def check_session(func):
|
||||||
"""
|
"""
|
||||||
def deco(self, name):
|
def deco(self, name):
|
||||||
vars = web.input(redir_after_login=None)
|
vars = web.input(redir_after_login=None)
|
||||||
|
|
||||||
ck = cookies()
|
ck = cookies()
|
||||||
if ck.has_key("session_id") and ck["session_id"] in SESSIONS:
|
if ck.has_key("session_id") and ck["session_id"] in ws.SESSIONS:
|
||||||
return func(self, name) #ok, continue..
|
return func(self, name) #ok, continue..
|
||||||
elif vars.redir_after_login:
|
elif vars.redir_after_login:
|
||||||
seeother(url("/login",redir=self_url()))
|
seeother(url("/login",redir=self_url()))
|
||||||
|
@ -154,6 +166,77 @@ def remote(func):
|
||||||
print traceback.format_exc()
|
print traceback.format_exc()
|
||||||
return deco
|
return deco
|
||||||
|
|
||||||
|
#utils:
|
||||||
|
def check_pwd(pwd):
|
||||||
|
m = md5()
|
||||||
|
m.update(ws.config.get('pwd_salt'))
|
||||||
|
m.update(pwd)
|
||||||
|
return (m.digest() == ws.config.get('pwd_md5'))
|
||||||
|
|
||||||
|
def get_stats():
|
||||||
|
stats = Storage({
|
||||||
|
'download_rate':fspeed(ws.proxy.get_download_rate()),
|
||||||
|
'upload_rate':fspeed(ws.proxy.get_upload_rate()),
|
||||||
|
'max_download':ws.proxy.get_config_value('max_download_speed_bps'),
|
||||||
|
'max_upload':ws.proxy.get_config_value('max_upload_speed_bps'),
|
||||||
|
})
|
||||||
|
if stats.max_upload < 0:
|
||||||
|
stats.max_upload = _("Unlimited")
|
||||||
|
else:
|
||||||
|
stats.max_upload = fspeed(stats.max_upload)
|
||||||
|
|
||||||
|
if stats.max_download < 0:
|
||||||
|
stats.max_download = _("Unlimited")
|
||||||
|
else:
|
||||||
|
stats.max_download = fspeed(stats.max_download)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
def get_torrent_status(torrent_id):
|
||||||
|
"""
|
||||||
|
helper method.
|
||||||
|
enhance ws.proxy.get_torrent_status with some extra data
|
||||||
|
"""
|
||||||
|
status = Storage(ws.proxy.get_torrent_status(torrent_id,ws.TORRENT_KEYS))
|
||||||
|
|
||||||
|
#add missing values for deluge 0.6:
|
||||||
|
for key in ws.TORRENT_KEYS:
|
||||||
|
if not key in status:
|
||||||
|
status[key] = 0
|
||||||
|
|
||||||
|
status["id"] = torrent_id
|
||||||
|
|
||||||
|
#for naming the status-images
|
||||||
|
status["calc_state_str"] = "downloading"
|
||||||
|
if status["paused"]:
|
||||||
|
status["calc_state_str"] = "inactive"
|
||||||
|
elif status["is_seed"]:
|
||||||
|
status["calc_state_str"] = "seeding"
|
||||||
|
|
||||||
|
#action for torrent_pause
|
||||||
|
if status["calc_state_str"] == "inactive":
|
||||||
|
status["action"] = "start"
|
||||||
|
else:
|
||||||
|
status["action"] = "stop"
|
||||||
|
|
||||||
|
if status["paused"]:
|
||||||
|
status["message"] = _("Paused %s%%") % status['progress']
|
||||||
|
else:
|
||||||
|
status["message"] = "%s %i%%" % (ws.STATE_MESSAGES[status["state"]]
|
||||||
|
, status['progress'])
|
||||||
|
|
||||||
|
#add some pre-calculated values
|
||||||
|
status.update({
|
||||||
|
"calc_total_downloaded" : (fsize(status["total_done"])
|
||||||
|
+ " (" + fsize(status["total_download"]) + ")"),
|
||||||
|
"calc_total_uploaded": (fsize(status['uploaded_memory']
|
||||||
|
+ status["total_payload_upload"]) + " ("
|
||||||
|
+ fsize(status["total_upload"]) + ")"),
|
||||||
|
})
|
||||||
|
return status
|
||||||
|
#/utils
|
||||||
|
|
||||||
#template-defs:
|
#template-defs:
|
||||||
def template_crop(text, end):
|
def template_crop(text, end):
|
||||||
if len(text) > end:
|
if len(text) > end:
|
||||||
|
@ -176,12 +259,15 @@ def template_sort_head(id,name):
|
||||||
|
|
||||||
return ws.render.sort_column_head(id, name, order, active_up, active_down)
|
return ws.render.sort_column_head(id, name, order, active_up, active_down)
|
||||||
|
|
||||||
|
def template_part_stats():
|
||||||
|
return ws.render.part_stats(get_stats())
|
||||||
|
|
||||||
def get_config(var):
|
def get_config(var):
|
||||||
return ws.config.get(var)
|
return ws.config.get(var)
|
||||||
|
|
||||||
template.Template.globals.update({
|
template.Template.globals.update({
|
||||||
'sort_head': template_sort_head,
|
'sort_head': template_sort_head,
|
||||||
|
'part_stats':template_part_stats,
|
||||||
'crop': template_crop,
|
'crop': template_crop,
|
||||||
'_': _ , #gettext/translations
|
'_': _ , #gettext/translations
|
||||||
'str': str, #because % in templetor is broken.
|
'str': str, #because % in templetor is broken.
|
||||||
|
@ -198,145 +284,20 @@ template.Template.globals.update({
|
||||||
})
|
})
|
||||||
#/template-defs
|
#/template-defs
|
||||||
|
|
||||||
|
def create_webserver(urls, methods):
|
||||||
|
from webpy022.request import webpyfunc
|
||||||
|
from webpy022 import webapi
|
||||||
|
from gtk_cherrypy_wsgiserver import CherryPyWSGIServer
|
||||||
|
|
||||||
|
func = webapi.wsgifunc(webpyfunc(urls, methods, False))
|
||||||
#------------------------------------------------------------------------------
|
server_address=("0.0.0.0", int(ws.config.get('port')))
|
||||||
#Some copy and paste from web.py
|
|
||||||
#mostly caused by /static
|
|
||||||
#TODO : FIX THIS.
|
|
||||||
#static-files serving should be moved to the normal webserver!
|
|
||||||
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
|
||||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
|
||||||
from gtk_cherrypy_wsgiserver import CherryPyWSGIServer
|
|
||||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
|
||||||
|
|
||||||
from webpy022.request import webpyfunc
|
|
||||||
from webpy022 import webapi
|
|
||||||
import os
|
|
||||||
|
|
||||||
import posixpath
|
|
||||||
import urllib
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
class RelativeHandler(SimpleHTTPRequestHandler):
|
|
||||||
def translate_path(self, path):
|
|
||||||
"""Translate a /-separated PATH to the local filename syntax.
|
|
||||||
|
|
||||||
Components that mean special things to the local file system
|
|
||||||
(e.g. drive or directory names) are ignored. (XXX They should
|
|
||||||
probably be diagnosed.)
|
|
||||||
|
|
||||||
"""
|
|
||||||
# abandon query parameters
|
|
||||||
path = urlparse.urlparse(path)[2]
|
|
||||||
path = posixpath.normpath(urllib.unquote(path))
|
|
||||||
words = path.split('/')
|
|
||||||
words = filter(None, words)
|
|
||||||
path = os.path.dirname(__file__)
|
|
||||||
for word in words:
|
|
||||||
drive, word = os.path.splitdrive(word)
|
|
||||||
head, word = os.path.split(word)
|
|
||||||
if word in (os.curdir, os.pardir): continue
|
|
||||||
path = os.path.join(path, word)
|
|
||||||
return path
|
|
||||||
|
|
||||||
class StaticApp(RelativeHandler):
|
|
||||||
"""WSGI application for serving static files."""
|
|
||||||
def __init__(self, environ, start_response):
|
|
||||||
self.headers = []
|
|
||||||
self.environ = environ
|
|
||||||
self.start_response = start_response
|
|
||||||
|
|
||||||
def send_response(self, status, msg=""):
|
|
||||||
self.status = str(status) + " " + msg
|
|
||||||
|
|
||||||
def send_header(self, name, value):
|
|
||||||
self.headers.append((name, value))
|
|
||||||
|
|
||||||
def end_headers(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def log_message(*a): pass
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
environ = self.environ
|
|
||||||
|
|
||||||
self.path = environ.get('PATH_INFO', '')
|
|
||||||
self.client_address = environ.get('REMOTE_ADDR','-'), \
|
|
||||||
environ.get('REMOTE_PORT','-')
|
|
||||||
self.command = environ.get('REQUEST_METHOD', '-')
|
|
||||||
|
|
||||||
from cStringIO import StringIO
|
|
||||||
self.wfile = StringIO() # for capturing error
|
|
||||||
|
|
||||||
f = self.send_head()
|
|
||||||
self.start_response(self.status, self.headers)
|
|
||||||
|
|
||||||
if f:
|
|
||||||
block_size = 16 * 1024
|
|
||||||
while True:
|
|
||||||
buf = f.read(block_size)
|
|
||||||
if not buf:
|
|
||||||
break
|
|
||||||
yield buf
|
|
||||||
f.close()
|
|
||||||
else:
|
|
||||||
value = self.wfile.getvalue()
|
|
||||||
yield value
|
|
||||||
|
|
||||||
class WSGIWrapper(BaseHTTPRequestHandler):
|
|
||||||
"""WSGI wrapper for logging the status and serving static files."""
|
|
||||||
def __init__(self, app):
|
|
||||||
self.app = app
|
|
||||||
self.format = '%s - - [%s] "%s %s %s" - %s'
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
def xstart_response(status, response_headers, *args):
|
|
||||||
write = start_response(status, response_headers, *args)
|
|
||||||
self.log(status, environ)
|
|
||||||
return write
|
|
||||||
|
|
||||||
path = environ.get('PATH_INFO', '')
|
|
||||||
if path.startswith('/static/'):
|
|
||||||
return StaticApp(environ, xstart_response)
|
|
||||||
else:
|
|
||||||
return self.app(environ, xstart_response)
|
|
||||||
|
|
||||||
def log(self, status, environ):
|
|
||||||
#mvoncken,no logging..
|
|
||||||
return
|
|
||||||
|
|
||||||
outfile = environ.get('wsgi.errors', web.debug)
|
|
||||||
req = environ.get('PATH_INFO', '_')
|
|
||||||
protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-')
|
|
||||||
method = environ.get('REQUEST_METHOD', '-')
|
|
||||||
host = "%s:%s" % (environ.get('REMOTE_ADDR','-'),
|
|
||||||
environ.get('REMOTE_PORT','-'))
|
|
||||||
|
|
||||||
#@@ It is really bad to extend from
|
|
||||||
#@@ BaseHTTPRequestHandler just for this method
|
|
||||||
time = self.log_date_time_string()
|
|
||||||
|
|
||||||
print >> outfile, self.format % (host, time, protocol,
|
|
||||||
method, req, status)
|
|
||||||
|
|
||||||
def create_webserver(urls,methods):
|
|
||||||
func = webapi.wsgifunc(webpyfunc(urls,methods, False))
|
|
||||||
server_address=("0.0.0.0",ws.config.get('port'))
|
|
||||||
|
|
||||||
func = WSGIWrapper(func)
|
|
||||||
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
|
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
|
||||||
|
print "http://%s:%d/" % server_address
|
||||||
|
|
||||||
print "(created) http://%s:%d/" % server_address
|
|
||||||
|
|
||||||
return server
|
return server
|
||||||
|
|
||||||
#------
|
#------
|
||||||
__all__ = ['deluge_page_noauth', 'deluge_page', 'remote',
|
__all__ = ['deluge_page_noauth', 'deluge_page', 'remote',
|
||||||
'auto_refreshed', 'check_session',
|
'auto_refreshed', 'check_session',
|
||||||
'do_redirect', 'error_page','start_session','getcookie'
|
'do_redirect', 'error_page','start_session','getcookie'
|
||||||
,'create_webserver','setcookie']
|
,'setcookie','create_webserver','end_session',
|
||||||
|
'get_torrent_status', 'check_pwd','static_handler']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -54,4 +54,7 @@ plugins/WebSeed/__init__.py
|
||||||
plugins/WebSeed/webseed.glade
|
plugins/WebSeed/webseed.glade
|
||||||
plugins/Scheduler/__init__.py
|
plugins/Scheduler/__init__.py
|
||||||
plugins/Scheduler/plugin.py
|
plugins/Scheduler/plugin.py
|
||||||
plugins/WebUI/__init__.py
|
plugins/WebUi/__init__.py
|
||||||
|
plugins/WebUi/webserver_common.py
|
||||||
|
plugins/WebUi/deluge_webserver.py
|
||||||
|
plugins/WebUi/scripts/template_strings.py
|
||||||
|
|
238
po/deluge.pot
238
po/deluge.pot
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2007-10-25 21:56-0500\n"
|
"POT-Creation-Date: 2007-10-27 20:34-0500\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
@ -88,7 +88,7 @@ msgstr ""
|
||||||
msgid "<b>Torrent Info</b>"
|
msgid "<b>Torrent Info</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:750
|
#: glade/delugegtk.glade:750 plugins/WebUi/scripts/template_strings.py:8
|
||||||
msgid "Details"
|
msgid "Details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -149,11 +149,13 @@ msgid "Status"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:965 src/interface.py:602
|
#: glade/delugegtk.glade:965 src/interface.py:602
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:19
|
||||||
msgid "Seeders"
|
msgid "Seeders"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:974 src/interface.py:605
|
#: glade/delugegtk.glade:974 src/interface.py:605
|
||||||
#: plugins/TorrentPeers/__init__.py:72
|
#: plugins/TorrentPeers/__init__.py:72
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:15
|
||||||
msgid "Peers"
|
msgid "Peers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -171,11 +173,11 @@ msgstr ""
|
||||||
msgid "Time Remaining"
|
msgid "Time Remaining"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:1010
|
#: glade/delugegtk.glade:1010 plugins/WebUi/scripts/template_strings.py:4
|
||||||
msgid "Availability"
|
msgid "Availability"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:1019
|
#: glade/delugegtk.glade:1019 plugins/WebUi/scripts/template_strings.py:21
|
||||||
msgid "Share Ratio"
|
msgid "Share Ratio"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -211,7 +213,7 @@ msgstr ""
|
||||||
msgid "Remove Torrent"
|
msgid "Remove Torrent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/delugegtk.glade:1157
|
#: glade/delugegtk.glade:1157 plugins/WebUi/scripts/template_strings.py:18
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -278,7 +280,7 @@ msgstr ""
|
||||||
msgid "Delete downloaded files"
|
msgid "Delete downloaded files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/dgtkpopups.glade:88
|
#: glade/dgtkpopups.glade:88 plugins/WebUi/scripts/template_strings.py:6
|
||||||
msgid "Delete .torrent file"
|
msgid "Delete .torrent file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -294,7 +296,7 @@ msgstr ""
|
||||||
msgid "Clear Finished"
|
msgid "Clear Finished"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/dgtkpopups.glade:241
|
#: glade/dgtkpopups.glade:241 plugins/WebUi/scripts/template_strings.py:22
|
||||||
msgid "Speed"
|
msgid "Speed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -542,6 +544,7 @@ msgid "<b>Seeding</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: glade/preferences_dialog.glade:964 src/core.py:91
|
#: glade/preferences_dialog.glade:964 src/core.py:91
|
||||||
|
#: plugins/WebUi/webserver_common.py:139
|
||||||
msgid "Seeding"
|
msgid "Seeding"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -646,6 +649,7 @@ msgstr ""
|
||||||
|
|
||||||
#: glade/preferences_dialog.glade:1478 glade/preferences_dialog.glade:1672
|
#: glade/preferences_dialog.glade:1478 glade/preferences_dialog.glade:1672
|
||||||
#: glade/preferences_dialog.glade:1866 glade/preferences_dialog.glade:2060
|
#: glade/preferences_dialog.glade:1866 glade/preferences_dialog.glade:2060
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:13
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1004,7 +1008,7 @@ msgstr ""
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/interface.py:614
|
#: src/interface.py:614 plugins/WebUi/scripts/template_strings.py:10
|
||||||
msgid "ETA"
|
msgid "ETA"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1088,31 +1092,32 @@ msgstr ""
|
||||||
msgid "Are you sure that you want to remove all seeding torrents?"
|
msgid "Are you sure that you want to remove all seeding torrents?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:85
|
#: src/core.py:85 plugins/WebUi/webserver_common.py:133
|
||||||
msgid "Queued"
|
msgid "Queued"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:86
|
#: src/core.py:86 plugins/WebUi/webserver_common.py:134
|
||||||
msgid "Checking"
|
msgid "Checking"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:87
|
#: src/core.py:87 plugins/WebUi/webserver_common.py:135
|
||||||
msgid "Connecting"
|
msgid "Connecting"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:88
|
#: src/core.py:88 plugins/WebUi/webserver_common.py:136
|
||||||
msgid "Downloading Metadata"
|
msgid "Downloading Metadata"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:89 plugins/BlocklistImport/ui.py:117
|
#: src/core.py:89 plugins/BlocklistImport/ui.py:117
|
||||||
|
#: plugins/WebUi/webserver_common.py:137
|
||||||
msgid "Downloading"
|
msgid "Downloading"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:90
|
#: src/core.py:90 plugins/WebUi/webserver_common.py:138
|
||||||
msgid "Finished"
|
msgid "Finished"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/core.py:92
|
#: src/core.py:92 plugins/WebUi/webserver_common.py:140
|
||||||
msgid "Allocating"
|
msgid "Allocating"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1390,66 +1395,66 @@ msgstr ""
|
||||||
msgid "Torrent Creator"
|
msgid "Torrent Creator"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:103
|
#: plugins/TorrentCreator/torrentcreator.glade:102
|
||||||
msgid "This torrent will be made from a single file"
|
msgid "This torrent will be made from a single file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:104
|
#: plugins/TorrentCreator/torrentcreator.glade:103
|
||||||
msgid "File:"
|
msgid "File:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:120
|
#: plugins/TorrentCreator/torrentcreator.glade:119
|
||||||
msgid "This torrent will be made from a directory"
|
msgid "This torrent will be made from a directory"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:121
|
#: plugins/TorrentCreator/torrentcreator.glade:120
|
||||||
msgid "Folder:"
|
msgid "Folder:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:138
|
#: plugins/TorrentCreator/torrentcreator.glade:137
|
||||||
msgid "<b>Source</b>"
|
msgid "<b>Source</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:184
|
#: plugins/TorrentCreator/torrentcreator.glade:182
|
||||||
msgid "Save Torrent File As:"
|
msgid "Save Torrent File As:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:237
|
#: plugins/TorrentCreator/torrentcreator.glade:235
|
||||||
msgid "Load this torrent into Deluge for seeding"
|
msgid "Load this torrent into Deluge for seeding"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:238
|
#: plugins/TorrentCreator/torrentcreator.glade:236
|
||||||
msgid "Add new torrent to queue"
|
msgid "Add new torrent to queue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:257
|
#: plugins/TorrentCreator/torrentcreator.glade:255
|
||||||
msgid "<b>Torrent File</b>"
|
msgid "<b>Torrent File</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:305
|
#: plugins/TorrentCreator/torrentcreator.glade:302
|
||||||
msgid "<b>Trackers</b>"
|
msgid "<b>Trackers</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:352
|
#: plugins/TorrentCreator/torrentcreator.glade:348
|
||||||
msgid "<b>Comments</b>"
|
msgid "<b>Comments</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:389
|
#: plugins/TorrentCreator/torrentcreator.glade:384
|
||||||
msgid "<b>Author</b>"
|
msgid "<b>Author</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:425
|
#: plugins/TorrentCreator/torrentcreator.glade:423
|
||||||
msgid "Set Private Flag"
|
msgid "Set Private Flag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:441
|
#: plugins/TorrentCreator/torrentcreator.glade:436
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:461
|
#: plugins/TorrentCreator/torrentcreator.glade:457
|
||||||
msgid ""
|
msgid ""
|
||||||
"The smaller the piece sizes, the more efficient the transfers will be, but "
|
"The smaller the piece sizes, the more efficient the transfers will be, but "
|
||||||
"the actual \".torrent\" file will be larger"
|
"the actual \".torrent\" file will be larger"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:442
|
#: plugins/TorrentCreator/torrentcreator.glade:437
|
||||||
msgid ""
|
msgid ""
|
||||||
"32 KiB\n"
|
"32 KiB\n"
|
||||||
"64 KiB\n"
|
"64 KiB\n"
|
||||||
|
@ -1457,13 +1462,14 @@ msgid ""
|
||||||
"256 KiB\n"
|
"256 KiB\n"
|
||||||
"512 KiB\n"
|
"512 KiB\n"
|
||||||
"1024 KiB\n"
|
"1024 KiB\n"
|
||||||
|
"2048 KiB\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:463
|
#: plugins/TorrentCreator/torrentcreator.glade:459
|
||||||
msgid "Piece Size:"
|
msgid "Piece Size:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: plugins/TorrentCreator/torrentcreator.glade:477
|
#: plugins/TorrentCreator/torrentcreator.glade:473
|
||||||
msgid "<b>Advanced</b>"
|
msgid "<b>Advanced</b>"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -2102,3 +2108,171 @@ msgid ""
|
||||||
"When set to -1 (unlimited), the global limits in Deluge's preferences will "
|
"When set to -1 (unlimited), the global limits in Deluge's preferences will "
|
||||||
"be obeyed."
|
"be obeyed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:31
|
||||||
|
msgid "Web User Interface"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:34
|
||||||
|
msgid ""
|
||||||
|
"A Web based User Interface\n"
|
||||||
|
"\n"
|
||||||
|
"Firefox greasemonkey script: http://userscripts.org/scripts/show/12639\n"
|
||||||
|
"\n"
|
||||||
|
"Remotely add a file: \"curl -F torrent=@./test1.torrent -F pwd=deluge http://"
|
||||||
|
"localhost:8112/remote/torrent/add\"\n"
|
||||||
|
"\n"
|
||||||
|
"There is support for multiple templates, but just one is included.\n"
|
||||||
|
"\n"
|
||||||
|
"Other contributors:\n"
|
||||||
|
"*somedude : template enhancements.\n"
|
||||||
|
"*markybob : stability : synced with his changes in deluge-svn.\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:179
|
||||||
|
msgid "WebUi Config"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:187
|
||||||
|
msgid "Port Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:188
|
||||||
|
msgid "New Password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:189
|
||||||
|
msgid "New Password(confirm)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:190
|
||||||
|
msgid "Template"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:191
|
||||||
|
msgid "Button Style"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:193
|
||||||
|
msgid "Cache Templates"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:209
|
||||||
|
msgid "Text and image"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:209
|
||||||
|
msgid "Image Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:209
|
||||||
|
msgid "Text Only"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/__init__.py:243
|
||||||
|
msgid "Confirmed Password <> New Password\n"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/webserver_common.py:48
|
||||||
|
msgid "translate something"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/deluge_webserver.py:142
|
||||||
|
msgid "Choose an url or a torrent, not both."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/deluge_webserver.py:153
|
||||||
|
msgid "no data."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/deluge_webserver.py:230
|
||||||
|
msgid "refresh must be > 0"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/deluge_webserver.py:238
|
||||||
|
msgid "Download Speed Limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/deluge_webserver.py:242
|
||||||
|
msgid "Upload Speed Limit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:1
|
||||||
|
msgid "# Of Files"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:2
|
||||||
|
msgid "About"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:3
|
||||||
|
msgid "Auto refresh:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:5
|
||||||
|
msgid "Debug:Data Dump"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:7
|
||||||
|
msgid "Delete downloaded files."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:9
|
||||||
|
msgid "Downloaded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:11
|
||||||
|
msgid "Next Announce"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:12
|
||||||
|
msgid "Off"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:14
|
||||||
|
msgid "Password is invalid,try again"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:16
|
||||||
|
msgid "Pieces"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:17
|
||||||
|
msgid "Refresh page every:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:20
|
||||||
|
msgid "Set"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:23
|
||||||
|
msgid "Submit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:24
|
||||||
|
msgid "Total Size"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:25
|
||||||
|
msgid "Tracker"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:26
|
||||||
|
msgid "Tracker Status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:27
|
||||||
|
msgid "Upload torrent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:28
|
||||||
|
msgid "Uploaded"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:29
|
||||||
|
msgid "Url"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: plugins/WebUi/scripts/template_strings.py:30
|
||||||
|
msgid "seconds"
|
||||||
|
msgstr ""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue