mlshell es una interfaz sencilla para programar operaciones en ml-donkey en python e incluye, como ejemplo, un optimizador de descargas.

Como muchos uso la red ed2k (entre otras) para realizar descargas, pero mis descargas no suelen ir a toda velocidad por dos razones:
- Mi ISP apesta: Limita mi cantidad de conexiones y tiene picos de trafico muy marcados, hay horas del día en que se pone insoportablemente lento.
- Tengo gustos raros: Mi música favorita no es del tipo que suele aparecer en los rankings ni en las listas de ventas. Pocos la oímos, pocos la tenemos, pocos la compartimos.
En consecuencia me resulta muy difícil conseguir buenos recursos y, más aún, verlos en linea. Por eso me duele mucho ver los logs y enterarme que ese álbum que tanto quiero tardará una semana más en llegar porque las dos horas que estuvo enteramente disponible yo tenía un limite de descargas de 6 KB/s.
Pero tampoco puedo dejar mi mula con un limite de descarga alto porque, aunque no descargará nunca muy rápido, la velocidad de subida está siempre a tope y eso afecta muy notoriamente en la navegación.
La solución: escribir un script que ajuste la velocidad de bajada/subida de acuerdo a la disponibilidad de mis descargas.
El problema: mldonkey_command parece estar roto en las nuevas versiones de mlnet como ejecutable independiente y no parece que vaya a ser reparado pronto, de hecho... algunos script distribuidos con el proyecto lo reemplazaron con una interfaz muy primitiva escrita en perl que obtiene los datos desde la interfaz web.
Entonces, antes de escribir un script que procese la información tuve que escribir uno que la pueda obtener del servidor y, también, modificar las configuraciones sobre la marcha. Para esto escribí un modulo python que usa urllib2 para manejar mldonkey a través de su interfaz web y le agregué algo de abstracción.
Nota: Depende de /usr/bin/html2text, que se encuentra en el paquete html2text en debian.
programacion/python/mlshell.py (ver ultima versión)
- """mlshell: Wrapper to the web interface of ml-donkey that implement minimal
- shell capacities and can be used to automatice operacions."""
- import urllib2
- import base64
- import subprocess
- import re
- import os
- import logging
- from ConfigParser import SafeConfigParser
- from optparse import OptionParser, OptionValueError
- logging.root.setLevel(5)
- def html2text(html):
- """Process a html document to render as simple text"""
- proc = subprocess.Popen(["/usr/bin/html2text", "-style", "compact"],
- stdin=subprocess.PIPE, stdout=subprocess.PIPE)
- proc.stdin.writelines(html)
- proc.stdin.close()
- text = unicode("".join(proc.stdout.readlines()), 'utf-8', 'replace').strip()
- text = re.compile(r""":(\s*)""").sub(": ", text)
- text = re.compile(r"""(\s*)wiki$""", re.MULTILINE).sub("", text)
- return text
- class MLSession:
- """Wrapper to the web interface of ml-donkey"""
- def __init__(self, user=None, password=None, host="localhost", port=4080):
- if user and password:
- self.header = "Basic " + base64.encodestring("%s:%s" %
- (user, password)).strip()
- else:
- self.header = None
- self.host = host
- self.port = port
- self.fmt_prompt = u"(d%.1fKB/s|u%.1fKB/s): "
- self.locals = {
- "s" : self.bw_stats,
- "auto_rate" : lambda: self.optimize_rate_limits(
- self.variables.get("maxdownload", None),
- self.variables.get("maxupload", None),
- self.variables.get("minupload", None),
- self.variables.get("margin", None))
- }
- self.variables = {}
- def bw_stats(self):
- """Return the current bandwidth stats: (down, up)"""
- stats = [st.strip() for st in self.execute("bw_stats").split()]
- down_rate = float(stats[1])
- up_rate = float(stats[4])
- return down_rate, up_rate
- def optimize_rate_limits(self, maxdownload=None, maxupload=None,
- minupload=None, margin=None):
- """Optimize the rate limits to the best download rate."""
- maxdownload = maxdownload or self.variables.get("maxdownload", "0")
- maxupload = maxupload or self.variables.get("maxupload", "10")
- minupload = minupload or self.variables.get("minupload", "1")
- margin = margin or self.variables.get("margin", "down_rate ** (1/3.)")
- table = {36:9, 32:8, 28:7, 24:6, 20:5, 16:4, 9:3, 6:2, 3:1}
- down_rate, up_rate = self.bw_stats()[:2]
- self.variables.update((("down_rate", down_rate), ("up_rate", up_rate)))
- maxdownload = eval(str(maxdownload), self.variables)
- maxupload = eval(str(maxupload), self.variables)
- if maxupload == 0:
- maxupload = 10
- minupload = eval(str(minupload), self.variables)
- margin = eval(str(margin), self.variables)
- down_limit = down_rate + margin
- down_limit = min((k for k in table.iterkeys() if k >= down_limit))
- if maxdownload:
- if down_limit == 0:
- down_limit = maxdownload
- else:
- down_limit = min(down_limit, maxdownload)
- up_limit = min(max(table.get(down_limit, 10), minupload), maxupload)
- logging.debug(" ".join((" down_rate: %.2f", "up_rate: %.2f",
- "margin: %.2f", "down_limit: %d", "up_limit: %d")) %
- (down_rate, up_rate, margin, down_limit, up_limit))
- logging.debug(self.execute("set max_hard_upload_rate %d" % up_limit))
- logging.debug(self.execute("set max_hard_download_rate %d" %
- down_limit))
- return down_limit, up_limit
- def get_prompt(self):
- """Generate a prompt text"""
- return self.fmt_prompt % self.bw_stats()
- def execute(self, order):
- """Find the method asosiated or the order on the web interface"""
- if order in self.locals:
- return self.locals[order]()
- else:
- order = order.replace(" ", "+")
- req = urllib2.Request("http://%s:%d/submit?q=%s" % (self.host,
- self.port, order))
- req.add_header('Authorization', self.header)
- response = urllib2.urlopen(req)
- html = "".join(response.readlines())
- return html2text(html)
- def main():
- """Main rutine"""
-
-
- config = SafeConfigParser({
- "host":"localhost",
- "port":"4080",
- "user":None,
- "password":None,
- "maxdownload":"0",
- "maxupload":"10",
- "minupload":"1",
- "margin":"down_rate ** (1/3.)",
- })
-
- config.read(os.path.expanduser('~/.mlshell'))
-
- def define_variable(option, opt_str, value, parser):
- """Handle the -d/--define option and populate the variables dict"""
- logging.debug(option.dest)
- logging.debug(value)
- variables = getattr(parser.values, option.dest)
- try:
- variable = re.search(r"".join(("^\s*([a-zA-Z_][a-zA-Z\d_]*)",
- "\s*=\s*(.*)\s*$")), value).groups()
- except AttributeError:
- raise OptionValueError("Declaración incorrecta: %s" % value)
- else:
- variables.update((variable,))
- logging.debug(variables)
-
- parser = OptionParser(usage="""
- %prog [-vqd]
- %prog [-vqd] file
- %prog [-vqdc] command""", version="%prog 2")
-
- parser.add_option("-c", help="Read the comands from" +
- " the arg instead of from the standard input", action="store_true",
- dest="command")
- parser.add_option("-v", "--verbose", action="count", dest="verbose")
- parser.add_option("-q", "--quiet", action="count", dest="quiet")
- parser.add_option("-d", "--define", metavar="VAR=VALUE", action="callback",
- callback=define_variable, type="string", nargs=1, dest="variables",
- help="Define a variable VAR to VALUE")
-
- parser.set_defaults(verbose=2, quiet=0, variables={})
-
- options, args = parser.parse_args()
-
-
- session = MLSession(
- config.get("Server", "user"),
- config.get("Server", "password")
- )
- for variable in config.options("Variables"):
- session.variables[variable] = config.get("Variables", variable)
- for variable in options.variables:
- session.variables[variable] = options.variables[variable]
- logging.debug(session.variables)
- if options.command:
- for order in args:
- print(session.execute(order))
- return 0
- elif args:
- print("Leyendo comandos en %s" % args[0])
- else:
- order = None
- while order not in ("q", "kill"):
- try:
- order = raw_input(session.get_prompt()).strip()
- except EOFError:
- return
- else:
- print(session.execute(order))
- if __name__ == "__main__":
- exit(main())
Si se lo ejecuta como programa independiente funciona de modo similar a una shell (de ahí su nombre) leyendo los comandos de la entrada estándar o de un archivo pasado como parámetro y escribiendo en la salida estándar. Las opciones pueden ser guardadas en un archivo de configuración ~/.mlshell
.mlshell
- [Server]
- host = localhost
- port = 4080
- user = admin
- password = mipass
- [Variables]
- maxdownload = 0
- maxupload = 0
- minupload = 1
- margin = down_rate ** (1/3.)
Como ejemplo escribí un par de ordenes sencillas, entre ellas la mejor es auto_rate, que al ejecutarse ajustará las velocidades de bajada y subida para sacar mejor provecho a los recursos actuales sin derrochar ancho de banda
. Para sacar mejor provecho lo instalé en mi crontab (usando la opción -c de mlshell):
crontab
- SHELL=/bin/sh
- PATH="/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/bin:/usr/bin/X11:/usr/games:/home/deimos/bin"
- * * * * * mlshell -c auto_rate > /dev/null 2>&1
Opcionalmente podemos poner limites en franjas horarias determinadas usando "-d maxdownload=24" o "-d maxupload=4", ecetera. Otros ejemplos los dejo como ejercicio del lector, que no por nada escribí tantas opciones con sus respectivas ayudas
.
A modo de ejemplo os pongo unas gráficas, esta la tomé luego de tener la primer versión instalada en crontab, observad el progreso:

Esta segunda muestra el funcionamiento normal de mis descargas usando auto_rate con un limite de descarga de 36 KB/s:

(Notad la casi perfecta simetría entre velocidad de descarga y subida)
Espero que a alguien más le sea útil mi ocurrencia
.
¿Y qué se supone que es lo que hace?
¿Y qué se supone que es lo que hace?
Si que eres rápido, lo publiqué para ver como quedaba el resaltado, ya termino de escribir
¿Y qué se supone que es lo que hace?
Si que eres rápido, lo publiqué para ver como quedaba el resaltado, ya termino de escribir
ok, ok.
Me encanta mlnet =D.
Mi ISP apesta
Bienvenido al club de usuarios de ISPs apestosos. ¿El tuyo limita la descarga desde http a velocidades de modém? El mío por lo menos sí.
Muy buena la foto de la mula.
¿Esperabas que te comentaran más?
¿Esperabas que te comentaran más?
jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.
¿Esperabas que te comentaran más?
jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos ml y nos gusta programarlo todo. Pero.. la verdad que no, estoy empezando a comprender los gustos de la audiencia.
Yo uso mlnet... pero mi conexión es bastante buena para no necesitar esto por suerte... de todas formas es muy util y me lo he guardado para usarlo en el futuro si me es necesario.
Saludos
¿Esperabas que te comentaran más?
jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.
Si tan solo tuviese internet...
Pero buee... sigue asi puntito, que lo que haces es genial.
Yo descargaba en la mula y alla en mi pais me hacia real falta eso :)
Saludos!
¿Esperabas que te comentaran más?
jajajaja, pues... a mi me cambió la vida. Supuse que eramos más los que usamos mldonkey y nos gusta programarlo todo. Pero.. la verdad que no me sorprendí, estoy empezando a comprender los gustos de la audiencia.
esto, yo es que uso el amule
gracias por el script que me sirve como ejemplo para aprender python
Muy bueno, me lo guardo por si alguna vez lo necesito :P
Tengo gustos raros: Mi música favorita no es del tipo que suele aparecer en los rankings ni en las listas de ventas. Pocos la oímos, pocos la tenemos, pocos la compartimos.
Y cual es?