lbry-sdk/lbry/torrent/session.py

302 lines
11 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
from typing import Optional, Tuple
2020-02-05 16:29:26 +01:00
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
2022-09-05 15:50:22 +02:00
| libtorrent.add_torrent_params_flags_t.flag_sequential_download
| libtorrent.add_torrent_params_flags_t.flag_paused
2020-02-05 16:29:26 +01:00
)
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
2022-10-18 23:00:40 +02:00
self.started = asyncio.Event(loop=loop)
self.finished = asyncio.Event(loop=loop)
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 = []
self._torrent_info: libtorrent.torrent_info = handle.torrent_file()
2020-02-26 16:40:11 +01:00
self._base_path = None
@property
def torrent_file(self) -> Optional[libtorrent.file_storage]:
return self._torrent_info.files()
2020-02-26 16:40:11 +01:00
@property
def largest_file(self) -> Optional[str]:
if self.torrent_file is None:
2020-02-26 16:40:11 +01:00
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.file_path(index))
@property
def save_path(self) -> Optional[str]:
return self._base_path
2020-02-28 18:58:59 +01:00
@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 byte_range_to_piece_range(
self, file_index, start_offset, end_offset) -> Tuple[libtorrent.peer_request, libtorrent.peer_request]:
start_piece = self._torrent_info.map_file(file_index, start_offset, 0)
end_piece = self._torrent_info.map_file(file_index, end_offset, 0)
return start_piece, end_piece
async def stream_range_as_completed(self, file_index, start, end):
first_piece, final_piece = self.byte_range_to_piece_range(file_index, start, end)
start_piece_offset = final_piece.start
piece_size = self._torrent_info.piece_length()
log.info("Streaming torrent from piece %d to %d (bytes: %d -> %d): %s",
first_piece.piece, final_piece.piece, start, end, self.name)
2022-09-07 23:59:06 +02:00
self.prioritize(file_index, start, end)
await self.resume()
for piece_index in range(first_piece.piece, final_piece.piece + 1):
while not self._handle.have_piece(piece_index):
log.info("Waiting for piece %d: %s", piece_index, self.name)
2022-09-07 23:59:06 +02:00
self._handle.set_piece_deadline(piece_index, 0)
await asyncio.sleep(0.2)
log.info("Streaming piece offset %d / %d for torrent %s", piece_index, final_piece.piece, self.name)
yield piece_size - start_piece_offset
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()
self._torrent_info = self._handle.torrent_file()
log.info("Metadata completed for btih:%s - %s", status.info_hash, self.name)
2022-09-07 23:59:06 +02:00
# prioritize first 2mb
self.prioritize(self.largest_file_index, 0, 2 * 1024 * 1024)
2020-02-26 16:40:11 +01:00
self._base_path = status.save_path
first_piece = self.torrent_file.piece_index_at_file(self.largest_file_index)
2020-02-28 18:58:59 +01:00
if not self.started.is_set():
if self._handle.have_piece(first_piece):
2022-09-07 23:59:06 +02:00
log.debug("Got first piece, set started - %s", self.name)
2020-02-28 18:58:59 +01:00
self.started.set()
2022-09-07 23:59:06 +02: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)
if (status.is_finished or status.is_seeding) and not self.finished.is_set():
2020-01-24 14:58:01 +01:00
self.finished.set()
log.info("Torrent finished: %s", self.name)
2020-01-24 14:58:01 +01:00
2022-09-07 23:59:06 +02:00
def prioritize(self, file_index, start, end, cleanup=False):
first_piece, last_piece = self.byte_range_to_piece_range(file_index, start, end)
priorities = self._handle.get_piece_priorities()
priorities = [0 if cleanup else 1 for _ in priorities]
self._handle.clear_piece_deadlines()
for idx, piece_number in enumerate(range(first_piece.piece, last_piece.piece + 1)):
priorities[piece_number] = 7 - idx if 0 <= idx <= 6 else 1
self._handle.set_piece_deadline(piece_number, idx)
log.debug("Prioritizing pieces for %s: %s", self.name, priorities)
self._handle.prioritize_pieces(priorities)
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
await asyncio.sleep(0.1)
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)
2020-01-24 14:58:01 +01:00
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
def save_path(self, btih):
return self._handles[btih].save_path
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()
def stream_largest_file(self, btih, start, end):
handle = self._handles[btih]
return handle.stream_range_as_completed(handle.largest_file_index, start, end)
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)
await session.bind()
await session.add_torrent(btih, os.path.expanduser("~/Downloads"))
2020-02-07 16:32:39 +01:00
while True:
session.full_path(btih)
await asyncio.sleep(1)
2020-02-07 16:32:39 +01:00
await session.pause()
executor.shutdown()
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)-4s %(name)s:%(lineno)d: %(message)s")
log = logging.getLogger(__name__)
2020-02-07 16:32:39 +01:00
asyncio.run(main())