From 74f247223f8e47b5a1a28910e5c8cd8c37a6716a Mon Sep 17 00:00:00 2001 From: Anton Argirov Date: Sun, 11 Jan 2015 11:47:17 +0600 Subject: [PATCH] Initial commit --- .gitignore | 3 + addon.xml | 16 + changelog.txt | 8 + lib/torrent2http/__init__.py | 44 +++ lib/torrent2http/engine.py | 368 ++++++++++++++++++++ lib/torrent2http/error.py | 21 ++ lib/torrent2http/logpipe.py | 33 ++ lib/torrent2http/mimetypes.py | 613 ++++++++++++++++++++++++++++++++++ lib/torrent2http/platform.py | 38 +++ 9 files changed, 1144 insertions(+) create mode 100644 .gitignore create mode 100644 addon.xml create mode 100644 changelog.txt create mode 100644 lib/torrent2http/__init__.py create mode 100644 lib/torrent2http/engine.py create mode 100644 lib/torrent2http/error.py create mode 100644 lib/torrent2http/logpipe.py create mode 100644 lib/torrent2http/mimetypes.py create mode 100644 lib/torrent2http/platform.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59e2119 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +*.py[cod] +bin/ \ No newline at end of file diff --git a/addon.xml b/addon.xml new file mode 100644 index 0000000..4c9b80e --- /dev/null +++ b/addon.xml @@ -0,0 +1,16 @@ + + + + + + + + all + en + Downloads torrents and share it over HTTP + Загружает торренты и раздает их по HTTP + Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent. + Provides sequential torrent downloading for online streaming video and other media over HTTP. + anteo@academ.org + + diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 0000000..8fdcf1c --- /dev/null +++ b/changelog.txt @@ -0,0 +1,8 @@ +[B]Version 0.0.3[/B] ++ Detect media types + +[B]Version 0.0.2[/B] ++ Added some configurable libtorrent options + +[B]Version 0.0.1[/B] ++ Initial release diff --git a/lib/torrent2http/__init__.py b/lib/torrent2http/__init__.py new file mode 100644 index 0000000..cff4053 --- /dev/null +++ b/lib/torrent2http/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from collections import namedtuple + + +# noinspection PyClassHasNoInit +class State: + QUEUED_FOR_CHECKING = 0 + CHECKING_FILES = 1 + DOWNLOADING_METADATA = 2 + DOWNLOADING = 3 + FINISHED = 4 + SEEDING = 5 + ALLOCATING = 6 + CHECKING_RESUME_DATA = 7 + + +# noinspection PyClassHasNoInit +class MediaType: + UNKNOWN = None + AUDIO = 'audio' + VIDEO = 'video' + SUBTITLES = 'subtitles' + + +# noinspection PyClassHasNoInit +class Encryption: + FORCED = 0 + ENABLED = 1 + DISABLED = 2 + + +SessionStatus = namedtuple('SessionStatus', "name, state, state_str, error, progress, download_rate, upload_rate, " + "total_download, total_upload, num_peers, num_seeds, total_seeds, " + "total_peers") + +FileStatus = namedtuple('FileStatus', "name, save_path, url, size, offset, download, progress, index, media_type") + +PeerInfo = namedtuple('PeerInfo', "ip, flags, source, up_speed, down_speed, total_upload, total_download, " + "country, client") + +from engine import Engine +from platform import Platform +from error import Error diff --git a/lib/torrent2http/engine.py b/lib/torrent2http/engine.py new file mode 100644 index 0000000..1f76a6b --- /dev/null +++ b/lib/torrent2http/engine.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +import json +import os +import socket +import stat +import subprocess +import sys +import time +import urllib2 +import logpipe +import mimetypes +import xbmc + +from error import Error +from platform import Platform +from . import SessionStatus, FileStatus, PeerInfo, MediaType, Encryption +from os.path import dirname + + +class Engine: + SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl', + '.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx'] + + def _ensure_binary_executable(self, path): + st = os.stat(path) + if not st.st_mode & stat.S_IEXEC: + self._log("%s is not executable, trying to change its mode..." % path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + st = os.stat(path) + if st.st_mode & stat.S_IEXEC: + self._log("Succeeded") + return True + else: + self._log("Failed") + return False + return True + + def _log(self, message): + if self.logger: + self.logger(message) + else: + xbmc.log("[torrent2http] %s" % message) + + def _get_binary_path(self, binaries_path): + binary = "torrent2http" + (".exe" if self.platform.system == 'windows' else "") + binary_dir = os.path.join(binaries_path, "%s_%s" % (self.platform.system, self.platform.arch)) + binary_path = os.path.join(binary_dir, binary) + if not os.path.isfile(binary_path): + raise Error("Can't find torrent2http binary for %s" % self.platform, + Error.UNKNOWN_PLATFORM, platform=str(self.platform)) + + if not self._ensure_binary_executable(binary_path): + if self.platform.system == "android": + self._log("Trying to copy torrent2http to ext4, since the sdcard is noexec...") + xbmc_home = os.environ.get('XBMC_HOME') or os.environ.get('KODI_HOME') + if not xbmc_home: + raise Error("Suppose we are running XBMC, but environment variable " + "XBMC_HOME or KODI_HOME is not found", Error.XBMC_HOME_NOT_DEFINED) + base_xbmc_path = dirname(dirname(dirname(xbmc_home))) + android_binary_dir = os.path.join(base_xbmc_path, "files") + if not os.path.exists(android_binary_dir): + os.makedirs(android_binary_dir) + android_binary_path = os.path.join(android_binary_dir, binary) + if not os.path.exists(android_binary_path) or \ + int(os.path.getmtime(android_binary_path)) < int(os.path.getmtime(binary_path)): + import shutil + shutil.copy2(binary_path, android_binary_path) + if not self._ensure_binary_executable(android_binary_path): + raise Error("Can't make %s executable" % android_binary_path, Error.NOEXEC_FILESYSTEM) + binary_path = android_binary_path + else: + raise Error("Can't make %s executable, ensure it's placed on exec partition and " + "partition is in read/write mode" % binary_path, Error.NOEXEC_FILESYSTEM) + self._log("Selected %s as torrent2http binary" % binary_path) + return binary_path + + @staticmethod + def _can_bind(host, port): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind((host, port)) + s.close() + except socket.error: + return False + return True + + @staticmethod + def _find_free_port(host): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind((host, 0)) + port = s.getsockname()[1] + s.close() + except socket.error: + return False + return port + + def __init__(self, uri=None, binaries_path=None, platform=None, download_path=".", + bind_host='127.0.0.1', bind_port=5001, connections_limit=None, download_kbps=None, upload_kbps=None, + enable_dht=True, enable_lsd=True, enable_natpmp=True, enable_upnp=True, enable_scrape=False, + log_stats=False, encryption=Encryption.ENABLED, keep_complete=False, keep_incomplete=False, + keep_files=False, log_files_progress=False, log_overall_progress=False, log_pieces_progress=False, + listen_port=6881, use_random_port=False, max_idle_timeout=None, no_sparse=False, resume_file=None, + user_agent=None, startup_timeout=5, state_file=None, enable_utp=True, enable_tcp=True, + debug_alerts=False, logger=None, torrent_connect_boost=50, connection_speed=50, + peer_connect_timeout=15, request_timeout=20, min_reconnect_time=60, max_failcount=3, + dht_routers=None, trackers=None): + self.dht_routers = dht_routers or [] + self.trackers = trackers or [] + self.max_failcount = max_failcount + self.min_reconnect_time = min_reconnect_time + self.request_timeout = request_timeout + self.peer_connect_timeout = peer_connect_timeout + self.connection_speed = connection_speed + self.torrent_connect_boost = torrent_connect_boost + self.platform = platform + self.bind_host = bind_host + self.bind_port = bind_port + self.binaries_path = binaries_path or os.path.join(dirname(dirname(dirname(os.path.abspath(__file__)))), 'bin') + self.download_path = download_path + self.connections_limit = connections_limit + self.download_kbps = download_kbps + self.upload_kbps = upload_kbps + self.enable_dht = enable_dht + self.enable_lsd = enable_lsd + self.enable_natpmp = enable_natpmp + self.enable_upnp = enable_upnp + self.enable_scrape = enable_scrape + self.log_stats = log_stats + self.encryption = encryption + self.keep_complete = keep_complete + self.keep_incomplete = keep_incomplete + self.keep_files = keep_files + self.log_files_progress = log_files_progress + self.log_overall_progress = log_overall_progress + self.log_pieces_progress = log_pieces_progress + self.listen_port = listen_port + self.use_random_port = use_random_port + self.max_idle_timeout = max_idle_timeout + self.no_sparse = no_sparse + self.resume_file = resume_file + self.user_agent = user_agent + self.startup_timeout = startup_timeout + self.state_file = state_file + self.wait_on_close_timeout = None + self.enable_utp = enable_utp + self.enable_tcp = enable_tcp + self.debug_alerts = debug_alerts + self.logger = logger + self.uri = uri + self.logpipe = None + self.process = None + + @staticmethod + def _validate_save_path(path): + if "://" in path: + if sys.platform.startswith('win') and path.lower().startswith("smb://"): + path = path.replace("smb:", "").replace("/", "\\") + else: + raise Error("Downloading to an unmounted network share is not supported", Error.INVALID_DOWNLOAD_PATH) + if not os.path.isdir(path): + raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH) + return path + + def start(self, start_index=None): + self.platform = self.platform or Platform() + binary_path = self._get_binary_path(self.binaries_path) + download_path = self._validate_save_path(self.download_path) + if not self._can_bind(self.bind_host, self.bind_port): + port = self._find_free_port(self.bind_host) + if port is False: + raise Error("Can't find port to bind torrent2http", Error.BIND_ERROR) + self._log("Can't bind to %s:%s, so we found another port: %d" % (self.bind_host, self.bind_port, port)) + self.bind_port = port + + kwargs = { + '--bind': "%s:%s" % (self.bind_host, self.bind_port), + '--uri': self.uri, + '--file-index': start_index, + '--dl-path': download_path, + '--connections-limit': self.connections_limit, + '--dl-rate': self.download_kbps, + '--ul-rate': self.upload_kbps, + '--enable-dht': self.enable_dht, + '--enable-lsd': self.enable_lsd, + '--enable-natpmp': self.enable_natpmp, + '--enable-upnp': self.enable_upnp, + '--enable-scrape': self.enable_scrape, + '--encryption': self.encryption, + '--show-stats': self.log_stats, + '--files-progress': self.log_files_progress, + '--overall-progress': self.log_overall_progress, + '--pieces-progress': self.log_pieces_progress, + '--listen-port': self.listen_port, + '--random-port': self.use_random_port, + '--keep-complete': self.keep_complete, + '--keep-incomplete': self.keep_incomplete, + '--keep-files': self.keep_files, + '--max-idle': self.max_idle_timeout, + '--no-sparse': self.no_sparse, + '--resume-file': self.resume_file, + '--user-agent': self.user_agent, + '--state-file': self.state_file, + '--enable-utp': self.enable_utp, + '--enable-tcp': self.enable_tcp, + '--debug-alerts': self.debug_alerts, + '--torrent-connect-boost': self.torrent_connect_boost, + '--connection-speed': self.connection_speed, + '--peer-connect-timeout': self.peer_connect_timeout, + '--request-timeout': self.request_timeout, + '--min-reconnect-time': self.min_reconnect_time, + '--max-failcount': self.max_failcount, + '--dht-routers': ",".join(self.dht_routers), + '--trackers': ",".join(self.trackers), + } + + args = [binary_path] + for k, v in kwargs.iteritems(): + if v is not None: + if isinstance(v, bool): + if v: + args.append(k) + else: + args.append("%s=false" % k) + else: + args.append(k) + if isinstance(v, str): + v = v.decode('utf-8') + if isinstance(v, unicode): + v = v.encode(sys.getfilesystemencoding() or 'utf-8') + else: + v = str(v) + args.append(v) + + self._log("Invoking %s" % " ".join(args)) + startupinfo = None + if self.platform.system == "windows": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= 1 + startupinfo.wShowWindow = 0 + + self.logpipe = logpipe.LogPipe(self._log) + try: + self.process = subprocess.Popen(args, stderr=self.logpipe, stdout=self.logpipe, startupinfo=startupinfo) + except OSError, e: + raise Error("Can't start torrent2http: %r" % e, Error.POPEN_ERROR) + + start = time.time() + initialized = False + while (time.time() - start) < self.startup_timeout: + time.sleep(0.1) + if not self.is_alive(): + raise Error("Can't start torrent2http, see log for details", Error.PROCESS_ERROR) + try: + self.status(1) + initialized = True + break + except Error: + pass + + if not initialized: + raise Error("Can't start torrent2http, time is out", Error.TIMEOUT) + self._log("torrent2http successfully started.") + + def check_torrent_error(self, status=None): + if not status: + status = self.status() + if status.error: + raise Error("Torrent error: %s" % status.error, Error.TORRENT_ERROR, reason=status.error) + + def status(self, timeout=10): + status = self._decode(self._request('status', timeout)) + status = SessionStatus(**status) + return status + + def _detect_media_type(self, name): + ext = os.path.splitext(name)[1] + if ext in self.SUBTITLES_FORMATS: + return MediaType.SUBTITLES + else: + mime_type = mimetypes.guess_type(name)[0] + if not mime_type: + return MediaType.UNKNOWN + mime_type = mime_type.split("/")[0] + if mime_type == 'audio': + return MediaType.AUDIO + elif mime_type == 'video': + return MediaType.VIDEO + else: + return MediaType.UNKNOWN + + def list(self, media_types=None, timeout=10): + files = self._decode(self._request('ls', timeout))['files'] + if files: + res = [FileStatus(index=index, media_type=self._detect_media_type(f['name']), **f) + for index, f in enumerate(files)] + if media_types is not None: + res = filter(lambda fs: fs.media_type in media_types, res) + return res + + def file_status(self, file_index, timeout=10): + res = self.list(timeout=timeout) + if res: + try: + return next((f for f in res if f.index == file_index)) + except StopIteration: + raise Error("Requested file index (%d) is invalid" % file_index, Error.INVALID_FILE_INDEX, + file_index=file_index) + + def peers(self, timeout=10): + peers = self._decode(self._request('peers', timeout))['peers'] + if peers: + return [PeerInfo(**p) for p in peers] + + def is_alive(self): + return self.process and self.process.poll() is None + + @staticmethod + def _decode(response): + try: + return json.loads(response) + except (KeyError, ValueError), e: + raise Error("Can't decode response from torrent2http: %r" % e, Error.REQUEST_ERROR) + + def _request(self, cmd, timeout=None): + if not self.is_alive(): + raise Error("torrent2http is not started", Error.REQUEST_ERROR) + try: + url = "http://%s:%s/%s" % (self.bind_host, self.bind_port, cmd) + kwargs = {} + if timeout is not None: + kwargs['timeout'] = timeout + return urllib2.urlopen(url, **kwargs).read() + except urllib2.URLError as e: + if isinstance(e.reason, socket.timeout): + raise Error("Timeout occurred while sending command '%s' to torrent2http" % cmd, Error.TIMEOUT) + else: + raise Error("Can't send command '%s' to torrent2http: %r" % (cmd, e), Error.REQUEST_ERROR) + except socket.error as e: + reason = e[1] if isinstance(e, tuple) else e + raise Error("Can't read from torrent2http: %s" % reason, Error.REQUEST_ERROR) + + def wait_on_close(self, wait_timeout=10): + self.wait_on_close_timeout = wait_timeout + + def close(self): + if self.logpipe and self.wait_on_close_timeout is None: + self.logpipe.close() + if self.is_alive(): + self._log("Shutting down torrent2http...") + self._request('shutdown') + finished = False + if self.wait_on_close_timeout is not None: + start = time.time() + os.close(self.logpipe.write_fd) + while (time.time() - start) < self.wait_on_close_timeout: + time.sleep(0.5) + if not self.is_alive(): + finished = True + break + if not finished: + self._log("Timeout occurred while shutting down torrent2http, killing it") + self.process.kill() + else: + self._log("torrent2http successfully shut down.") + self.wait_on_close_timeout = None + self.logpipe = None + self.process = None \ No newline at end of file diff --git a/lib/torrent2http/error.py b/lib/torrent2http/error.py new file mode 100644 index 0000000..9726edf --- /dev/null +++ b/lib/torrent2http/error.py @@ -0,0 +1,21 @@ + +class Error(Exception): + TORRENT_ERROR = 12 + UNKNOWN_PLATFORM = 1 + XBMC_HOME_NOT_DEFINED = 2 + NOEXEC_FILESYSTEM = 3 + REQUEST_ERROR = 5 + INVALID_DOWNLOAD_PATH = 6 + BIND_ERROR = 7 + POPEN_ERROR = 8 + PROCESS_ERROR = 9 + TIMEOUT = 10 + INVALID_FILE_INDEX = 11 + + def __init__(self, message, code=0, **kwargs): + self.message = message + self.code = code + self.kwargs = kwargs + + def __str__(self): + return self.message \ No newline at end of file diff --git a/lib/torrent2http/logpipe.py b/lib/torrent2http/logpipe.py new file mode 100644 index 0000000..d599c7b --- /dev/null +++ b/lib/torrent2http/logpipe.py @@ -0,0 +1,33 @@ +import os +import threading +import re + + +class LogPipe(threading.Thread): + + def __init__(self, logger): + threading.Thread.__init__(self) + self.daemon = False + self.logger = logger + self.read_fd, self.write_fd = os.pipe() + self.stop = threading.Event() + self.start() + + def fileno(self): + return self.write_fd + + def run(self): + self.logger("Logging thread started.") + with os.fdopen(self.read_fd) as f: + for line in iter(f.readline, ""): + line = re.sub(r'^\d+/\d+/\d+ \d+:\d+:\d+ ', '', line) + self.logger(line.strip()) + if self.stop.is_set(): + break + self.logger("Logging thread finished.") + + def close(self): + self.stop.set() + f = os.fdopen(self.write_fd, "w") + f.write("Stopping logging thread...\n") + f.close() diff --git a/lib/torrent2http/mimetypes.py b/lib/torrent2http/mimetypes.py new file mode 100644 index 0000000..eff6135 --- /dev/null +++ b/lib/torrent2http/mimetypes.py @@ -0,0 +1,613 @@ +"""Guess the MIME type of a file. + +This module defines two useful functions: + +guess_type(url, strict=1) -- guess the MIME type and encoding of a URL. + +guess_extension(type, strict=1) -- guess the extension for a given MIME type. + +It also contains the following, for tuning the behavior: + +Data: + +knownfiles -- list of files to parse +inited -- flag set when init() has been called +suffix_map -- dictionary mapping suffixes to suffixes +encodings_map -- dictionary mapping suffixes to encodings +types_map -- dictionary mapping suffixes to types + +Functions: + +init([files]) -- parse a list of files, default knownfiles (on Windows, the + default values are taken from the registry) +read_mime_types(file) -- parse one file, return a dictionary or None +""" + +import os +import sys +import posixpath +import urllib +try: + import _winreg +except ImportError: + _winreg = None + +__all__ = [ + "guess_type","guess_extension","guess_all_extensions", + "add_type","read_mime_types","init" +] + +knownfiles = [ + "/etc/mime.types", + "/etc/httpd/mime.types", # Mac OS X + "/etc/httpd/conf/mime.types", # Apache + "/etc/apache/mime.types", # Apache 1 + "/etc/apache2/mime.types", # Apache 2 + "/usr/local/etc/httpd/conf/mime.types", + "/usr/local/lib/netscape/mime.types", + "/usr/local/etc/httpd/conf/mime.types", # Apache 1.2 + "/usr/local/etc/mime.types", # Apache 1.3 + ] + +inited = False +_db = None + + +class MimeTypes: + """MIME-types datastore. + + This datastore can handle information from mime.types-style files + and supports basic determination of MIME type from a filename or + URL, and can guess a reasonable extension given a MIME type. + """ + + def __init__(self, filenames=(), strict=True): + if not inited: + init() + self.encodings_map = encodings_map.copy() + self.suffix_map = suffix_map.copy() + self.types_map = ({}, {}) # dict for (non-strict, strict) + self.types_map_inv = ({}, {}) + for (ext, type) in types_map.items(): + self.add_type(type, ext, True) + for (ext, type) in common_types.items(): + self.add_type(type, ext, False) + for name in filenames: + self.read(name, strict) + + def add_type(self, type, ext, strict=True): + """Add a mapping between a type and an extension. + + When the extension is already known, the new + type will replace the old one. When the type + is already known the extension will be added + to the list of known extensions. + + If strict is true, information will be added to + list of standard types, else to the list of non-standard + types. + """ + self.types_map[strict][ext] = type + exts = self.types_map_inv[strict].setdefault(type, []) + if ext not in exts: + exts.append(ext) + + def guess_type(self, url, strict=True): + """Guess the type of a file based on its URL. + + Return value is a tuple (type, encoding) where type is None if + the type can't be guessed (no or unknown suffix) or a string + of the form type/subtype, usable for a MIME Content-type + header; and encoding is None for no encoding or the name of + the program used to encode (e.g. compress or gzip). The + mappings are table driven. Encoding suffixes are case + sensitive; type suffixes are first tried case sensitive, then + case insensitive. + + The suffixes .tgz, .taz and .tz (case sensitive!) are all + mapped to '.tar.gz'. (This is table-driven too, using the + dictionary suffix_map.) + + Optional `strict' argument when False adds a bunch of commonly found, + but non-standard types. + """ + scheme, url = urllib.splittype(url) + if scheme == 'data': + # syntax of data URLs: + # dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + # mediatype := [ type "/" subtype ] *( ";" parameter ) + # data := *urlchar + # parameter := attribute "=" value + # type/subtype defaults to "text/plain" + comma = url.find(',') + if comma < 0: + # bad data URL + return None, None + semi = url.find(';', 0, comma) + if semi >= 0: + type = url[:semi] + else: + type = url[:comma] + if '=' in type or '/' not in type: + type = 'text/plain' + return type, None # never compressed, so encoding is None + base, ext = posixpath.splitext(url) + while ext in self.suffix_map: + base, ext = posixpath.splitext(base + self.suffix_map[ext]) + if ext in self.encodings_map: + encoding = self.encodings_map[ext] + base, ext = posixpath.splitext(base) + else: + encoding = None + types_map = self.types_map[True] + if ext in types_map: + return types_map[ext], encoding + elif ext.lower() in types_map: + return types_map[ext.lower()], encoding + elif strict: + return None, encoding + types_map = self.types_map[False] + if ext in types_map: + return types_map[ext], encoding + elif ext.lower() in types_map: + return types_map[ext.lower()], encoding + else: + return None, encoding + + def guess_all_extensions(self, type, strict=True): + """Guess the extensions for a file based on its MIME type. + + Return value is a list of strings giving the possible filename + extensions, including the leading dot ('.'). The extension is not + guaranteed to have been associated with any particular data stream, + but would be mapped to the MIME type `type' by guess_type(). + + Optional `strict' argument when false adds a bunch of commonly found, + but non-standard types. + """ + type = type.lower() + extensions = self.types_map_inv[True].get(type, []) + if not strict: + for ext in self.types_map_inv[False].get(type, []): + if ext not in extensions: + extensions.append(ext) + return extensions + + def guess_extension(self, type, strict=True): + """Guess the extension for a file based on its MIME type. + + Return value is a string giving a filename extension, + including the leading dot ('.'). The extension is not + guaranteed to have been associated with any particular data + stream, but would be mapped to the MIME type `type' by + guess_type(). If no extension can be guessed for `type', None + is returned. + + Optional `strict' argument when false adds a bunch of commonly found, + but non-standard types. + """ + extensions = self.guess_all_extensions(type, strict) + if not extensions: + return None + return extensions[0] + + def read(self, filename, strict=True): + """ + Read a single mime.types-format file, specified by pathname. + + If strict is true, information will be added to + list of standard types, else to the list of non-standard + types. + """ + with open(filename) as fp: + self.readfp(fp, strict) + + def readfp(self, fp, strict=True): + """ + Read a single mime.types-format file. + + If strict is true, information will be added to + list of standard types, else to the list of non-standard + types. + """ + while 1: + line = fp.readline() + if not line: + break + words = line.split() + for i in range(len(words)): + if words[i][0] == '#': + del words[i:] + break + if not words: + continue + type, suffixes = words[0], words[1:] + for suff in suffixes: + self.add_type(type, '.' + suff, strict) + + def read_windows_registry(self, strict=True): + """ + Load the MIME types database from Windows registry. + + If strict is true, information will be added to + list of standard types, else to the list of non-standard + types. + """ + + # Windows only + if not _winreg: + return + + def enum_types(mimedb): + i = 0 + while True: + try: + ctype = _winreg.EnumKey(mimedb, i) + except EnvironmentError: + break + try: + ctype = ctype.encode(default_encoding) # omit in 3.x! + except UnicodeEncodeError: + pass + else: + yield ctype + i += 1 + + default_encoding = sys.getdefaultencoding() + with _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, + r'MIME\Database\Content Type') as mimedb: + for ctype in enum_types(mimedb): + try: + with _winreg.OpenKey(mimedb, ctype) as key: + suffix, datatype = _winreg.QueryValueEx(key, + 'Extension') + except EnvironmentError: + continue + if datatype != _winreg.REG_SZ: + continue + try: + suffix = suffix.encode(default_encoding) # omit in 3.x! + except UnicodeEncodeError: + continue + self.add_type(ctype, suffix, strict) + + +def guess_type(url, strict=True): + """Guess the type of a file based on its URL. + + Return value is a tuple (type, encoding) where type is None if the + type can't be guessed (no or unknown suffix) or a string of the + form type/subtype, usable for a MIME Content-type header; and + encoding is None for no encoding or the name of the program used + to encode (e.g. compress or gzip). The mappings are table + driven. Encoding suffixes are case sensitive; type suffixes are + first tried case sensitive, then case insensitive. + + The suffixes .tgz, .taz and .tz (case sensitive!) are all mapped + to ".tar.gz". (This is table-driven too, using the dictionary + suffix_map). + + Optional `strict' argument when false adds a bunch of commonly found, but + non-standard types. + """ + if _db is None: + init() + return _db.guess_type(url, strict) + + +def guess_all_extensions(type, strict=True): + """Guess the extensions for a file based on its MIME type. + + Return value is a list of strings giving the possible filename + extensions, including the leading dot ('.'). The extension is not + guaranteed to have been associated with any particular data + stream, but would be mapped to the MIME type `type' by + guess_type(). If no extension can be guessed for `type', None + is returned. + + Optional `strict' argument when false adds a bunch of commonly found, + but non-standard types. + """ + if _db is None: + init() + return _db.guess_all_extensions(type, strict) + +def guess_extension(type, strict=True): + """Guess the extension for a file based on its MIME type. + + Return value is a string giving a filename extension, including the + leading dot ('.'). The extension is not guaranteed to have been + associated with any particular data stream, but would be mapped to the + MIME type `type' by guess_type(). If no extension can be guessed for + `type', None is returned. + + Optional `strict' argument when false adds a bunch of commonly found, + but non-standard types. + """ + if _db is None: + init() + return _db.guess_extension(type, strict) + +def add_type(type, ext, strict=True): + """Add a mapping between a type and an extension. + + When the extension is already known, the new + type will replace the old one. When the type + is already known the extension will be added + to the list of known extensions. + + If strict is true, information will be added to + list of standard types, else to the list of non-standard + types. + """ + if _db is None: + init() + return _db.add_type(type, ext, strict) + + +def init(files=None): + global suffix_map, types_map, encodings_map, common_types + global inited, _db + inited = True # so that MimeTypes.__init__() doesn't call us again + db = MimeTypes() + if files is None: + if _winreg: + db.read_windows_registry() + files = knownfiles + for file in files: + if os.path.isfile(file): + db.read(file) + encodings_map = db.encodings_map + suffix_map = db.suffix_map + types_map = db.types_map[True] + common_types = db.types_map[False] + # Make the DB a global variable now that it is fully initialized + _db = db + + +def read_mime_types(file): + try: + f = open(file) + except IOError: + return None + db = MimeTypes() + db.readfp(f, True) + return db.types_map[True] + + +def _default_mime_types(): + global suffix_map + global encodings_map + global types_map + global common_types + + suffix_map = { + '.tgz': '.tar.gz', + '.taz': '.tar.gz', + '.tz': '.tar.gz', + '.tbz2': '.tar.bz2', + } + + encodings_map = { + '.gz': 'gzip', + '.Z': 'compress', + '.bz2': 'bzip2', + } + + # Before adding new types, make sure they are either registered with IANA, + # at http://www.isi.edu/in-notes/iana/assignments/media-types + # or extensions, i.e. using the x- prefix + + # If you add to these, please keep them sorted! + types_map = { + '.3gp' : 'video/3gp', + '.a' : 'application/octet-stream', + '.ai' : 'application/postscript', + '.aif' : 'audio/x-aiff', + '.aifc' : 'audio/x-aiff', + '.aiff' : 'audio/x-aiff', + '.asf' : 'video/x-ms-asf', + '.asx' : 'video/x-ms-asf', + '.au' : 'audio/basic', + '.avi' : 'video/x-msvideo', + '.axv' : 'video/annodex', + '.bat' : 'text/plain', + '.bcpio' : 'application/x-bcpio', + '.bin' : 'application/octet-stream', + '.bmp' : 'image/x-ms-bmp', + '.c' : 'text/plain', + # Duplicates :( + '.cdf' : 'application/x-cdf', + '.cdf' : 'application/x-netcdf', + '.cpio' : 'application/x-cpio', + '.csh' : 'application/x-csh', + '.css' : 'text/css', + '.dif' : 'video/dv', + '.dl' : 'video/dl', + '.dll' : 'application/octet-stream', + '.dv' : 'video/dv', + '.doc' : 'application/msword', + '.dot' : 'application/msword', + '.dvi' : 'application/x-dvi', + '.eml' : 'message/rfc822', + '.eps' : 'application/postscript', + '.etx' : 'text/x-setext', + '.exe' : 'application/octet-stream', + '.fli' : 'video/fli', + '.flv' : 'video/x-flv', + '.gif' : 'image/gif', + '.gl' : 'video/gl', + '.gtar' : 'application/x-gtar', + '.h' : 'text/plain', + '.hdf' : 'application/x-hdf', + '.htm' : 'text/html', + '.html' : 'text/html', + '.ief' : 'image/ief', + '.jpe' : 'image/jpeg', + '.jpeg' : 'image/jpeg', + '.jpg' : 'image/jpeg', + '.js' : 'application/x-javascript', + '.ksh' : 'text/plain', + '.latex' : 'application/x-latex', + '.lsf' : 'video/x-la-lsf', + '.lsx' : 'video/x-la-lsf', + '.m1v' : 'video/mpeg', + '.man' : 'application/x-troff-man', + '.me' : 'application/x-troff-me', + '.mht' : 'message/rfc822', + '.mhtml' : 'message/rfc822', + '.mif' : 'application/x-mif', + '.mng' : 'video/x-mng', + '.movie' : 'video/x-sgi-movie', + '.mp2' : 'audio/mpeg', + '.mp3' : 'audio/mpeg', + '.mp4' : 'video/mp4', + '.mpa' : 'video/mpeg', + '.mpe' : 'video/mpeg', + '.mpeg' : 'video/mpeg', + '.mpg' : 'video/mpeg', + '.mpv' : 'video/matroska', + '.mkv' : 'video/matroska', + '.mov' : 'video/quicktime', + '.ms' : 'application/x-troff-ms', + '.nc' : 'application/x-netcdf', + '.nws' : 'message/rfc822', + '.o' : 'application/octet-stream', + '.obj' : 'application/octet-stream', + '.oda' : 'application/oda', + '.ogv' : 'video/ogg', + '.p12' : 'application/x-pkcs12', + '.p7c' : 'application/pkcs7-mime', + '.pbm' : 'image/x-portable-bitmap', + '.pdf' : 'application/pdf', + '.pfx' : 'application/x-pkcs12', + '.pgm' : 'image/x-portable-graymap', + '.pl' : 'text/plain', + '.png' : 'image/png', + '.pnm' : 'image/x-portable-anymap', + '.pot' : 'application/vnd.ms-powerpoint', + '.ppa' : 'application/vnd.ms-powerpoint', + '.ppm' : 'image/x-portable-pixmap', + '.pps' : 'application/vnd.ms-powerpoint', + '.ppt' : 'application/vnd.ms-powerpoint', + '.ps' : 'application/postscript', + '.pwz' : 'application/vnd.ms-powerpoint', + '.py' : 'text/x-python', + '.pyc' : 'application/x-python-code', + '.pyo' : 'application/x-python-code', + '.qt' : 'video/quicktime', + '.ra' : 'audio/x-pn-realaudio', + '.ram' : 'application/x-pn-realaudio', + '.ras' : 'image/x-cmu-raster', + '.rdf' : 'application/xml', + '.rgb' : 'image/x-rgb', + '.roff' : 'application/x-troff', + '.rtx' : 'text/richtext', + '.sgm' : 'text/x-sgml', + '.sgml' : 'text/x-sgml', + '.sh' : 'application/x-sh', + '.shar' : 'application/x-shar', + '.snd' : 'audio/basic', + '.so' : 'application/octet-stream', + '.src' : 'application/x-wais-source', + '.sv4cpio': 'application/x-sv4cpio', + '.sv4crc' : 'application/x-sv4crc', + '.swf' : 'application/x-shockwave-flash', + '.t' : 'application/x-troff', + '.tar' : 'application/x-tar', + '.tcl' : 'application/x-tcl', + '.tex' : 'application/x-tex', + '.texi' : 'application/x-texinfo', + '.texinfo': 'application/x-texinfo', + '.tif' : 'image/tiff', + '.tiff' : 'image/tiff', + '.tr' : 'application/x-troff', + '.ts' : 'video/MP2T', + '.tsv' : 'text/tab-separated-values', + '.txt' : 'text/plain', + '.ustar' : 'application/x-ustar', + '.vcf' : 'text/x-vcard', + '.wav' : 'audio/x-wav', + '.webm' : 'video/webm', + '.wiz' : 'application/msword', + '.wm' : 'video/x-ms-wm', + '.wmv' : 'video/x-ms-wmv', + '.wmx' : 'video/x-ms-wmx', + '.wvx' : 'video/x-ms-wvx', + '.wsdl' : 'application/xml', + '.xbm' : 'image/x-xbitmap', + '.xlb' : 'application/vnd.ms-excel', + # Duplicates :( + '.xls' : 'application/excel', + '.xls' : 'application/vnd.ms-excel', + '.xml' : 'text/xml', + '.xpdl' : 'application/xml', + '.xpm' : 'image/x-xpixmap', + '.xsl' : 'application/xml', + '.xwd' : 'image/x-xwindowdump', + '.zip' : 'application/zip', + } + + # These are non-standard types, commonly found in the wild. They will + # only match if strict=0 flag is given to the API methods. + + # Please sort these too + common_types = { + '.jpg' : 'image/jpg', + '.mid' : 'audio/midi', + '.midi': 'audio/midi', + '.pct' : 'image/pict', + '.pic' : 'image/pict', + '.pict': 'image/pict', + '.rtf' : 'application/rtf', + '.xul' : 'text/xul' + } + + +_default_mime_types() + + +if __name__ == '__main__': + import getopt + + USAGE = """\ +Usage: mimetypes.py [options] type + +Options: + --help / -h -- print this message and exit + --lenient / -l -- additionally search of some common, but non-standard + types. + --extension / -e -- guess extension instead of type + +More than one type argument may be given. +""" + + def usage(code, msg=''): + print USAGE + if msg: print msg + sys.exit(code) + + try: + opts, args = getopt.getopt(sys.argv[1:], 'hle', + ['help', 'lenient', 'extension']) + except getopt.error, msg: + usage(1, msg) + + strict = 1 + extension = 0 + for opt, arg in opts: + if opt in ('-h', '--help'): + usage(0) + elif opt in ('-l', '--lenient'): + strict = 0 + elif opt in ('-e', '--extension'): + extension = 1 + for gtype in args: + if extension: + guess = guess_extension(gtype, strict) + if not guess: print "I don't know anything about type", gtype + else: print guess + else: + guess, encoding = guess_type(gtype, strict) + if not guess: print "I don't know anything about type", gtype + else: print 'type:', guess, 'encoding:', encoding diff --git a/lib/torrent2http/platform.py b/lib/torrent2http/platform.py new file mode 100644 index 0000000..dbedd2b --- /dev/null +++ b/lib/torrent2http/platform.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import +from platform import uname +from .error import Error +import sys +import os + + +class Platform: + def __init__(self): + self.arch = self.arch() + self.system = self.system() + + def __str__(self): + return "%s/%s" % (self.system, self.arch) + + @staticmethod + def arch(): + if uname()[4].startswith('arm'): + return 'arm' + elif sys.maxsize > 2**32: + return 'x64' + else: + return 'x86' + + @staticmethod + def system(): + if sys.platform.startswith('linux'): + if 'ANDROID_DATA' in os.environ: + return 'android' + else: + return 'linux' + elif sys.platform.startswith('win'): + return 'windows' + elif sys.platform.startswith('darwin'): + return 'darwin' + else: + raise Error("Platform %s is unknown" % sys.platform, Error.UNKNOWN_PLATFORM, + platform=sys.platform)