-
Notifications
You must be signed in to change notification settings - Fork 0
/
torrent.py
101 lines (86 loc) · 3.46 KB
/
torrent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from bencoding import decode, encode
from data import Piece
from hashlib import sha1
from collections import namedtuple
from datetime import datetime
from typing import List
class Torrent:
def __init__(self, filepath: str):
self.filepath = filepath
with open(filepath, 'rb') as file:
data = decode(file.read())
self.meta_info = MetaInfo(data)
self.info_hash = sha1(encode(data['info'])).digest()
self.uploaded = 0
self.downloaded = 0
self.tracker_id = None
self.complete = False
self.running = False
@property
def left(self):
if not self.meta_info.multi_file:
return self.meta_info.length - self.downloaded
else:
total = sum(file.length for file in self.meta_info.files)
return total - self.downloaded
def __str__(self):
attributes = self.__dict__.items()
attributes = ', '.join(f'{key}={value}' for key, value in attributes)
return f'{self.__class__.__name__}({attributes})'
def __repr__(self):
return self.__str__()
@property
def bitfield(self) -> bytes:
pieces = self.meta_info.pieces
length = len(pieces) + (len(pieces) % 8)
bitfield = bytearray(length // 8)
current = 0
for i in range(len(bitfield)):
byte = 0
for j in range(8):
bit = pieces[current].completed if current < len(pieces) else 0
byte += bit
current += 1
byte <<= 1
byte >>= 1
bitfield[i] = byte
return bytes(bitfield)
async def start(self, peers: List[Peer]):
self.running = True
class MetaInfo:
File = namedtuple('File', ('filepath', 'length', 'md5sum'))
def __init__(self, data: dict):
self.announce = str(data['announce'], encoding='utf-8')
self.announce_list = data.get('announce-list')
self.creation_date: int = data.get('creation date')
if self.creation_date:
self.creation_date = datetime.fromtimestamp(self.creation_date)
self.comment = data.get('comment')
self.created_by = data.get('created by')
self.encoding = data.get('encoding')
self.piece_length = data['info']['piece length']
self.pieces = list()
pieces = data['info']['pieces']
for i in range(0, len(pieces), 20):
piece_hash = pieces[i:i + 20]
self.pieces.append(Piece(i, piece_hash, self.piece_length, 2**14))
self.private = bool(data['info'].get('private'))
if data['info'].get('length'):
self.multi_file = False
self.name = str(data['info']['name'], encoding='utf-8')
self.length = data['info']['length']
self.md5sum = data['info'].get('md5sum')
else:
self.multi_file = True
self.directory = str(data['info']['name'], encoding='utf-8')
self.files = list()
for file in data['info']['files']:
filepath = str(b'/'.join(file['path']), encoding='utf-8')
self.files.append(self.File(filepath, file['length'],
file.get('md5sum')))
def __str__(self) -> str:
attributes = self.__dict__.items()
attributes = ', '.join(f'{key}={value}' for key, value in attributes)
return f'{self.__class__.__name__}({attributes})'
def __repr__(self) -> str:
return self.__str__()