lbry-sdk/lbry/torrent/session.py

255 lines
8.9 KiB
Python
Raw Normal View History

2020-01-24 14:58:01 +01:00
import asyncio
import binascii
2020-02-07 16:32:39 +01:00
import os
import logging
2020-02-26 01:18:01 +01:00
import random
2020-02-07 16:32:39 +01:00
from hashlib import sha1
from tempfile import mkdtemp
2020-02-05 16:29:26 +01:00
from typing import Optional
2020-01-24 14:58:01 +01:00
import libtorrent
log = logging.getLogger(__name__)
2020-02-05 16:29:26 +01:00
DEFAULT_FLAGS = ( # fixme: somehow the logic here is inverted?
2020-02-07 16:32:39 +01:00
libtorrent.add_torrent_params_flags_t.flag_auto_managed
2020-02-05 16:29:26 +01:00
| libtorrent.add_torrent_params_flags_t.flag_update_subscribe
)
2020-01-24 14:58:01 +01:00
class TorrentHandle:
def __init__(self, loop, executor, handle):
self._loop = loop
self._executor = executor
2020-02-07 16:32:39 +01:00
self._handle: libtorrent.torrent_handle = handle
2020-02-28 18:58:59 +01:00
self.started = asyncio.Event(loop=loop)
2020-01-24 14:58:01 +01:00
self.finished = asyncio.Event(loop=loop)
2020-02-07 16:32:39 +01:00
self.metadata_completed = asyncio.Event(loop=loop)
2020-02-11 03:15:18 +01:00
self.size = 0
self.total_wanted_done = 0
self.name = ''
2020-02-26 07:20:26 +01:00
self.tasks = []
2020-02-28 18:58:59 +01:00
self.torrent_file: Optional[libtorrent.file_storage] = None
2020-02-26 16:40:11 +01:00
self._base_path = None
2020-02-28 18:58:59 +01:00
self._handle.set_sequential_download(1)
2020-02-26 16:40:11 +01:00
@property
def largest_file(self) -> Optional[str]:
if not self.torrent_file:
return None
2020-02-28 18:58:59 +01:00
index = self.largest_file_index
return os.path.join(self._base_path, self.torrent_file.at(index).path)
@property
def largest_file_index(self):
largest_size, index = 0, 0
2020-02-26 16:40:11 +01:00
for file_num in range(self.torrent_file.num_files()):
if self.torrent_file.file_size(file_num) > largest_size:
largest_size = self.torrent_file.file_size(file_num)
2020-02-28 18:58:59 +01:00
index = file_num
return index
2020-02-26 07:20:26 +01:00
def stop_tasks(self):
while self.tasks:
self.tasks.pop().cancel()
2020-01-24 14:58:01 +01:00
def _show_status(self):
2020-02-07 16:32:39 +01:00
# fixme: cleanup
2020-02-28 18:58:59 +01:00
if not self._handle.is_valid():
return
2020-01-24 14:58:01 +01:00
status = self._handle.status()
2020-02-07 16:32:39 +01:00
if status.has_metadata:
2020-02-11 03:15:18 +01:00
self.size = status.total_wanted
self.total_wanted_done = status.total_wanted_done
self.name = status.name
if not self.metadata_completed.is_set():
self.metadata_completed.set()
log.info("Metadata completed for btih:%s - %s", status.info_hash, self.name)
2020-02-26 16:40:11 +01:00
self.torrent_file = self._handle.get_torrent_info().files()
self._base_path = status.save_path
2020-02-28 18:58:59 +01:00
first_piece = self.torrent_file.at(self.largest_file_index).offset
if not self.started.is_set():
if self._handle.have_piece(first_piece):
self.started.set()
else:
# prioritize it
self._handle.set_piece_deadline(first_piece, 100)
2020-01-24 14:58:01 +01:00
if not status.is_seeding:
2020-02-24 17:34:13 +01:00
log.debug('%.2f%% complete (down: %.1f kB/s up: %.1f kB/s peers: %d seeds: %d) %s - %s',
status.progress * 100, status.download_rate / 1000, status.upload_rate / 1000,
status.num_peers, status.num_seeds, status.state, status.save_path)
2020-01-24 14:58:01 +01:00
elif not self.finished.is_set():
self.finished.set()
log.info("Torrent finished: %s", self.name)
2020-01-24 14:58:01 +01:00
async def status_loop(self):
while True:
2020-02-11 01:50:16 +01:00
self._show_status()
2020-01-24 14:58:01 +01:00
if self.finished.is_set():
break
2020-02-07 16:32:39 +01:00
await asyncio.sleep(0.1, loop=self._loop)
2020-01-24 14:58:01 +01:00
async def pause(self):
await self._loop.run_in_executor(
self._executor, self._handle.pause
)
async def resume(self):
await self._loop.run_in_executor(
2020-02-24 17:34:13 +01:00
self._executor, lambda: self._handle.resume() # pylint: disable=unnecessary-lambda
2020-01-24 14:58:01 +01:00
)
class TorrentSession:
def __init__(self, loop, executor):
self._loop = loop
self._executor = executor
2020-02-07 16:32:39 +01:00
self._session: Optional[libtorrent.session] = None
2020-01-24 14:58:01 +01:00
self._handles = {}
2020-02-11 01:50:16 +01:00
self.tasks = []
2020-02-28 18:58:59 +01:00
self.wait_start = True
2020-01-24 14:58:01 +01:00
2020-02-07 16:32:39 +01:00
async def add_fake_torrent(self):
2020-02-24 17:34:13 +01:00
tmpdir = mkdtemp()
info, btih = _create_fake_torrent(tmpdir)
2020-02-07 16:32:39 +01:00
flags = libtorrent.add_torrent_params_flags_t.flag_seed_mode
handle = self._session.add_torrent({
2020-02-24 17:34:13 +01:00
'ti': info, 'save_path': tmpdir, 'flags': flags
2020-02-07 16:32:39 +01:00
})
self._handles[btih] = TorrentHandle(self._loop, self._executor, handle)
return btih
async def bind(self, interface: str = '0.0.0.0', port: int = 10889):
2020-01-24 14:58:01 +01:00
settings = {
'listen_interfaces': f"{interface}:{port}",
2022-07-30 02:56:09 +02:00
'enable_natpmp': False,
'enable_upnp': False
2020-01-24 14:58:01 +01:00
}
self._session = await self._loop.run_in_executor(
2020-01-29 02:37:52 +01:00
self._executor, libtorrent.session, settings # pylint: disable=c-extension-no-member
2020-01-24 14:58:01 +01:00
)
2020-02-11 01:50:16 +01:00
self.tasks.append(self._loop.create_task(self.process_alerts()))
def stop(self):
while self.tasks:
self.tasks.pop().cancel()
self._session.save_state()
self._session.pause()
self._session.stop_dht()
self._session.stop_lsd()
self._session.stop_natpmp()
self._session.stop_upnp()
self._session = None
2020-01-24 14:58:01 +01:00
def _pop_alerts(self):
for alert in self._session.pop_alerts():
log.info("torrent alert: %s", alert)
2020-01-24 14:58:01 +01:00
async def process_alerts(self):
while True:
await self._loop.run_in_executor(
self._executor, self._pop_alerts
)
await asyncio.sleep(1, loop=self._loop)
async def pause(self):
2020-01-29 02:37:52 +01:00
await self._loop.run_in_executor(
self._executor, lambda: self._session.save_state() # pylint: disable=unnecessary-lambda
2020-01-24 14:58:01 +01:00
)
await self._loop.run_in_executor(
2020-01-29 02:37:52 +01:00
self._executor, lambda: self._session.pause() # pylint: disable=unnecessary-lambda
2020-01-24 14:58:01 +01:00
)
async def resume(self):
await self._loop.run_in_executor(
self._executor, self._session.resume
)
2020-02-05 16:29:26 +01:00
def _add_torrent(self, btih: str, download_directory: Optional[str]):
params = {'info_hash': binascii.unhexlify(btih.encode()), 'flags': DEFAULT_FLAGS}
2020-02-05 16:29:26 +01:00
if download_directory:
params['save_path'] = download_directory
2020-02-28 18:58:59 +01:00
handle = self._session.add_torrent(params)
handle.force_dht_announce()
self._handles[btih] = TorrentHandle(self._loop, self._executor, handle)
2020-01-24 14:58:01 +01:00
2020-02-26 16:40:11 +01:00
def full_path(self, btih):
return self._handles[btih].largest_file
2020-01-24 14:58:01 +01:00
async def add_torrent(self, btih, download_path):
await self._loop.run_in_executor(
self._executor, self._add_torrent, btih, download_path
)
2020-02-26 07:20:26 +01:00
self._handles[btih].tasks.append(self._loop.create_task(self._handles[btih].status_loop()))
2020-02-07 16:32:39 +01:00
await self._handles[btih].metadata_completed.wait()
2020-02-28 18:58:59 +01:00
if self.wait_start:
# fixme: temporary until we add streaming support, otherwise playback fails!
await self._handles[btih].started.wait()
2020-01-24 14:58:01 +01:00
2020-02-26 01:18:01 +01:00
def remove_torrent(self, btih, remove_files=False):
2020-02-05 16:29:26 +01:00
if btih in self._handles:
handle = self._handles[btih]
2020-02-26 07:20:26 +01:00
handle.stop_tasks()
2020-02-26 01:18:01 +01:00
self._session.remove_torrent(handle._handle, 1 if remove_files else 0)
2020-02-05 16:29:26 +01:00
self._handles.pop(btih)
2020-02-11 01:50:16 +01:00
async def save_file(self, btih, download_directory):
handle = self._handles[btih]
await handle.resume()
2020-02-11 01:50:16 +01:00
2020-02-11 03:15:18 +01:00
def get_size(self, btih):
return self._handles[btih].size
def get_name(self, btih):
return self._handles[btih].name
def get_downloaded(self, btih):
return self._handles[btih].total_wanted_done
2020-02-28 18:58:59 +01:00
def is_completed(self, btih):
return self._handles[btih].finished.is_set()
2020-01-24 14:58:01 +01:00
def get_magnet_uri(btih):
return f"magnet:?xt=urn:btih:{btih}"
2020-02-07 16:32:39 +01:00
2020-02-24 17:34:13 +01:00
def _create_fake_torrent(tmpdir):
# beware, that's just for testing
path = os.path.join(tmpdir, 'tmp')
with open(path, 'wb') as myfile:
2020-02-26 01:18:01 +01:00
size = myfile.write(bytes([random.randint(0, 255) for _ in range(40)]) * 1024)
2020-02-24 17:34:13 +01:00
file_storage = libtorrent.file_storage()
file_storage.add_file('tmp', size)
t = libtorrent.create_torrent(file_storage, 0, 4 * 1024 * 1024)
libtorrent.set_piece_hashes(t, tmpdir)
info = libtorrent.torrent_info(t.generate())
btih = sha1(info.metadata()).hexdigest()
return info, btih
2020-02-07 16:32:39 +01:00
async def main():
if os.path.exists("~/Downloads/ubuntu-18.04.3-live-server-amd64.torrent"):
os.remove("~/Downloads/ubuntu-18.04.3-live-server-amd64.torrent")
if os.path.exists("~/Downloads/ubuntu-18.04.3-live-server-amd64.iso"):
os.remove("~/Downloads/ubuntu-18.04.3-live-server-amd64.iso")
btih = "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c"
executor = None
session = TorrentSession(asyncio.get_event_loop(), executor)
session2 = TorrentSession(asyncio.get_event_loop(), executor)
await session.bind('localhost', port=4040)
await session2.bind('localhost', port=4041)
btih = await session.add_fake_torrent()
session2._session.add_dht_node(('localhost', 4040))
await session2.add_torrent(btih, "/tmp/down")
while True:
await asyncio.sleep(100)
await session.pause()
executor.shutdown()
if __name__ == "__main__":
asyncio.run(main())