Files
libtorrent/bindings/python/client.py
T
2023-08-26 11:15:27 +02:00

433 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright Daniel Wallin 2006. Use, modification and distribution is
# subject to the Boost Software License, Version 1.0. (See accompanying
# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
import atexit
import optparse
import os.path
import sys
import time
from typing import Any
from typing import Dict
from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from typing import Union
import libtorrent as lt
class WindowsConsole:
def __init__(self) -> None:
self.console = Console.getconsole()
def clear(self) -> None:
self.console.page()
def write(self, msg: str) -> None:
self.console.write(msg)
def sleep_and_input(self, seconds: float) -> Optional[bytes]:
time.sleep(seconds)
# Helps mypy
if sys.platform == "win32":
if msvcrt.kbhit():
return msvcrt.getch()
return None
class UnixConsole:
def __init__(self) -> None:
self.fd = sys.stdin
self.old = termios.tcgetattr(self.fd.fileno())
new = termios.tcgetattr(self.fd.fileno())
new[3] = new[3] & ~termios.ICANON
new[6][termios.VTIME] = 0
new[6][termios.VMIN] = 1
termios.tcsetattr(self.fd.fileno(), termios.TCSADRAIN, new)
atexit.register(self._onexit)
def _onexit(self) -> None:
termios.tcsetattr(self.fd.fileno(), termios.TCSADRAIN, self.old)
def clear(self) -> None:
sys.stdout.write("\033[2J\033[0;0H")
sys.stdout.flush()
def write(self, msg: str) -> None:
sys.stdout.write(msg)
sys.stdout.flush()
def sleep_and_input(self, seconds: float) -> Optional[str]:
read, __, __ = select.select([self.fd.fileno()], [], [], seconds)
if len(read) > 0:
return self.fd.read(1)
return None
ConsoleType = Union[UnixConsole, WindowsConsole]
if os.name == "nt":
import msvcrt
import Console
else:
import select
import termios
def add_suffix(val: float) -> str:
prefix = ["B", "kB", "MB", "GB", "TB"]
for i in range(len(prefix)):
if abs(val) < 1000:
if i == 0:
return "%5.3g%s" % (val, prefix[i])
else:
return "%4.3g%s" % (val, prefix[i])
val /= 1000
return "%6.3gPB" % val
def progress_bar(progress: float, width: int) -> str:
assert progress <= 1
progress_chars = int(progress * width + 0.5)
return progress_chars * "#" + (width - progress_chars) * "-"
def print_peer_info(console: ConsoleType, peers: List[lt.peer_info]) -> None:
out = " down (total ) up (total )" " q r flags block progress client\n"
for p in peers:
out += "%s/s " % add_suffix(p.down_speed)
out += "(%s) " % add_suffix(p.total_download)
out += "%s/s " % add_suffix(p.up_speed)
out += "(%s) " % add_suffix(p.total_upload)
out += "%2d " % p.download_queue_length
out += "%2d " % p.upload_queue_length
out += "I" if p.flags & lt.peer_info.interesting else "."
out += "C" if p.flags & lt.peer_info.choked else "."
out += "i" if p.flags & lt.peer_info.remote_interested else "."
out += "c" if p.flags & lt.peer_info.remote_choked else "."
out += "e" if p.flags & lt.peer_info.supports_extensions else "."
out += "l" if p.flags & lt.peer_info.local_connection else "r"
out += " "
if p.downloading_piece_index >= 0:
assert p.downloading_progress <= p.downloading_total
out += progress_bar(float(p.downloading_progress) / p.downloading_total, 15)
else:
out += progress_bar(0, 15)
out += " "
if p.flags & lt.peer_info.handshake:
id = "waiting for handshake"
elif p.flags & lt.peer_info.connecting:
id = "connecting to peer"
else:
try:
id = p.client.decode()
except ValueError:
id = p.client.hex()
out += "%s\n" % id[:10]
console.write(out)
def print_download_queue(
console: ConsoleType, download_queue: Sequence[Mapping[str, Any]]
) -> None:
out = ""
for e in download_queue:
out += "%4d: [" % e["piece_index"]
for b in e["blocks"]:
s = b["state"]
if s == 3:
out += "#"
elif s == 2:
out += "="
elif s == 1:
out += "-"
else:
out += " "
out += "]\n"
console.write(out)
def add_torrent(ses: lt.session, filename: str, options: optparse.Values) -> None:
atp = lt.add_torrent_params()
if filename.startswith("magnet:"):
atp = lt.parse_magnet_uri(filename)
else:
ti = lt.torrent_info(filename)
resume_file = os.path.join(options.save_path, ti.name() + ".fastresume")
try:
atp = lt.read_resume_data(open(resume_file, "rb").read())
except Exception as e:
print('failed to open resume file "%s": %s' % (resume_file, e))
atp.ti = ti
atp.save_path = options.save_path
atp.storage_mode = lt.storage_mode_t.storage_mode_sparse
atp.flags |= (
lt.torrent_flags.duplicate_is_error
| lt.torrent_flags.auto_managed
| lt.torrent_flags.duplicate_is_error
)
ses.async_add_torrent(atp)
def main() -> None:
parser = optparse.OptionParser()
parser.add_option("-p", "--port", type="int", help="set listening port")
parser.add_option(
"-i",
"--listen-interface",
type="string",
help="set interface for incoming connections",
)
parser.add_option(
"-o",
"--outgoing-interface",
type="string",
help="set interface for outgoing connections",
)
parser.add_option(
"-d",
"--max-download-rate",
type="float",
help="the maximum download rate given in kB/s. 0 means infinite.",
)
parser.add_option(
"-u",
"--max-upload-rate",
type="float",
help="the maximum upload rate given in kB/s. 0 means infinite.",
)
parser.add_option(
"-s",
"--save-path",
type="string",
help="the path where the downloaded file/folder should be placed.",
)
parser.add_option(
"-r",
"--proxy-host",
type="string",
help="sets HTTP proxy host and port (separated by ':')",
)
parser.set_defaults(
port=6881,
listen_interface="0.0.0.0",
outgoing_interface="",
max_download_rate=0,
max_upload_rate=0,
save_path=".",
proxy_host="",
)
(options, args) = parser.parse_args()
if options.port < 0 or options.port > 65525:
options.port = 6881
options.max_upload_rate *= 1000
options.max_download_rate *= 1000
if options.max_upload_rate <= 0:
options.max_upload_rate = -1
if options.max_download_rate <= 0:
options.max_download_rate = -1
settings = {
"user_agent": "python_client/" + lt.__version__,
"listen_interfaces": "%s:%d" % (options.listen_interface, options.port),
"download_rate_limit": int(options.max_download_rate),
"upload_rate_limit": int(options.max_upload_rate),
"alert_mask": lt.alert.category_t.all_categories,
"outgoing_interfaces": options.outgoing_interface,
}
if options.proxy_host != "":
settings["proxy_hostname"] = options.proxy_host.split(":")[0]
settings["proxy_type"] = lt.proxy_type_t.http
settings["proxy_port"] = options.proxy_host.split(":")[1]
ses = lt.session(settings)
# map torrent_handle to torrent_status
torrents: Dict[lt.torrent_handle, lt.torrent_status] = {}
alerts_log = []
for filename in args:
add_torrent(ses, filename, options)
console: ConsoleType
if os.name == "nt":
console = WindowsConsole()
else:
console = UnixConsole()
alive = True
while alive:
console.clear()
out = ""
for h, t in torrents.items():
out += "name: %-40s\n" % t.name[:40]
if t.state != lt.torrent_status.seeding:
state_str = [
"queued",
"checking",
"downloading metadata",
"downloading",
"finished",
"seeding",
"",
"checking fastresume",
]
out += state_str[t.state] + " "
out += "%5.4f%% " % (t.progress * 100)
out += progress_bar(t.progress, 49)
out += "\n"
out += "total downloaded: %d Bytes\n" % t.total_done
out += "peers: %d seeds: %d distributed copies: %d\n" % (
t.num_peers,
t.num_seeds,
t.distributed_copies,
)
out += "\n"
out += "download: %s/s (%s) " % (
add_suffix(t.download_rate),
add_suffix(t.total_download),
)
out += "upload: %s/s (%s) " % (
add_suffix(t.upload_rate),
add_suffix(t.total_upload),
)
if t.state != lt.torrent_status.seeding:
out += "info-hash: %s\n" % t.info_hashes
out += "next announce: %s\n" % t.next_announce
out += "tracker: %s\n" % t.current_tracker
console.write(out)
print_peer_info(console, t.handle.get_peer_info())
print_download_queue(console, t.handle.get_download_queue())
if t.state != lt.torrent_status.seeding:
try:
out = "\n"
fp = h.file_progress()
ti = t.torrent_file
if ti is not None:
for idx, p in enumerate(fp):
out += progress_bar(
p / float(ti.files().file_size(idx)), 20
)
out += " " + ti.files().file_path(idx) + "\n"
console.write(out)
except Exception:
pass
console.write(76 * "-" + "\n")
console.write("(q)uit), (p)ause), (u)npause), (r)eannounce\n")
console.write(76 * "-" + "\n")
alerts = ses.pop_alerts()
for a in alerts:
alerts_log.append(a.message())
# add new torrents to our list of torrent_status
if isinstance(a, lt.add_torrent_alert):
h = a.handle
h.set_max_connections(60)
h.set_max_uploads(-1)
torrents[h] = h.status()
# update our torrent_status array for torrents that have
# changed some of their state
if isinstance(a, lt.state_update_alert):
for s in a.status:
torrents[s.handle] = s
if len(alerts_log) > 20:
alerts_log = alerts_log[-20:]
for line in alerts_log:
console.write(line + "\n")
c = console.sleep_and_input(0.5)
ses.post_torrent_updates()
if not c:
continue
if c == "r":
for h in torrents:
h.force_reannounce()
elif c == "q":
alive = False
elif c == "p":
for h in torrents:
h.pause()
elif c == "u":
for h in torrents:
h.resume()
ses.pause()
for h, t in torrents.items():
if not h.is_valid() or not t.has_metadata:
continue
h.save_resume_data()
while len(torrents) > 0:
alerts = ses.pop_alerts()
for a in alerts:
if isinstance(a, lt.save_resume_data_alert):
print(a)
data = lt.write_resume_data_buf(a.params)
h = a.handle
if h in torrents:
open(
os.path.join(
options.save_path, torrents[h].name + ".fastresume"
),
"wb",
).write(data)
del torrents[h]
if isinstance(a, lt.save_resume_data_failed_alert):
h = a.handle
if h in torrents:
print("failed to save resume data for ", torrents[h].name)
del torrents[h]
time.sleep(0.5)
main()