rimossi cloudscraper, simplejson e torrentool, aggiornato sambatools
This commit is contained in:
@@ -1 +0,0 @@
|
||||
VERSION = (1, 0, 2)
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Exposes commonly used classes and functions.
|
||||
|
||||
"""
|
||||
from .bencode import Bencode
|
||||
from .torrent import Torrent
|
||||
from .utils import upload_to_cache_server, get_open_trackers_from_local, get_open_trackers_from_remote
|
||||
@@ -1,204 +0,0 @@
|
||||
from collections import OrderedDict
|
||||
from operator import itemgetter
|
||||
from codecs import encode
|
||||
from sys import version_info
|
||||
|
||||
from .exceptions import BencodeDecodingError, BencodeEncodingError
|
||||
|
||||
|
||||
PY3 = version_info >= (3, 0)
|
||||
|
||||
if PY3:
|
||||
str_type = str
|
||||
byte_types = (bytes, bytearray)
|
||||
chr_ = chr
|
||||
int_types = int
|
||||
else:
|
||||
str_type = basestring
|
||||
byte_types = bytes
|
||||
chr_ = lambda ch: ch
|
||||
int_types = (int, long)
|
||||
|
||||
|
||||
class Bencode(object):
|
||||
"""Exposes utilities for bencoding."""
|
||||
|
||||
@classmethod
|
||||
def encode(cls, value):
|
||||
"""Encodes a value into bencoded bytes.
|
||||
|
||||
:param value: Python object to be encoded (str, int, list, dict).
|
||||
:param str val_encoding: Encoding used by strings in a given object.
|
||||
:rtype: bytes
|
||||
"""
|
||||
val_encoding = 'utf-8'
|
||||
|
||||
def encode_str(v):
|
||||
try:
|
||||
v_enc = encode(v, val_encoding)
|
||||
|
||||
except UnicodeDecodeError:
|
||||
if PY3:
|
||||
raise
|
||||
else:
|
||||
# Suppose bytestring
|
||||
v_enc = v
|
||||
|
||||
prefix = encode('%s:' % len(v_enc), val_encoding)
|
||||
return prefix + v_enc
|
||||
|
||||
def encode_(val):
|
||||
if isinstance(val, str_type):
|
||||
result = encode_str(val)
|
||||
|
||||
elif isinstance(val, int_types):
|
||||
result = encode(('i%se' % val), val_encoding)
|
||||
|
||||
elif isinstance(val, (list, set, tuple)):
|
||||
result = encode('l', val_encoding)
|
||||
for item in val:
|
||||
result += encode_(item)
|
||||
result += encode('e', val_encoding)
|
||||
|
||||
elif isinstance(val, dict):
|
||||
result = encode('d', val_encoding)
|
||||
|
||||
# Dictionaries are expected to be sorted by key.
|
||||
for k, v in OrderedDict(sorted(val.items(), key=itemgetter(0))).items():
|
||||
result += (encode_str(k) + encode_(v))
|
||||
|
||||
result += encode('e', val_encoding)
|
||||
|
||||
elif isinstance(val, byte_types):
|
||||
result = encode('%s:' % len(val), val_encoding)
|
||||
result += val
|
||||
|
||||
else:
|
||||
raise BencodeEncodingError('Unable to encode `%s` %s' % (type(val), val))
|
||||
|
||||
return result
|
||||
|
||||
return encode_(value)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, encoded):
|
||||
"""Decodes bencoded data introduced as bytes.
|
||||
|
||||
Returns decoded structure(s).
|
||||
|
||||
:param bytes encoded:
|
||||
"""
|
||||
def create_dict(items):
|
||||
# Let's guarantee that dictionaries are sorted.
|
||||
k_v_pair = zip(*[iter(items)] * 2)
|
||||
return OrderedDict(sorted(k_v_pair, key=itemgetter(0)))
|
||||
|
||||
def create_list(items):
|
||||
return list(items)
|
||||
|
||||
stack_items = []
|
||||
stack_containers = []
|
||||
|
||||
def compress_stack():
|
||||
target_container = stack_containers.pop()
|
||||
subitems = []
|
||||
|
||||
while True:
|
||||
subitem = stack_items.pop()
|
||||
subitems.append(subitem)
|
||||
if subitem is target_container:
|
||||
break
|
||||
|
||||
container_creator = subitems.pop()
|
||||
container = container_creator(reversed(subitems))
|
||||
stack_items.append(container)
|
||||
|
||||
def parse_forward(till_char, sequence):
|
||||
number = ''
|
||||
char_sub_idx = 0
|
||||
|
||||
for char_sub_idx, char_sub in enumerate(sequence):
|
||||
char_sub = chr_(char_sub)
|
||||
if char_sub == till_char:
|
||||
break
|
||||
|
||||
number += char_sub
|
||||
|
||||
number = int(number or 0)
|
||||
char_sub_idx += 1
|
||||
|
||||
return number, char_sub_idx
|
||||
|
||||
while encoded:
|
||||
char = encoded[0]
|
||||
char = chr_(char)
|
||||
|
||||
if char == 'd': # Dictionary
|
||||
stack_items.append(create_dict)
|
||||
stack_containers.append(create_dict)
|
||||
encoded = encoded[1:]
|
||||
|
||||
elif char == 'l': # List
|
||||
stack_items.append(create_list)
|
||||
stack_containers.append(create_list)
|
||||
encoded = encoded[1:]
|
||||
|
||||
elif char == 'i': # Integer
|
||||
number, char_sub_idx = parse_forward('e', encoded[1:])
|
||||
char_sub_idx += 1
|
||||
|
||||
stack_items.append(number)
|
||||
encoded = encoded[char_sub_idx:]
|
||||
|
||||
elif char.isdigit(): # String
|
||||
str_len, char_sub_idx = parse_forward(':', encoded)
|
||||
last_char_idx = char_sub_idx + str_len
|
||||
|
||||
string = encoded[char_sub_idx:last_char_idx]
|
||||
try:
|
||||
string = string.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
# Considered bytestring (e.g. `pieces` hashes concatenation).
|
||||
pass
|
||||
|
||||
stack_items.append(string)
|
||||
encoded = encoded[last_char_idx:]
|
||||
|
||||
elif char == 'e': # End of a dictionary or a list.
|
||||
compress_stack()
|
||||
encoded = encoded[1:]
|
||||
|
||||
else:
|
||||
raise BencodeDecodingError('Unable to interpret `%s` char.' % char)
|
||||
|
||||
if len(stack_items) == 1:
|
||||
stack_items = stack_items.pop()
|
||||
|
||||
return stack_items
|
||||
|
||||
@classmethod
|
||||
def read_string(cls, string):
|
||||
"""Decodes a given bencoded string or bytestring.
|
||||
|
||||
Returns decoded structure(s).
|
||||
|
||||
:param str string:
|
||||
:rtype: list
|
||||
"""
|
||||
if PY3 and not isinstance(string, byte_types):
|
||||
string = string.encode()
|
||||
|
||||
return cls.decode(string)
|
||||
|
||||
@classmethod
|
||||
def read_file(cls, filepath):
|
||||
"""Decodes bencoded data of a given file.
|
||||
|
||||
Returns decoded structure(s).
|
||||
|
||||
:param str filepath:
|
||||
:rtype: list
|
||||
"""
|
||||
with open(filepath, mode='rb') as f:
|
||||
contents = f.read()
|
||||
return cls.decode(contents)
|
||||
@@ -1,94 +0,0 @@
|
||||
from __future__ import division
|
||||
import click
|
||||
from os import path, getcwd
|
||||
|
||||
from . import VERSION
|
||||
from .api import Torrent
|
||||
from .utils import humanize_filesize, upload_to_cache_server, get_open_trackers_from_remote, \
|
||||
get_open_trackers_from_local
|
||||
from .exceptions import RemoteUploadError, RemoteDownloadError
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version='.'.join(map(str, VERSION)))
|
||||
def start():
|
||||
"""Torrentool command line utilities."""
|
||||
|
||||
|
||||
@start.group()
|
||||
def torrent():
|
||||
"""Torrent-related commands."""
|
||||
|
||||
|
||||
@torrent.command()
|
||||
@click.argument('torrent_path', type=click.Path(exists=True, writable=False, dir_okay=False))
|
||||
def info(torrent_path):
|
||||
"""Print out information from .torrent file."""
|
||||
|
||||
my_torrent = Torrent.from_file(torrent_path)
|
||||
|
||||
size = my_torrent.total_size
|
||||
|
||||
click.secho('Name: %s' % my_torrent.name, fg='blue')
|
||||
click.secho('Files:')
|
||||
for file_tuple in my_torrent.files:
|
||||
click.secho(file_tuple.name)
|
||||
|
||||
click.secho('Hash: %s' % my_torrent.info_hash, fg='blue')
|
||||
click.secho('Size: %s (%s)' % (humanize_filesize(size), size), fg='blue')
|
||||
click.secho('Magnet: %s' % my_torrent.get_magnet(), fg='yellow')
|
||||
|
||||
|
||||
@torrent.command()
|
||||
@click.argument('source', type=click.Path(exists=True, writable=False))
|
||||
@click.option('--dest', default=getcwd, type=click.Path(file_okay=False), help='Destination path to put .torrent file into. Default: current directory.')
|
||||
@click.option('--tracker', default=None, help='Tracker announce URL (multiple comma-separated values supported).')
|
||||
@click.option('--open_trackers', default=False, is_flag=True, help='Add open trackers announce URLs.')
|
||||
@click.option('--comment', default=None, help='Arbitrary comment.')
|
||||
@click.option('--cache', default=False, is_flag=True, help='Upload file to torrent cache services.')
|
||||
def create(source, dest, tracker, open_trackers, comment, cache):
|
||||
"""Create torrent file from a single file or a directory."""
|
||||
|
||||
source_title = path.basename(source).replace('.', '_').replace(' ', '_')
|
||||
dest = '%s.torrent' % path.join(dest, source_title)
|
||||
|
||||
click.secho('Creating torrent from %s ...' % source)
|
||||
|
||||
my_torrent = Torrent.create_from(source)
|
||||
|
||||
if comment:
|
||||
my_torrent.comment = comment
|
||||
|
||||
urls = []
|
||||
|
||||
if tracker:
|
||||
urls = tracker.split(',')
|
||||
|
||||
if open_trackers:
|
||||
click.secho('Fetching an up-to-date open tracker list ...')
|
||||
try:
|
||||
urls.extend(get_open_trackers_from_remote())
|
||||
except RemoteDownloadError:
|
||||
click.secho('Failed. Using built-in open tracker list.', fg='red', err=True)
|
||||
urls.extend(get_open_trackers_from_local())
|
||||
|
||||
if urls:
|
||||
my_torrent.announce_urls = urls
|
||||
|
||||
my_torrent.to_file(dest)
|
||||
|
||||
click.secho('Torrent file created: %s' % dest, fg='green')
|
||||
click.secho('Torrent info hash: %s' % my_torrent.info_hash, fg='blue')
|
||||
|
||||
if cache:
|
||||
click.secho('Uploading to %s torrent cache service ...')
|
||||
try:
|
||||
result = upload_to_cache_server(dest)
|
||||
click.secho('Cached torrent URL: %s' % result, fg='yellow')
|
||||
|
||||
except RemoteUploadError as e:
|
||||
click.secho('Failed: %s' % e, fg='red', err=True)
|
||||
|
||||
|
||||
def main():
|
||||
start(obj={})
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
class TorrentoolException(Exception):
|
||||
"""Base torrentool exception. All others are inherited from it."""
|
||||
|
||||
|
||||
class BencodeError(TorrentoolException):
|
||||
"""Base exception for bencode related errors."""
|
||||
|
||||
|
||||
class BencodeDecodingError(BencodeError):
|
||||
"""Raised when torrentool is unable to decode bencoded data."""
|
||||
|
||||
|
||||
class BencodeEncodingError(BencodeError):
|
||||
"""Raised when torrentool is unable to encode data into bencode."""
|
||||
|
||||
|
||||
class TorrentError(TorrentoolException):
|
||||
"""Base exception for Torrent object related errors."""
|
||||
|
||||
|
||||
class RemoteUploadError(TorrentoolException):
|
||||
"""Base class for upload to remotes related issues."""
|
||||
|
||||
|
||||
class RemoteDownloadError(TorrentoolException):
|
||||
"""Base class for issues related to downloads from remotes."""
|
||||
@@ -1,8 +0,0 @@
|
||||
udp://tracker.coppersurfer.tk:6969/announce
|
||||
udp://tracker.internetwarriors.net:1337/announce
|
||||
udp://tracker.leechers-paradise.org:6969/announce
|
||||
udp://tracker.opentrackr.org:1337/announce
|
||||
udp://tracker.openbittorrent.com:80/announce
|
||||
udp://tracker.sktorrent.net:6969/announce
|
||||
udp://tracker.zer0day.to:1337/announce
|
||||
udp://exodus.desync.com:6969/announce
|
||||
@@ -1,436 +0,0 @@
|
||||
from calendar import timegm
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from functools import reduce
|
||||
from hashlib import sha1
|
||||
from os import walk, sep
|
||||
from os.path import join, isdir, getsize, normpath, basename
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except ImportError: # Py2
|
||||
from urllib import urlencode
|
||||
|
||||
from .bencode import Bencode
|
||||
from .exceptions import TorrentError
|
||||
from .utils import get_app_version
|
||||
|
||||
|
||||
_ITERABLE_TYPES = (list, tuple, set)
|
||||
|
||||
|
||||
TorrentFile = namedtuple('TorrentFile', ['name', 'length'])
|
||||
|
||||
|
||||
class Torrent(object):
|
||||
"""Represents a torrent file, and exposes utilities to work with it."""
|
||||
|
||||
_filepath = None
|
||||
|
||||
def __init__(self, dict_struct=None):
|
||||
dict_struct = dict_struct or {'info': {}}
|
||||
self._struct = dict_struct
|
||||
|
||||
def __str__(self):
|
||||
return 'Torrent: %s' % self.name
|
||||
|
||||
announce_urls = property()
|
||||
"""List of lists of tracker announce URLs."""
|
||||
|
||||
comment = property()
|
||||
"""Optional. Free-form textual comments of the author."""
|
||||
|
||||
creation_date = property()
|
||||
"""Optional. The creation time of the torrent, in standard UNIX epoch format. UTC."""
|
||||
|
||||
created_by = property()
|
||||
"""Optional. Name and version of the program used to create the .torrent"""
|
||||
|
||||
private = property()
|
||||
"""Optional. If True the client MUST publish its presence to get other peers
|
||||
ONLY via the trackers explicitly described in the metainfo file. If False or is not present,
|
||||
the client may obtain peer from other means, e.g. PEX peer exchange, dht.
|
||||
|
||||
"""
|
||||
|
||||
name = property()
|
||||
"""Torrent name (title)."""
|
||||
|
||||
webseeds = property()
|
||||
"""A list of URLs where torrent data can be retrieved.
|
||||
|
||||
See also: Torrent.httpseeds
|
||||
|
||||
http://bittorrent.org/beps/bep_0019.html
|
||||
"""
|
||||
|
||||
httpseeds = property()
|
||||
"""A list of URLs where torrent data can be retrieved.
|
||||
|
||||
See also and prefer Torrent.webseeds
|
||||
|
||||
http://bittorrent.org/beps/bep_0017.html
|
||||
"""
|
||||
|
||||
def _list_getter(self, key):
|
||||
return self._struct.get(key, [])
|
||||
|
||||
def _list_setter(self, key, val):
|
||||
if val is None:
|
||||
try:
|
||||
del self._struct[key]
|
||||
return
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
if not isinstance(val, _ITERABLE_TYPES):
|
||||
val = [val]
|
||||
|
||||
self._struct[key] = val
|
||||
|
||||
@webseeds.getter
|
||||
def webseeds(self):
|
||||
return self._list_getter('url-list')
|
||||
|
||||
@webseeds.setter
|
||||
def webseeds(self, val):
|
||||
self._list_setter('url-list', val)
|
||||
|
||||
@httpseeds.getter
|
||||
def httpseeds(self):
|
||||
return self._list_getter('httpseeds')
|
||||
|
||||
@httpseeds.setter
|
||||
def httpseeds(self, val):
|
||||
self._list_setter('httpseeds', val)
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
"""Files in torrent.
|
||||
|
||||
List of namedtuples (filepath, size).
|
||||
|
||||
:rtype: list[TorrentFile]
|
||||
"""
|
||||
files = []
|
||||
info = self._struct.get('info')
|
||||
|
||||
if not info:
|
||||
return files
|
||||
|
||||
if 'files' in info:
|
||||
base = info['name']
|
||||
|
||||
for f in info['files']:
|
||||
files.append(TorrentFile(join(base, *f['path']), f['length']))
|
||||
|
||||
else:
|
||||
files.append(TorrentFile(info['name'], info['length']))
|
||||
|
||||
return files
|
||||
|
||||
@property
|
||||
def total_size(self):
|
||||
"""Total size of all files in torrent."""
|
||||
return reduce(lambda prev, curr: prev + curr[1], self.files, 0)
|
||||
|
||||
@property
|
||||
def info_hash(self):
|
||||
"""Hash of torrent file info section. Also known as torrent hash."""
|
||||
info = self._struct.get('info')
|
||||
|
||||
if not info:
|
||||
return None
|
||||
|
||||
return sha1(Bencode.encode(info)).hexdigest()
|
||||
|
||||
@property
|
||||
def magnet_link(self):
|
||||
"""Magnet link using BTIH (BitTorrent Info Hash) URN."""
|
||||
return self.get_magnet(detailed=False)
|
||||
|
||||
@announce_urls.getter
|
||||
def announce_urls(self):
|
||||
"""List of lists of announce (tracker) URLs.
|
||||
|
||||
First inner list is considered as primary announcers list,
|
||||
the following lists as back-ups.
|
||||
|
||||
http://bittorrent.org/beps/bep_0012.html
|
||||
|
||||
"""
|
||||
urls = self._struct.get('announce-list')
|
||||
|
||||
if not urls:
|
||||
urls = self._struct.get('announce')
|
||||
if not urls:
|
||||
return []
|
||||
urls = [[urls]]
|
||||
|
||||
return urls
|
||||
|
||||
@announce_urls.setter
|
||||
def announce_urls(self, val):
|
||||
self._struct['announce'] = ''
|
||||
self._struct['announce-list'] = []
|
||||
|
||||
def set_single(val):
|
||||
del self._struct['announce-list']
|
||||
self._struct['announce'] = val
|
||||
|
||||
if isinstance(val, _ITERABLE_TYPES):
|
||||
length = len(val)
|
||||
|
||||
if length:
|
||||
if length == 1:
|
||||
set_single(val[0])
|
||||
else:
|
||||
for item in val:
|
||||
if not isinstance(item, _ITERABLE_TYPES):
|
||||
item = [item]
|
||||
self._struct['announce-list'].append(item)
|
||||
self._struct['announce'] = val[0]
|
||||
|
||||
else:
|
||||
set_single(val)
|
||||
|
||||
@comment.getter
|
||||
def comment(self):
|
||||
return self._struct.get('comment')
|
||||
|
||||
@comment.setter
|
||||
def comment(self, val):
|
||||
self._struct['comment'] = val
|
||||
|
||||
@creation_date.getter
|
||||
def creation_date(self):
|
||||
date = self._struct.get('creation date')
|
||||
if date is not None:
|
||||
date = datetime.utcfromtimestamp(int(date))
|
||||
return date
|
||||
|
||||
@creation_date.setter
|
||||
def creation_date(self, val):
|
||||
self._struct['creation date'] = timegm(val.timetuple())
|
||||
|
||||
@created_by.getter
|
||||
def created_by(self):
|
||||
return self._struct.get('created by')
|
||||
|
||||
@created_by.setter
|
||||
def created_by(self, val):
|
||||
self._struct['created by'] = val
|
||||
|
||||
@private.getter
|
||||
def private(self):
|
||||
return self._struct.get('info', {}).get('private', False)
|
||||
|
||||
@private.setter
|
||||
def private(self, val):
|
||||
if not val:
|
||||
try:
|
||||
del self._struct['info']['private']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self._struct['info']['private'] = 1
|
||||
|
||||
@name.getter
|
||||
def name(self):
|
||||
return self._struct.get('info', {}).get('name', None)
|
||||
|
||||
@name.setter
|
||||
def name(self, val):
|
||||
self._struct['info']['name'] = val
|
||||
|
||||
def get_magnet(self, detailed=True):
|
||||
"""Returns torrent magnet link, consisting of BTIH (BitTorrent Info Hash) URN
|
||||
anr optional other information.
|
||||
|
||||
:param bool|list|tuple|set detailed:
|
||||
For boolean - whether additional info (such as trackers) should be included.
|
||||
For iterable - expected allowed parameter names:
|
||||
tr - trackers
|
||||
ws - webseeds
|
||||
|
||||
"""
|
||||
result = 'magnet:?xt=urn:btih:' + self.info_hash
|
||||
|
||||
def add_tr():
|
||||
urls = self.announce_urls
|
||||
if not urls:
|
||||
return
|
||||
|
||||
trackers = []
|
||||
|
||||
urls = urls[0] # Only primary announcers are enough.
|
||||
for url in urls:
|
||||
trackers.append(('tr', url))
|
||||
|
||||
if trackers:
|
||||
return urlencode(trackers)
|
||||
|
||||
def add_ws():
|
||||
webseeds = [('ws', url) for url in self.webseeds]
|
||||
if webseeds:
|
||||
return urlencode(webseeds)
|
||||
|
||||
params_map = {
|
||||
'tr': add_tr,
|
||||
'ws': add_ws,
|
||||
}
|
||||
|
||||
if detailed:
|
||||
details = []
|
||||
|
||||
if isinstance(detailed, _ITERABLE_TYPES):
|
||||
requested_params = detailed
|
||||
else:
|
||||
requested_params = params_map.keys()
|
||||
|
||||
for param in requested_params:
|
||||
param_val = params_map[param]()
|
||||
param_val and details.append(param_val)
|
||||
|
||||
if details:
|
||||
result += '&%s' % '&'.join(details)
|
||||
|
||||
return result
|
||||
|
||||
def to_file(self, filepath=None):
|
||||
"""Writes Torrent object into file, either
|
||||
|
||||
:param filepath:
|
||||
"""
|
||||
if filepath is None and self._filepath is None:
|
||||
raise TorrentError('Unable to save torrent to file: no filepath supplied.')
|
||||
|
||||
if filepath is not None:
|
||||
self._filepath = filepath
|
||||
|
||||
with open(self._filepath, mode='wb') as f:
|
||||
f.write(self.to_string())
|
||||
|
||||
def to_string(self):
|
||||
"""Returns bytes representing torrent file.
|
||||
|
||||
:param str encoding: Encoding used by strings in Torrent object.
|
||||
:rtype: bytearray
|
||||
"""
|
||||
return Bencode.encode(self._struct)
|
||||
|
||||
@classmethod
|
||||
def _get_target_files_info(cls, src_path):
|
||||
src_path = u'%s' % src_path # Force walk() to return unicode names.
|
||||
|
||||
is_dir = isdir(src_path)
|
||||
target_files = []
|
||||
|
||||
if is_dir:
|
||||
for base, _, files in walk(src_path):
|
||||
target_files.extend([join(base, fname) for fname in sorted(files)])
|
||||
|
||||
else:
|
||||
target_files.append(src_path)
|
||||
|
||||
target_files_ = []
|
||||
total_size = 0
|
||||
for fpath in target_files:
|
||||
file_size = getsize(fpath)
|
||||
if not file_size:
|
||||
continue
|
||||
target_files_.append((fpath, file_size, normpath(fpath.replace(src_path, '')).strip(sep).split(sep)))
|
||||
total_size += file_size
|
||||
|
||||
return target_files_, total_size
|
||||
|
||||
@classmethod
|
||||
def create_from(cls, src_path):
|
||||
"""Returns Torrent object created from a file or a directory.
|
||||
|
||||
:param str src_path:
|
||||
:rtype: Torrent
|
||||
"""
|
||||
is_dir = isdir(src_path)
|
||||
target_files, size_data = cls._get_target_files_info(src_path)
|
||||
|
||||
SIZE_MIN = 32768 # 32 KiB
|
||||
SIZE_DEFAULT = 262144 # 256 KiB
|
||||
SIZE_MAX = 1048576 # 1 MiB
|
||||
|
||||
CHUNKS_MIN = 1000 # todo use those limits as advised
|
||||
CHUNKS_MAX = 2200
|
||||
|
||||
size_piece = SIZE_MIN
|
||||
if size_data > SIZE_MIN:
|
||||
size_piece = SIZE_DEFAULT
|
||||
|
||||
if size_piece > SIZE_MAX:
|
||||
size_piece = SIZE_MAX
|
||||
|
||||
def read(filepath):
|
||||
with open(filepath, 'rb') as f:
|
||||
while True:
|
||||
chunk = f.read(size_piece - len(pieces_buffer))
|
||||
chunk_size = len(chunk)
|
||||
if chunk_size == 0:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
pieces = bytearray()
|
||||
pieces_buffer = bytearray()
|
||||
|
||||
for fpath, _, _ in target_files:
|
||||
for chunk in read(fpath):
|
||||
pieces_buffer += chunk
|
||||
|
||||
if len(pieces_buffer) == size_piece:
|
||||
pieces += sha1(pieces_buffer).digest()[:20]
|
||||
pieces_buffer = bytearray()
|
||||
|
||||
if len(pieces_buffer):
|
||||
pieces += sha1(pieces_buffer).digest()[:20]
|
||||
pieces_buffer = bytearray()
|
||||
|
||||
info = {
|
||||
'name': basename(src_path),
|
||||
'pieces': bytes(pieces),
|
||||
'piece length': size_piece,
|
||||
}
|
||||
|
||||
if is_dir:
|
||||
files = []
|
||||
|
||||
for _, length, path in target_files:
|
||||
files.append({'length': length, 'path': path})
|
||||
|
||||
info['files'] = files
|
||||
|
||||
else:
|
||||
info['length'] = target_files[0][1]
|
||||
|
||||
torrent = cls({'info': info})
|
||||
torrent.created_by = get_app_version()
|
||||
torrent.creation_date = datetime.utcnow()
|
||||
|
||||
return torrent
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
"""Alternative constructor to get Torrent object from string.
|
||||
|
||||
:param str string:
|
||||
:rtype: Torrent
|
||||
"""
|
||||
return cls(Bencode.read_string(string))
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filepath):
|
||||
"""Alternative constructor to get Torrent object from file.
|
||||
|
||||
:param str filepath:
|
||||
:rtype: Torrent
|
||||
"""
|
||||
torrent = cls(Bencode.read_file(filepath))
|
||||
torrent._filepath = filepath
|
||||
return torrent
|
||||
@@ -1,91 +0,0 @@
|
||||
import math
|
||||
from os import path
|
||||
|
||||
from .exceptions import RemoteUploadError, RemoteDownloadError
|
||||
|
||||
|
||||
OPEN_TRACKERS_FILENAME = 'open_trackers.ini'
|
||||
REMOTE_TIMEOUT = 4
|
||||
|
||||
|
||||
def get_app_version():
|
||||
"""Returns full version string including application name
|
||||
suitable for putting into Torrent.created_by.
|
||||
|
||||
"""
|
||||
from torrentool import VERSION
|
||||
return 'torrentool/%s' % '.'.join(map(str, VERSION))
|
||||
|
||||
|
||||
def humanize_filesize(bytes_size):
|
||||
"""Returns human readable filesize.
|
||||
|
||||
:param int bytes_size:
|
||||
:rtype: str
|
||||
"""
|
||||
if not bytes_size:
|
||||
return '0 B'
|
||||
|
||||
names = ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
|
||||
|
||||
name_idx = int(math.floor(math.log(bytes_size, 1024)))
|
||||
size = round(bytes_size / math.pow(1024, name_idx), 2)
|
||||
|
||||
return '%s %s' % (size, names[name_idx])
|
||||
|
||||
|
||||
def upload_to_cache_server(fpath):
|
||||
"""Uploads .torrent file to a cache server.
|
||||
|
||||
Returns upload file URL.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
url_base = 'http://torrage.info'
|
||||
url_upload = '%s/autoupload.php' % url_base
|
||||
url_download = '%s/torrent.php?h=' % url_base
|
||||
file_field = 'torrent'
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.post(url_upload, files={file_field: open(fpath, 'rb')}, timeout=REMOTE_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
info_cache = response.text
|
||||
return url_download + info_cache
|
||||
|
||||
except (ImportError, requests.RequestException) as e:
|
||||
|
||||
# Now trace is lost. `raise from` to consider.
|
||||
raise RemoteUploadError('Unable to upload to %s: %s' % (url_upload, e))
|
||||
|
||||
|
||||
def get_open_trackers_from_remote():
|
||||
"""Returns open trackers announce URLs list from remote repo."""
|
||||
|
||||
url_base = 'https://raw.githubusercontent.com/idlesign/torrentool/master/torrentool/repo'
|
||||
url = '%s/%s' % (url_base, OPEN_TRACKERS_FILENAME)
|
||||
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.get(url, timeout=REMOTE_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
|
||||
open_trackers = response.text.splitlines()
|
||||
|
||||
except (ImportError, requests.RequestException) as e:
|
||||
|
||||
# Now trace is lost. `raise from` to consider.
|
||||
raise RemoteDownloadError('Unable to download from %s: %s' % (url, e))
|
||||
|
||||
return open_trackers
|
||||
|
||||
|
||||
def get_open_trackers_from_local():
|
||||
"""Returns open trackers announce URLs list from local backup."""
|
||||
with open(path.join(path.dirname(__file__), 'repo', OPEN_TRACKERS_FILENAME)) as f:
|
||||
open_trackers = map(str.strip, f.readlines())
|
||||
|
||||
return list(open_trackers)
|
||||
Reference in New Issue
Block a user