Added code comments, better support of unicode path names
parent
10d91a1d2d
commit
7fcf3e20ab
|
@ -0,0 +1,60 @@
|
|||
script.module.torrent2http
|
||||
==========================
|
||||
|
||||
This add-on is binding to [torrent2http](https://github.com/anteo/torrent2http) client.
|
||||
It is bundled with torrent2http binaries for Android ARM, Linux x86/x64/ARM, Windows x86/x64 and Darwin/OSX x64 platforms.
|
||||
You can download it from my [repository](http://bit.ly/184XKjm)
|
||||
|
||||
This add-on can be used to stream media files from torrents without need to download entire files.
|
||||
|
||||
Internally, it runs pre-compiled torrent2http client binary, which starts local HTTP server, presenting contents of torrent.
|
||||
Next, request to HTTP server can be sent to receive list of files or to start streaming of needed file inside of torrent.
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
### Get list of files inside torrent ###
|
||||
|
||||
Getting list of files inside torrent is straightforward:
|
||||
|
||||
import xbmc
|
||||
from torrent2http import State, Engine, MediaType
|
||||
from contextlib import closing
|
||||
|
||||
# Create instance of Engine
|
||||
engine = Engine(uri="...")
|
||||
files = []
|
||||
# Ensure we'll close engine on exception
|
||||
with closing(engine):
|
||||
# Start engine
|
||||
engine.start()
|
||||
# Wait until files received
|
||||
while not files and not xbmc.abortRequested:
|
||||
# Will list only video files in torrent
|
||||
files = engine.list(media_types=[MediaType.VIDEO])
|
||||
# Check if there is loading torrent error and raise exception
|
||||
engine.check_torrent_error()
|
||||
xbmc.sleep(200)
|
||||
|
||||
### Start streaming ###
|
||||
|
||||
import xbmc
|
||||
from torrent2http import State, Engine, MediaType
|
||||
from contextlib import closing
|
||||
|
||||
# We can know file_id of needed video file on this step, if no, we'll try to detect one.
|
||||
file_id = None
|
||||
engine = Engine(uri="...")
|
||||
with closing(self.engine):
|
||||
# Start engine and instruct torrent2http to begin download first file,
|
||||
# so it can start searching and connecting to peers
|
||||
self.engine.start(file_id or 0)
|
||||
while not xbmc.abortRequested:
|
||||
sleep(self.SLEEP_DELAY)
|
||||
status = self.engine.status()
|
||||
self.engine.check_torrent_error(status)
|
||||
if status.state in [State.DOWNLOADING, State.FINISHED, State.SEEDING]:
|
||||
ready = True
|
||||
break
|
||||
|
||||
... todo ...
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="script.module.torrent2http" name="torrent2http" version="0.0.5" provider-name="anteo">
|
||||
<addon id="script.module.torrent2http" name="torrent2http" version="0.0.6" provider-name="anteo">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.14.0"/>
|
||||
</requires>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
[B]Version 0.0.6[/B]
|
||||
+ Added code comments
|
||||
* Better support of unicode path names
|
||||
|
||||
[B]Version 0.0.5[/B]
|
||||
+ Torrent2http binaries updated to 1.0.1
|
||||
|
||||
|
|
|
@ -7,17 +7,22 @@ import subprocess
|
|||
import sys
|
||||
import time
|
||||
import urllib2
|
||||
import httplib
|
||||
from os.path import dirname
|
||||
|
||||
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
|
||||
from util import can_bind, find_free_port, ensure_fs_encoding
|
||||
|
||||
|
||||
class Engine:
|
||||
"""
|
||||
This is python binding class to torrent2http client.
|
||||
"""
|
||||
SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl',
|
||||
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
|
||||
|
||||
|
@ -42,6 +47,12 @@ class Engine:
|
|||
xbmc.log("[torrent2http] %s" % message)
|
||||
|
||||
def _get_binary_path(self, binaries_path):
|
||||
"""
|
||||
Detects platform and returns corresponding torrent2http binary path
|
||||
|
||||
:param binaries_path:
|
||||
:return: torrent2http binary 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)
|
||||
|
@ -74,27 +85,6 @@ class Engine:
|
|||
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,
|
||||
|
@ -105,6 +95,55 @@ class Engine:
|
|||
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):
|
||||
"""
|
||||
Creates engine instance. It doesn't do anything except initializing object members. For starting engine use
|
||||
start() method.
|
||||
|
||||
:param uri: Torrent URI (magnet://, file:// or http://)
|
||||
:param binaries_path: Path to torrent2http binaries
|
||||
:param platform: Object with two methods implemented: arch() and system()
|
||||
:param download_path: Torrent download path
|
||||
:param bind_host: Bind host of torrent2http
|
||||
:param bind_port: Bind port of torrent2http
|
||||
:param connections_limit: Set a global limit on the number of connections opened
|
||||
:param download_kbps: Max download rate (kB/s)
|
||||
:param upload_kbps: Max upload rate (kB/s)
|
||||
:param enable_dht: Enable DHT (Distributed Hash Table)
|
||||
:param enable_lsd: Enable LSD (Local Service Discovery)
|
||||
:param enable_natpmp: Enable NATPMP (NAT port-mapping)
|
||||
:param enable_upnp: Enable UPnP (UPnP port-mapping)
|
||||
:param enable_scrape: Enable sending scrape request to tracker (updates total peers/seeds count)
|
||||
:param log_stats: Log all stats (incl. log_overall_progress, log_files_progress, log_pieces_progress)
|
||||
:param encryption: Encryption: 0=forced 1=enabled (default) 2=disabled
|
||||
:param keep_complete: Keep complete files after exiting
|
||||
:param keep_incomplete: Keep incomplete files after exiting
|
||||
:param keep_files: Keep all files after exiting (incl. keep_complete and keep_incomplete)
|
||||
:param log_files_progress: Log files progress
|
||||
:param log_overall_progress: Log overall progress
|
||||
:param log_pieces_progress: Log pieces progress
|
||||
:param listen_port: Use specified port for incoming connections
|
||||
:param use_random_port: Use random listen port (49152-65535)
|
||||
:param max_idle_timeout: Automatically shutdown torrent2http if no connection are active after a timeout
|
||||
:param no_sparse: Do not use sparse file allocation
|
||||
:param resume_file: Use fast resume file
|
||||
:param user_agent: Set an user agent
|
||||
:param startup_timeout: torrent2http startup timeout
|
||||
:param state_file: Use file for saving/restoring session state
|
||||
:param enable_utp: Enable uTP protocol
|
||||
:param enable_tcp: Enable TCP protocol
|
||||
:param debug_alerts: Show debug alert notifications
|
||||
:param logger: Instance of logging.Logger
|
||||
:param torrent_connect_boost: The number of peers to try to connect to immediately when the first tracker
|
||||
response is received for a torrent
|
||||
:param connection_speed: The number of peer connection attempts that are made per second
|
||||
:param peer_connect_timeout: The number of seconds to wait after a connection attempt is initiated to a peer
|
||||
:param request_timeout: The number of seconds until the current front piece request will time out
|
||||
:param min_reconnect_time: The time to wait between peer connection attempts. If the peer fails, the time is
|
||||
multiplied by fail counter
|
||||
:param max_failcount: The maximum times we try to connect to a peer before stop connecting again
|
||||
:param dht_routers: List of additional DHT routers (host:port pairs)
|
||||
:param trackers: List of additional tracker URLs
|
||||
"""
|
||||
self.dht_routers = dht_routers or []
|
||||
self.trackers = trackers or []
|
||||
self.max_failcount = max_failcount
|
||||
|
@ -154,21 +193,36 @@ class Engine:
|
|||
|
||||
@staticmethod
|
||||
def _validate_save_path(path):
|
||||
"""
|
||||
Ensures download path can be accessed locally.
|
||||
|
||||
:param path: Download path
|
||||
:return: Translated path
|
||||
"""
|
||||
import xbmc
|
||||
path = xbmc.translatePath(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):
|
||||
if not os.path.isdir(ensure_fs_encoding(path)):
|
||||
raise Error("Download path doesn't exist (%s)" % path, Error.INVALID_DOWNLOAD_PATH)
|
||||
return path
|
||||
|
||||
def start(self, start_index=None):
|
||||
"""
|
||||
Starts torrent2http client with specified settings. If it can be started in startup_timeout seconds, exception
|
||||
will be raised.
|
||||
|
||||
:param start_index: File index to start download instantly, if not specified, downloading will be paused, until
|
||||
any file requested
|
||||
"""
|
||||
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 not can_bind(self.bind_host, self.bind_port):
|
||||
port = 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))
|
||||
|
@ -225,10 +279,8 @@ class Engine:
|
|||
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')
|
||||
if isinstance(v, str) or isinstance(v, unicode):
|
||||
v = ensure_fs_encoding(v)
|
||||
else:
|
||||
v = str(v)
|
||||
args.append(v)
|
||||
|
@ -266,12 +318,25 @@ class Engine:
|
|||
self._log("torrent2http successfully started.")
|
||||
|
||||
def check_torrent_error(self, status=None):
|
||||
"""
|
||||
It is recommended to call this method periodically to check if any libtorrent errors occurred.
|
||||
Usually libtorrent sets error if it can't download or parse torrent file by specified URI.
|
||||
Note that torrent2http remains started after such error, so you need to shutdown it manually.
|
||||
|
||||
:param status: Pass return of status() method if you don't want status() called twice
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Returns libtorrent session status. See SessionStatus named tuple.
|
||||
|
||||
:rtype : SessionStatus
|
||||
:param timeout: torrent2http client request timeout
|
||||
"""
|
||||
status = self._decode(self._request('status', timeout))
|
||||
status = SessionStatus(**status)
|
||||
return status
|
||||
|
@ -293,6 +358,16 @@ class Engine:
|
|||
return MediaType.UNKNOWN
|
||||
|
||||
def list(self, media_types=None, timeout=10):
|
||||
"""
|
||||
Returns list of files in the torrent (see FileStatus named tuple).
|
||||
Note that it will return None if torrent file is not loaded yet by torrent2http client, so you may need to call
|
||||
this method periodically until results are returned.
|
||||
|
||||
:param media_types: List of media types (see MediaType constants)
|
||||
:param timeout: torrent2http client request timeout
|
||||
:rtype : list of FileStatus
|
||||
:return: List of files of specified media types or None if torrent is not loaded yet
|
||||
"""
|
||||
files = self._decode(self._request('ls', timeout))['files']
|
||||
if files:
|
||||
res = [FileStatus(index=index, media_type=self._detect_media_type(f['name']), **f)
|
||||
|
@ -302,6 +377,16 @@ class Engine:
|
|||
return res
|
||||
|
||||
def file_status(self, file_index, timeout=10):
|
||||
"""
|
||||
Returns file in the torrent with specified index (see FileStatus named tuple)
|
||||
Note that it will return None if torrent file is not loaded yet by torrent2http client, so you may need to call
|
||||
this method periodically until results are returned.
|
||||
|
||||
:param file_index: Requested file's index
|
||||
:param timeout: torrent2http client request timeout
|
||||
:return: File with specified index
|
||||
:rtype: FileStatus
|
||||
"""
|
||||
res = self.list(timeout=timeout)
|
||||
if res:
|
||||
try:
|
||||
|
@ -311,6 +396,13 @@ class Engine:
|
|||
file_index=file_index)
|
||||
|
||||
def peers(self, timeout=10):
|
||||
"""
|
||||
Returns list of peers connected (see PeerInfo named tuple).
|
||||
|
||||
:param timeout: torrent2http client request timeout
|
||||
:return: List of peers
|
||||
:rtype: list of PeerInfo
|
||||
"""
|
||||
peers = self._decode(self._request('peers', timeout))['peers']
|
||||
if peers:
|
||||
return [PeerInfo(**p) for p in peers]
|
||||
|
@ -334,8 +426,8 @@ class Engine:
|
|||
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):
|
||||
except (urllib2.URLError, httplib.HTTPException) as e:
|
||||
if isinstance(e, urllib2.URLError) and isinstance(e.reason, socket.timeout):
|
||||
raise Error("Timeout occurred while sending command '%s' to torrent2http" % cmd, Error.TIMEOUT)
|
||||
elif not self.is_alive() and self.started:
|
||||
raise Error("torrent2http has crashed.", Error.CRASHED)
|
||||
|
@ -346,9 +438,20 @@ class Engine:
|
|||
raise Error("Can't read from torrent2http: %s" % reason, Error.REQUEST_ERROR)
|
||||
|
||||
def wait_on_close(self, wait_timeout=10):
|
||||
"""
|
||||
By default, close() method sends shutdown command to torrent2http, stops logging and returns immediately, not
|
||||
waiting while torrent2http exits. It can be handy to wait torrent2http to view log messages during shutdown.
|
||||
So call this method with reasonable timeout before calling close().
|
||||
|
||||
:param wait_timeout: Time in seconds to wait until torrent2http client shut down
|
||||
"""
|
||||
self.wait_on_close_timeout = wait_timeout
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Shuts down torrent2http and stops logging. If wait_on_close() was called earlier, it will wait until
|
||||
torrent2http successfully exits.
|
||||
"""
|
||||
if self.logpipe and self.wait_on_close_timeout is None:
|
||||
self.logpipe.close()
|
||||
if self.is_alive():
|
||||
|
@ -371,4 +474,4 @@ class Engine:
|
|||
self.wait_on_close_timeout = None
|
||||
self.started = False
|
||||
self.logpipe = None
|
||||
self.process = None
|
||||
self.process = None
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
|
||||
class Error(Exception):
|
||||
TORRENT_ERROR = 12
|
||||
""" Error returned by libtorrent """
|
||||
CRASHED = 13
|
||||
""" torrent2http client has crashed abnormally """
|
||||
UNKNOWN_PLATFORM = 1
|
||||
""" Unknown/unsupported platform """
|
||||
XBMC_HOME_NOT_DEFINED = 2
|
||||
""" XBMC_HOME or KODI_HOME is not set """
|
||||
NOEXEC_FILESYSTEM = 3
|
||||
""" torrent2http binary is placed on noexec filesystem, so it can't be started """
|
||||
REQUEST_ERROR = 5
|
||||
""" Error occurred while sending request to torrent2http """
|
||||
INVALID_DOWNLOAD_PATH = 6
|
||||
""" Dowload path is invalid """
|
||||
BIND_ERROR = 7
|
||||
""" Bind error can occur on start, if it's impossible to find a port to bind torrent2http to """
|
||||
POPEN_ERROR = 8
|
||||
""" Can't start torrent2http client, path to binary doesn't exist or can't be executed """
|
||||
PROCESS_ERROR = 9
|
||||
""" torrent2http client started but exited abnormally, may be conflict in startup options """
|
||||
TIMEOUT = 10
|
||||
""" torrent2http not answered during specified timeout """
|
||||
INVALID_FILE_INDEX = 11
|
||||
""" Specified file index is invalid, no file with specified index found in torrent """
|
||||
|
||||
def __init__(self, message, code=0, **kwargs):
|
||||
self.message = message
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import sys
|
||||
import socket
|
||||
|
||||
|
||||
def can_bind(host, port):
|
||||
"""
|
||||
Checks we can bind to specified host and port
|
||||
|
||||
:param host: Host
|
||||
:param port: Port
|
||||
:return: True if bind succeed
|
||||
"""
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.bind((host, port))
|
||||
s.close()
|
||||
except socket.error:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def find_free_port(host):
|
||||
"""
|
||||
Finds free TCP port that can be used for binding
|
||||
|
||||
:param host: Host
|
||||
:return: Free port
|
||||
"""
|
||||
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 ensure_fs_encoding(string):
|
||||
if isinstance(string, str):
|
||||
string = string.decode('utf-8')
|
||||
return string.encode(sys.getfilesystemencoding() or 'utf-8')
|
Loading…
Reference in New Issue