parent
f39749807f
commit
6e04f3fc14
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="script.module.pyrrent2http" name="pyrrent2http" version="1.0.0" provider-name="inpos">
|
||||
<addon id="script.module.pyrrent2http" name="pyrrent2http" version="1.1.0" provider-name="inpos">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.14.0"/>
|
||||
<import addon="xbmc.python" version="3.0.0"/>
|
||||
<import addon="script.module.libtorrent" />
|
||||
<import addon="script.module.chardet" />
|
||||
</requires>
|
||||
|
@ -17,5 +17,5 @@
|
|||
<description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description>
|
||||
<description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description>
|
||||
<email>inpos@yandex.ru</email>
|
||||
<source>https://github.com/inpos/script.module.pyrrent2http</source></extension>
|
||||
<source>https://git.ukamnya.ru/ukamnya/script.module.pyrrent2http</source></extension>
|
||||
</addon>
|
||||
|
|
|
@ -3,33 +3,6 @@
|
|||
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")
|
||||
|
@ -39,5 +12,5 @@ FileStatus = namedtuple('FileStatus', "name, save_path, url, size, offset, downl
|
|||
PeerInfo = namedtuple('PeerInfo', "ip, flags, source, up_speed, down_speed, total_upload, total_download, "
|
||||
"country, client")
|
||||
|
||||
from engine import Engine
|
||||
from error import Error
|
||||
from .engine import Engine
|
||||
from .error import Error
|
||||
|
|
|
@ -1,31 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import threading
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import chardet
|
||||
import sys
|
||||
import time
|
||||
import pyrrent2http
|
||||
import xbmc
|
||||
from error import Error
|
||||
from . import SessionStatus, FileStatus, PeerInfo, Encryption
|
||||
from util import can_bind, find_free_port, localize_path, uri2path, detect_media_type
|
||||
import threading
|
||||
import urllib
|
||||
import chardet
|
||||
|
||||
from . import SessionStatus, FileStatus, PeerInfo
|
||||
from . import pyrrent2http
|
||||
from .error import Error
|
||||
from .structs import Encryption
|
||||
from .util import can_bind, find_free_port, localize_path, uri2path, detect_media_type
|
||||
|
||||
LOGGING = True
|
||||
|
||||
|
||||
class Engine:
|
||||
"""
|
||||
This is python binding class to pyrrent2http client.
|
||||
"""
|
||||
|
||||
def _log(self, message):
|
||||
if self.logger:
|
||||
self.logger(message)
|
||||
else:
|
||||
xbmc.log("[pyrrent2http] %s" % message)
|
||||
|
||||
|
||||
def __init__(self, uri=None, platform=None, download_path=".",
|
||||
bind_host='127.0.0.1', bind_port=5001, connections_limit=None, download_kbps=None, upload_kbps=None,
|
||||
bind_host='127.0.0.1', bind_port=5001, connections_limit=200, download_kbps=-1, upload_kbps=-1,
|
||||
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,
|
||||
|
@ -198,7 +204,7 @@ class Engine:
|
|||
'maxFailCount': self.max_failcount,
|
||||
'showPiecesProgress': self.log_pieces_progress,
|
||||
'idleTimeout': self.max_idle_timeout,
|
||||
#'fileIndex': start_index,
|
||||
# 'fileIndex': start_index,
|
||||
'connectionsLimit': self.connections_limit,
|
||||
'enableScrape': self.enable_scrape,
|
||||
'enableUTP': self.enable_utp,
|
||||
|
@ -208,15 +214,19 @@ class Engine:
|
|||
}
|
||||
|
||||
self._log("Invoking pyrrent2http")
|
||||
|
||||
class Logging(object):
|
||||
def __init__(self, _log):
|
||||
self._log = _log
|
||||
|
||||
def info(self, message):
|
||||
if LOGGING:
|
||||
self._log('INFO: %s' % (message,))
|
||||
|
||||
def error(self, message):
|
||||
if LOGGING:
|
||||
self._log('ERROR: %s' % (message,))
|
||||
|
||||
pyrrent2http.logging = Logging(self._log)
|
||||
|
||||
self.pyrrent2http = pyrrent2http.Pyrrent2http(**kwargs)
|
||||
|
@ -224,10 +234,9 @@ class Engine:
|
|||
self.pyrrent2http.startServices()
|
||||
self.pyrrent2http.addTorrent()
|
||||
self.pyrrent2http.startHTTP()
|
||||
self.pyrrent2http_loop = threading.Thread(target = self.pyrrent2http.loop)
|
||||
self.pyrrent2http_loop = threading.Thread(target=self.pyrrent2http.loop)
|
||||
self.pyrrent2http_loop.start()
|
||||
|
||||
|
||||
start = time.time()
|
||||
self.started = True
|
||||
initialized = False
|
||||
|
@ -236,7 +245,7 @@ class Engine:
|
|||
if not self.is_alive():
|
||||
raise Error("Can't start pyrrent2http, see log for details", Error.PROCESS_ERROR)
|
||||
try:
|
||||
#self.status(1)
|
||||
# self.status(1)
|
||||
initialized = True
|
||||
break
|
||||
except Error:
|
||||
|
@ -252,6 +261,7 @@ class Engine:
|
|||
|
||||
def pause(self):
|
||||
self.pyrrent2http.pause = True
|
||||
|
||||
def resume(self):
|
||||
self.pyrrent2http.pause = False
|
||||
|
||||
|
@ -279,8 +289,6 @@ class Engine:
|
|||
status = SessionStatus(**status)
|
||||
return status
|
||||
|
||||
|
||||
|
||||
def list(self, media_types=None, timeout=10):
|
||||
"""
|
||||
Returns list of files in the torrent (see FileStatus named tuple).
|
||||
|
@ -296,8 +304,9 @@ class Engine:
|
|||
if files:
|
||||
res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
|
||||
if media_types is not None:
|
||||
res = filter(lambda fs: fs.media_type in media_types, res)
|
||||
res = [fs for fs in res if fs.media_type in media_types]
|
||||
return res
|
||||
|
||||
def list_from_info(self, media_types=None):
|
||||
try:
|
||||
info = pyrrent2http.lt.torrent_info(uri2path(self.uri))
|
||||
|
@ -306,21 +315,21 @@ class Engine:
|
|||
files = []
|
||||
for i in range(info.num_files()):
|
||||
f = info.file_at(i)
|
||||
Url = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.quote(f.path)
|
||||
Url = 'http://' + "%s:%s" % (self.bind_host, self.bind_port) + '/files/' + urllib.parse.quote(f.path)
|
||||
files.append({
|
||||
'name': localize_path(f.path),
|
||||
'size': f.size,
|
||||
'offset': f.offset,
|
||||
'media_type': media_types and detect_media_type(f.path.decode(chardet.detect(f.path)['encoding'])) or '',
|
||||
'download': 0,
|
||||
'progress': 0.0,
|
||||
'save_path': '',
|
||||
'url': Url
|
||||
})
|
||||
if files:
|
||||
'name': localize_path(f.path),
|
||||
'size': f.size,
|
||||
'offset': f.offset,
|
||||
'media_type': media_types is not None and detect_media_type(f.path) or '',
|
||||
'download': 0,
|
||||
'progress': 0.0,
|
||||
'save_path': '',
|
||||
'url': Url
|
||||
})
|
||||
if len(files) > 0:
|
||||
res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
|
||||
if media_types is not None:
|
||||
res = filter(lambda fs: fs.media_type in media_types, res)
|
||||
res = [fs for fs in res if fs.media_type in media_types]
|
||||
return res
|
||||
|
||||
def file_status(self, file_index, timeout=10):
|
||||
|
@ -339,7 +348,7 @@ class Engine:
|
|||
return FileStatus(**filestatus)
|
||||
except:
|
||||
raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX,
|
||||
file_index=file_index)
|
||||
file_index=file_index)
|
||||
|
||||
def peers(self, timeout=10):
|
||||
"""
|
||||
|
@ -355,6 +364,7 @@ class Engine:
|
|||
|
||||
def is_alive(self):
|
||||
return self.pyrrent2http_loop.is_alive()
|
||||
|
||||
def wait_on_close(self, wait_timeout=10):
|
||||
"""
|
||||
By default, close() method sends shutdown command to pyrrent2http, stops logging and returns immediately, not
|
||||
|
|
|
@ -1,613 +0,0 @@
|
|||
"""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
|
|
@ -4,12 +4,13 @@ import chardet
|
|||
|
||||
try:
|
||||
from python_libtorrent import get_libtorrent # @UnresolvedImport
|
||||
lt=get_libtorrent()
|
||||
print('Imported libtorrent v%s from python_libtorrent' %(lt.version, ))
|
||||
except Exception, e:
|
||||
print('Error importing python_libtorrent.Exception: %s' %(str(e),))
|
||||
|
||||
lt = get_libtorrent()
|
||||
print(('Imported libtorrent v%s from python_libtorrent' % (lt.version,)))
|
||||
except Exception as e:
|
||||
print(('Error importing python_libtorrent.Exception: %s' % (str(e),)))
|
||||
try:
|
||||
import libtorrent as lt # @UnresolvedImport
|
||||
import libtorrent as lt # @UnresolvedImport
|
||||
except Exception as e:
|
||||
strerror = e.args
|
||||
print(strerror)
|
||||
|
@ -17,17 +18,16 @@ except Exception, e:
|
|||
|
||||
from random import SystemRandom
|
||||
import time
|
||||
import urllib
|
||||
import BaseHTTPServer
|
||||
import SocketServer
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import http.server
|
||||
import socketserver
|
||||
import threading
|
||||
import io
|
||||
from util import localize_path, Struct, detect_media_type, uri2path, encode_msg
|
||||
|
||||
|
||||
from .util import localize_path, Struct, detect_media_type, uri2path, encode_msg
|
||||
|
||||
if os.getenv('ANDROID_ROOT'):
|
||||
from ctypes import *
|
||||
|
||||
libc = CDLL('/system/lib/libc.so')
|
||||
libc.lseek64.restype = c_ulonglong
|
||||
libc.lseek64.argtypes = [c_uint, c_ulonglong, c_uint]
|
||||
|
@ -41,15 +41,16 @@ if os.getenv('ANDROID_ROOT'):
|
|||
if not hasattr(os, 'getppid'):
|
||||
import ctypes
|
||||
|
||||
TH32CS_SNAPPROCESS = 0x02L
|
||||
TH32CS_SNAPPROCESS = 0x02
|
||||
CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot # @UndefinedVariable
|
||||
GetCurrentProcessId = ctypes.windll.kernel32.GetCurrentProcessId # @UndefinedVariable
|
||||
GetCurrentProcessId = ctypes.windll.kernel32.GetCurrentProcessId # @UndefinedVariable
|
||||
|
||||
MAX_PATH = 260
|
||||
|
||||
_kernel32dll = ctypes.windll.Kernel32
|
||||
CloseHandle = _kernel32dll.CloseHandle
|
||||
|
||||
|
||||
class PROCESSENTRY32(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwSize", ctypes.c_ulong),
|
||||
|
@ -65,9 +66,11 @@ if not hasattr(os, 'getppid'):
|
|||
("szExeFile", ctypes.c_wchar * MAX_PATH)
|
||||
]
|
||||
|
||||
|
||||
Process32First = _kernel32dll.Process32FirstW
|
||||
Process32Next = _kernel32dll.Process32NextW
|
||||
|
||||
|
||||
def getppid():
|
||||
'''
|
||||
:return: The pid of the parent of this process.
|
||||
|
@ -93,27 +96,31 @@ if not hasattr(os, 'getppid'):
|
|||
|
||||
return result
|
||||
|
||||
|
||||
os.getppid = getppid
|
||||
|
||||
#################################################################################
|
||||
|
||||
AVOID_HTTP_SERVER_EXCEPTION_OUTPUT = True
|
||||
VERSION = "0.6.0"
|
||||
#USER_AGENT = "pyrrent2http/" + VERSION + " libtorrent/" + lt.version
|
||||
# USER_AGENT = "pyrrent2http/" + VERSION + " libtorrent/" + lt.version
|
||||
USER_AGENT = 'libtorrent/1.0.9.0'
|
||||
|
||||
VIDEO_EXTS={'.avi':'video/x-msvideo','.mp4':'video/mp4','.mkv':'video/x-matroska',
|
||||
'.m4v':'video/mp4','.mov':'video/quicktime', '.mpg':'video/mpeg','.ogv':'video/ogg',
|
||||
'.ogg':'video/ogg', '.webm':'video/webm', '.ts': 'video/mp2t', '.3gp':'video/3gpp'}
|
||||
VIDEO_EXTS = {'.avi': 'video/x-msvideo', '.mp4': 'video/mp4', '.mkv': 'video/x-matroska',
|
||||
'.m4v': 'video/mp4', '.mov': 'video/quicktime', '.mpg': 'video/mpeg', '.ogv': 'video/ogg',
|
||||
'.ogg': 'video/ogg', '.webm': 'video/webm', '.ts': 'video/mp2t', '.3gp': 'video/3gpp'}
|
||||
|
||||
|
||||
######################################################################################
|
||||
|
||||
class Ticker(object):
|
||||
def __init__(self, interval):
|
||||
self.tick = False
|
||||
self._timer = None
|
||||
self.interval = interval
|
||||
self._timer = None
|
||||
self.interval = interval
|
||||
self.is_running = False
|
||||
self.start()
|
||||
|
||||
@property
|
||||
def true(self):
|
||||
if self.tick:
|
||||
|
@ -137,23 +144,25 @@ class Ticker(object):
|
|||
self._timer.cancel()
|
||||
self.is_running = False
|
||||
|
||||
|
||||
#######################################################################################
|
||||
|
||||
class TorrentFile(object):
|
||||
tfs = None
|
||||
closed = True
|
||||
save_path = str()
|
||||
fileEntry = None
|
||||
index = 0
|
||||
filePtr = None
|
||||
downloaded = 0
|
||||
progress = 0.0
|
||||
pdl_thread = None
|
||||
tfs = None
|
||||
closed = True
|
||||
save_path = str()
|
||||
fileEntry = None
|
||||
index = 0
|
||||
filePtr = None
|
||||
downloaded = 0
|
||||
progress = 0.0
|
||||
pdl_thread = None
|
||||
|
||||
def __init__(self, tfs, fileEntry, savePath, index):
|
||||
self.tfs = tfs
|
||||
self.fileEntry = fileEntry
|
||||
self.name = self.fileEntry.path
|
||||
self.unicode_name = self.name.decode(chardet.detect(self.name)['encoding'])
|
||||
self.unicode_name = isinstance(self.name, str) and self.name or self.name.decode(chardet.detect(self.name)['encoding'])
|
||||
self.media_type = detect_media_type(self.unicode_name)
|
||||
self.save_path = savePath
|
||||
self.index = index
|
||||
|
@ -165,8 +174,10 @@ class TorrentFile(object):
|
|||
|
||||
def Downloaded(self):
|
||||
return self.downloaded
|
||||
|
||||
def Progress(self):
|
||||
return self.progress
|
||||
|
||||
def __fileptr_(self):
|
||||
if self.closed:
|
||||
return None
|
||||
|
@ -180,35 +191,44 @@ class TorrentFile(object):
|
|||
else:
|
||||
self.filePtr = io.open(self.save_path, 'rb')
|
||||
return self.filePtr
|
||||
|
||||
def log(self, message):
|
||||
fnum = self.tfs.openedFiles.index(self)
|
||||
logging.info("[Thread No.%d] %s\n" % (fnum, message))
|
||||
|
||||
def Pieces(self):
|
||||
startPiece, _ = self.pieceFromOffset(1)
|
||||
endPiece, _ = self.pieceFromOffset(self.size - 1)
|
||||
return startPiece, endPiece
|
||||
|
||||
def SetPriority(self, priority):
|
||||
self.tfs.setPriority(self.index, priority)
|
||||
|
||||
def readOffset(self):
|
||||
if os.getenv('ANDROID_ROOT'):
|
||||
return libc.lseek64(self.filePtr, 0, os.SEEK_CUR)
|
||||
else:
|
||||
return self.filePtr.seek(0, os.SEEK_CUR)
|
||||
|
||||
def havePiece(self, piece):
|
||||
return self.tfs.handle.have_piece(piece)
|
||||
|
||||
def pieceFromOffset(self, offset):
|
||||
piece = int((self.offset + offset) / self.piece_length)
|
||||
pieceOffset = int((self.offset + offset) % self.piece_length)
|
||||
return piece, pieceOffset
|
||||
|
||||
def waitForPiece(self, piece):
|
||||
def set_deadlines(p):
|
||||
next_piece = p + 1
|
||||
BUF_SIZE = 2 # Лучшее враг хорошего
|
||||
BUF_SIZE = 2 # Лучшее враг хорошего
|
||||
for i in range(BUF_SIZE):
|
||||
if (next_piece + i < self.endPiece and
|
||||
not self.pieces_deadlined[(next_piece + i) - self.startPiece] and not self.havePiece(next_piece + i)):
|
||||
not self.pieces_deadlined[(next_piece + i) - self.startPiece] and not self.havePiece(
|
||||
next_piece + i)):
|
||||
self.tfs.handle.set_piece_deadline(next_piece + i, 70 + (20 * i))
|
||||
self.pieces_deadlined[(next_piece + i) - self.startPiece] = True
|
||||
|
||||
if not self.havePiece(piece):
|
||||
self.log('Waiting for piece %d' % (piece,))
|
||||
self.tfs.handle.set_piece_deadline(piece, 50)
|
||||
|
@ -217,9 +237,10 @@ class TorrentFile(object):
|
|||
return False
|
||||
time.sleep(0.1)
|
||||
if not isinstance(self.pdl_thread, threading.Thread) or not self.pdl_thread.is_alive():
|
||||
self.pdl_thread = threading.Thread(target = set_deadlines, args = (piece,))
|
||||
self.pdl_thread = threading.Thread(target=set_deadlines, args=(piece,))
|
||||
self.pdl_thread.start()
|
||||
return True
|
||||
|
||||
def Close(self):
|
||||
if self.closed: return
|
||||
self.log('Closing %s...' % (self.name,))
|
||||
|
@ -231,6 +252,7 @@ class TorrentFile(object):
|
|||
else:
|
||||
self.filePtr.close()
|
||||
self.filePtr = None
|
||||
|
||||
def ShowPieces(self):
|
||||
pieces = self.tfs.handle.status().pieces
|
||||
str_ = ''
|
||||
|
@ -240,6 +262,7 @@ class TorrentFile(object):
|
|||
else:
|
||||
str_ += "#"
|
||||
self.log(str_)
|
||||
|
||||
def Read(self, buf):
|
||||
filePtr = self.__fileptr_()
|
||||
if filePtr is None:
|
||||
|
@ -250,7 +273,7 @@ class TorrentFile(object):
|
|||
readOffset = self.readOffset()
|
||||
startPiece, _ = self.pieceFromOffset(readOffset)
|
||||
endPiece, _ = self.pieceFromOffset(readOffset + toRead)
|
||||
for i in range(startPiece, endPiece + 1):
|
||||
for i in range(startPiece, endPiece + 1):
|
||||
if not self.waitForPiece(i):
|
||||
raise IOError
|
||||
if os.getenv('ANDROID_ROOT'):
|
||||
|
@ -258,6 +281,7 @@ class TorrentFile(object):
|
|||
else:
|
||||
read = filePtr.readinto(buf)
|
||||
return read
|
||||
|
||||
def Seek(self, offset, whence):
|
||||
filePtr = self.__fileptr_()
|
||||
if filePtr is None: return
|
||||
|
@ -270,21 +294,23 @@ class TorrentFile(object):
|
|||
newOffset = filePtr.seek(offset, whence)
|
||||
self.log('Seeking to %d/%d' % (newOffset, self.size))
|
||||
return newOffset
|
||||
|
||||
def IsComplete(self):
|
||||
return self.downloaded == self.size
|
||||
|
||||
|
||||
#######################################################################################
|
||||
|
||||
class TorrentFS(object):
|
||||
handle = None
|
||||
info = None
|
||||
priorities = list()
|
||||
openedFiles = list()
|
||||
lastOpenedFile = None
|
||||
shuttingDown = False
|
||||
fileCounter = int()
|
||||
progresses = list()
|
||||
save_path = None
|
||||
handle = None
|
||||
info = None
|
||||
priorities = list()
|
||||
openedFiles = list()
|
||||
lastOpenedFile = None
|
||||
shuttingDown = False
|
||||
fileCounter = int()
|
||||
progresses = list()
|
||||
save_path = None
|
||||
|
||||
def __init__(self, root, handle):
|
||||
self.root = root
|
||||
|
@ -296,8 +322,9 @@ class TorrentFS(object):
|
|||
num_files = self.info.num_files()
|
||||
for i in range(num_files):
|
||||
self.setPriority(i, 0)
|
||||
|
||||
def file(self, index):
|
||||
for name in self.files.keys():
|
||||
for name in list(self.files.keys()):
|
||||
if self.files[name].index == index:
|
||||
return self.files[name]
|
||||
file_ = self.__file_at_(index)
|
||||
|
@ -311,22 +338,27 @@ class TorrentFS(object):
|
|||
logging.info('Closing %d opened file(s)' % (len(self.openedFiles),))
|
||||
for f in self.openedFiles:
|
||||
f.Close()
|
||||
|
||||
def addOpenedFile(self, file_):
|
||||
self.openedFiles.append(file_)
|
||||
|
||||
def setPriority(self, index, priority):
|
||||
if self.priorities[index] != priority:
|
||||
logging.info('Setting %s priority to %d' % (self.info.file_at(index).path, priority))
|
||||
self.priorities[index] = priority
|
||||
self.handle.file_priority(index, priority)
|
||||
|
||||
def findOpenedFile(self, file):
|
||||
for i, f in enumerate(self.openedFiles):
|
||||
if f == file:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def removeOpenedFile(self, file):
|
||||
pos = self.findOpenedFile(file)
|
||||
if pos >= 0:
|
||||
del self.openedFiles[pos]
|
||||
|
||||
def waitForMetadata(self):
|
||||
if not self.handle.status().has_metadata:
|
||||
time.sleep(0.1)
|
||||
|
@ -334,19 +366,24 @@ class TorrentFS(object):
|
|||
self.info = self.handle.torrent_file()
|
||||
except:
|
||||
self.info = self.handle.get_torrent_info()
|
||||
|
||||
def HasTorrentInfo(self):
|
||||
return self.info is not None
|
||||
|
||||
def LoadFileProgress(self):
|
||||
self.progresses = self.handle.file_progress()
|
||||
for k in self.files.keys():
|
||||
for k in list(self.files.keys()):
|
||||
self.files[k].downloaded = self.getFileDownloadedBytes(self.files[k].index)
|
||||
if self.files[k].size > 0: self.files[k].progress = float(self.files[k].downloaded) / float(self.files[k].size)
|
||||
if self.files[k].size > 0: self.files[k].progress = float(self.files[k].downloaded) / float(
|
||||
self.files[k].size)
|
||||
|
||||
def getFileDownloadedBytes(self, i):
|
||||
try:
|
||||
bytes_ = self.progresses[i]
|
||||
except IndexError:
|
||||
bytes_ = 0
|
||||
return bytes_
|
||||
|
||||
def __files_(self):
|
||||
info = self.info
|
||||
files_ = []
|
||||
|
@ -354,29 +391,33 @@ class TorrentFS(object):
|
|||
file_ = self.__file_at_(i)
|
||||
file_.downloaded = self.getFileDownloadedBytes(i)
|
||||
if file_.size > 0:
|
||||
file_.progress = float(file_.downloaded)/float(file_.size)
|
||||
file_.progress = float(file_.downloaded) / float(file_.size)
|
||||
files_.append(file_)
|
||||
return files_
|
||||
|
||||
def __file_at_(self, index):
|
||||
info = self.info
|
||||
fileEntry = info.file_at(index)
|
||||
fe_path = fileEntry.path
|
||||
path = os.path.abspath(os.path.join(self.save_path, localize_path(fe_path)))
|
||||
return TorrentFile(
|
||||
self,
|
||||
fileEntry,
|
||||
path,
|
||||
index
|
||||
)
|
||||
self,
|
||||
fileEntry,
|
||||
path,
|
||||
index
|
||||
)
|
||||
|
||||
def FileByName(self, name):
|
||||
for i, f in enumerate(self.info.files()):
|
||||
if f.path == name:
|
||||
return self.__file_at_(i)
|
||||
raise IOError
|
||||
|
||||
def Open(self, name):
|
||||
if self.shuttingDown or not self.HasTorrentInfo():
|
||||
raise IOError
|
||||
return self.OpenFile(name)
|
||||
|
||||
def checkPriorities(self):
|
||||
for index, priority in enumerate(self.priorities):
|
||||
if priority == 0:
|
||||
|
@ -388,6 +429,7 @@ class TorrentFS(object):
|
|||
break
|
||||
if not found:
|
||||
self.setPriority(index, 0)
|
||||
|
||||
def OpenFile(self, name):
|
||||
try:
|
||||
tf = self.FileByName(name)
|
||||
|
@ -405,24 +447,27 @@ class TorrentFS(object):
|
|||
self.checkPriorities()
|
||||
return tf
|
||||
|
||||
|
||||
#############################################################
|
||||
|
||||
class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
||||
class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
def handle_error(self, *args, **kwargs):
|
||||
'''Обходим злосчастный "Broken Pipe" и прочие трейсы'''
|
||||
if not AVOID_HTTP_SERVER_EXCEPTION_OUTPUT:
|
||||
BaseHTTPServer.HTTPServer.handle_error(self, *args, **kwargs)
|
||||
http.server.HTTPServer.handle_error(self, *args, **kwargs)
|
||||
|
||||
|
||||
def HttpHandlerFactory():
|
||||
class HttpHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
class HttpHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
#print ('---Headers---\n%s\n' % (self.headers,))
|
||||
#print ('---Request---\n%s\n' % (self.path,))
|
||||
# print ('---Headers---\n%s\n' % (self.headers,))
|
||||
# print ('---Request---\n%s\n' % (self.path,))
|
||||
if self.path.startswith('/files/'):
|
||||
self.filesHandler()
|
||||
else:
|
||||
self.send_error(404, 'Not found')
|
||||
self.end_headers()
|
||||
|
||||
def filesHandler(self):
|
||||
f, start_range, end_range = self.send_head()
|
||||
if not f.closed:
|
||||
|
@ -454,15 +499,16 @@ def HttpHandlerFactory():
|
|||
total += chunk
|
||||
start_range += chunk
|
||||
f.Close()
|
||||
|
||||
def send_head(self):
|
||||
fname = urllib.unquote(self.path.lstrip('/files/'))
|
||||
fname = urllib.parse.unquote(self.path.lstrip('/files/'))
|
||||
try:
|
||||
f = self.server.root_obj.TorrentFS.Open(fname)
|
||||
f = self.server.root_obj.TorrentFS.Open(fname)
|
||||
except IOError:
|
||||
self.send_error(404, "File not found")
|
||||
return (None, 0, 0)
|
||||
_, ext = os.path.splitext(fname)
|
||||
ctype = (ext != '' and ext in VIDEO_EXTS.keys())and VIDEO_EXTS[ext] or 'application/octet-stream'
|
||||
ctype = (ext != '' and ext in list(VIDEO_EXTS.keys())) and VIDEO_EXTS[ext] or 'application/octet-stream'
|
||||
if "Range" in self.headers:
|
||||
self.send_response(206, 'Partial Content')
|
||||
else:
|
||||
|
@ -489,28 +535,32 @@ def HttpHandlerFactory():
|
|||
self.send_header("Content-Length", end_range - start_range)
|
||||
self.send_header("Last-Modified", self.date_time_string(f.fileEntry.mtime))
|
||||
self.end_headers()
|
||||
#print "Sending Bytes ",start_range, " to ", end_range, "...\n"
|
||||
return (f, start_range, end_range)
|
||||
# print "Sending Bytes ",start_range, " to ", end_range, "...\n"
|
||||
return f, start_range, end_range
|
||||
|
||||
# Вырубаем access-log
|
||||
def log_message(self, format, *args):
|
||||
def log_message(self, fmt, *args):
|
||||
return
|
||||
|
||||
return HttpHandler
|
||||
|
||||
|
||||
class Pyrrent2http(object):
|
||||
pause = False
|
||||
def __init__(self, uri = '', bindAddress = 'localhost:5001', downloadPath = '.',
|
||||
idleTimeout = -1, keepComplete = False,
|
||||
keepIncomplete = False, keepFiles = False, showAllStats = False,
|
||||
showOverallProgress = False, showFilesProgress = False,
|
||||
showPiecesProgress = False, debugAlerts = False,
|
||||
exitOnFinish = False, resumeFile = '', stateFile = '',
|
||||
userAgent = USER_AGENT, dhtRouters = '', trackers = '',
|
||||
listenPort = 6881, torrentConnectBoost = 50, connectionSpeed = 50,
|
||||
peerConnectTimeout = 15, requestTimeout = 20, maxDownloadRate = -1,
|
||||
maxUploadRate = -1, connectionsLimit = 200, encryption = 1,
|
||||
minReconnectTime = 60, maxFailCount = 3, noSparseFile = False,
|
||||
randomPort = False, enableScrape = False, enableDHT = True,
|
||||
enableLSD = True, enableUPNP = True, enableNATPMP = True, enableUTP = True, enableTCP = True, proxy=None):
|
||||
|
||||
def __init__(self, uri='', bindAddress='localhost:5001', downloadPath='.',
|
||||
idleTimeout=-1, keepComplete=False,
|
||||
keepIncomplete=False, keepFiles=False, showAllStats=False,
|
||||
showOverallProgress=False, showFilesProgress=False,
|
||||
showPiecesProgress=False, debugAlerts=False,
|
||||
exitOnFinish=False, resumeFile='', stateFile='',
|
||||
userAgent=USER_AGENT, dhtRouters='', trackers='',
|
||||
listenPort=6881, torrentConnectBoost=50, connectionSpeed=50,
|
||||
peerConnectTimeout=15, requestTimeout=20, maxDownloadRate=-1,
|
||||
maxUploadRate=-1, connectionsLimit=200, encryption=1,
|
||||
minReconnectTime=60, maxFailCount=3, noSparseFile=False,
|
||||
randomPort=False, enableScrape=False, enableDHT=True,
|
||||
enableLSD=True, enableUPNP=True, enableNATPMP=True, enableUTP=True, enableTCP=True, proxy=None):
|
||||
self.torrentHandle = None
|
||||
self.forceShutdown = False
|
||||
self.session = None
|
||||
|
@ -604,11 +654,11 @@ class Pyrrent2http(object):
|
|||
#
|
||||
self.torrentHandle.set_max_connections(60)
|
||||
if self.config.trackers != '':
|
||||
trackers = self.config.trackers.split(',')
|
||||
startTier = 256 - len(trackers)
|
||||
trackers = self.config.trackers.split(',')
|
||||
startTier = 256 - len(trackers)
|
||||
for n in range(len(trackers)):
|
||||
tracker = trackers[n].strip()
|
||||
logging.info('Adding tracker: %s' % (tracker,) )
|
||||
logging.info('Adding tracker: %s' % (tracker,))
|
||||
self.torrentHandle.add_tracker({'url': tracker})
|
||||
if self.config.enableScrape:
|
||||
logging.info('Sending scrape request to tracker')
|
||||
|
@ -623,7 +673,7 @@ class Pyrrent2http(object):
|
|||
except Exception as e:
|
||||
logging.error(e.args)
|
||||
name = self.TorrentFS.info.name()
|
||||
self.torrent_name = name.decode(chardet.detect(name)['encoding'])
|
||||
self.torrent_name = name
|
||||
|
||||
def startHTTP(self):
|
||||
logging.info('Starting HTTP Server...')
|
||||
|
@ -635,7 +685,7 @@ class Pyrrent2http(object):
|
|||
srv_port = int(strport)
|
||||
self.httpListener = ThreadingHTTPServer((host, srv_port), handler)
|
||||
self.httpListener.root_obj = self
|
||||
self.listener_thread = threading.Thread(target = self.httpListener.serve_forever)
|
||||
self.listener_thread = threading.Thread(target=self.httpListener.serve_forever)
|
||||
self.listener_thread.start()
|
||||
|
||||
def startServices(self):
|
||||
|
@ -655,7 +705,7 @@ class Pyrrent2http(object):
|
|||
def startSession(self):
|
||||
logging.info('Starting session...')
|
||||
self.session = lt.session(lt.fingerprint('LT', lt.version_major, lt.version_minor, 0, 0),
|
||||
flags=int(lt.session_flags_t.add_default_plugins))
|
||||
flags=int(lt.session_flags_t.add_default_plugins))
|
||||
alertMask = (lt.alert.category_t.error_notification |
|
||||
lt.alert.category_t.storage_notification |
|
||||
lt.alert.category_t.tracker_notification |
|
||||
|
@ -681,12 +731,12 @@ class Pyrrent2http(object):
|
|||
### Непонятно, как заставить использовать прокси только для подключения к трекеру?
|
||||
if self.config.proxy is not None:
|
||||
ps = lt.proxy_settings()
|
||||
#peer_ps = lt.proxy_settings()
|
||||
#peer_ps.type = lt.proxy_type.none
|
||||
# peer_ps = lt.proxy_settings()
|
||||
# peer_ps.type = lt.proxy_type.none
|
||||
ps.hostname = self.config.proxy['host']
|
||||
ps.port = self.config.proxy['port']
|
||||
ps.type = lt.proxy_type.socks5
|
||||
#self.session.set_peer_proxy(peer_ps)
|
||||
# self.session.set_peer_proxy(peer_ps)
|
||||
self.session.set_proxy(ps)
|
||||
settings['force_proxy'] = False
|
||||
settings['proxy_peer_connections'] = False
|
||||
|
@ -763,64 +813,68 @@ class Pyrrent2http(object):
|
|||
tstatus = self.torrentHandle.status()
|
||||
|
||||
status = {
|
||||
'name' : self.torrent_name,
|
||||
'state' : int(tstatus.state),
|
||||
'state_str' : str(tstatus.state),
|
||||
'error' : tstatus.error,
|
||||
'progress' : tstatus.progress,
|
||||
'download_rate' : tstatus.download_rate / 1024,
|
||||
'upload_rate' : tstatus.upload_rate / 1024,
|
||||
'total_download' : tstatus.total_download,
|
||||
'total_upload' : tstatus.total_upload,
|
||||
'num_peers' : tstatus.num_peers,
|
||||
'num_seeds' : tstatus.num_seeds,
|
||||
'total_seeds' : tstatus.num_complete,
|
||||
'total_peers' : tstatus.num_incomplete
|
||||
}
|
||||
'name': self.torrent_name,
|
||||
'state': int(tstatus.state),
|
||||
'state_str': str(tstatus.state),
|
||||
'error': tstatus.error,
|
||||
'progress': tstatus.progress,
|
||||
'download_rate': tstatus.download_rate // 1024,
|
||||
'upload_rate': tstatus.upload_rate // 1024,
|
||||
'total_download': tstatus.total_download,
|
||||
'total_upload': tstatus.total_upload,
|
||||
'num_peers': tstatus.num_peers,
|
||||
'num_seeds': tstatus.num_seeds,
|
||||
'total_seeds': tstatus.num_complete,
|
||||
'total_peers': tstatus.num_incomplete
|
||||
}
|
||||
return status
|
||||
|
||||
def Ls(self, index):
|
||||
fi = {}
|
||||
if self.TorrentFS.HasTorrentInfo():
|
||||
x = [n for n in self.TorrentFS.files.keys() if self.TorrentFS.files[n].index == index]
|
||||
x = [n for n in list(self.TorrentFS.files.keys()) if self.TorrentFS.files[n].index == index]
|
||||
name = x[0]
|
||||
files = self.TorrentFS.files
|
||||
Url = 'http://' + self.config.bindAddress + '/files/' + urllib.quote(name)
|
||||
Url = 'http://' + self.config.bindAddress + '/files/' + urllib.parse.quote(name)
|
||||
fi = {
|
||||
'index': files[name].index,
|
||||
'name': files[name].unicode_name,
|
||||
'media_type': files[name].media_type,
|
||||
'size': files[name].size,
|
||||
'offset': files[name].offset,
|
||||
'download': files[name].downloaded,
|
||||
'progress': files[name].progress,
|
||||
'save_path': files[name].save_path,
|
||||
'url': Url
|
||||
}
|
||||
'index': files[name].index,
|
||||
'name': files[name].unicode_name,
|
||||
'media_type': files[name].media_type,
|
||||
'size': files[name].size,
|
||||
'offset': files[name].offset,
|
||||
'download': files[name].downloaded,
|
||||
'progress': files[name].progress,
|
||||
'save_path': files[name].save_path,
|
||||
'url': Url
|
||||
}
|
||||
return fi
|
||||
|
||||
def Peers(self):
|
||||
peers = {'peers': []}
|
||||
for peer in self.torrentHandle.get_peer_info():
|
||||
if peer.flags & peer.connecting or peer.flags & peer.handshake:
|
||||
continue
|
||||
pi = {
|
||||
'Ip': peer.ip,
|
||||
'Flags': peer.flags,
|
||||
'Source': peer.source,
|
||||
'UpSpeed': peer.up_speed/1024,
|
||||
'DownSpeed': peer.down_speed/1024,
|
||||
'TotalDownload': peer.total_download,
|
||||
'TotalUpload': peer.total_upload,
|
||||
'Country': peer.country,
|
||||
'Client': peer.client
|
||||
}
|
||||
'Ip': peer.ip,
|
||||
'Flags': peer.flags,
|
||||
'Source': peer.source,
|
||||
'UpSpeed': peer.up_speed // 1024,
|
||||
'DownSpeed': peer.down_speed // 1024,
|
||||
'TotalDownload': peer.total_download,
|
||||
'TotalUpload': peer.total_upload,
|
||||
'Country': peer.country,
|
||||
'Client': peer.client
|
||||
}
|
||||
peers['peers'].append(pi)
|
||||
return peers
|
||||
|
||||
def consumeAlerts(self):
|
||||
alerts = self.session.pop_alerts()
|
||||
for alert in alerts:
|
||||
if type(alert) == lt.save_resume_data_alert:
|
||||
self.processSaveResumeDataAlert(alert)
|
||||
break
|
||||
|
||||
def waitForAlert(self, alert_type, timeout):
|
||||
start = time.time()
|
||||
while True:
|
||||
|
@ -831,6 +885,7 @@ class Pyrrent2http(object):
|
|||
alert = self.session.pop_alert()
|
||||
if type(alert) == alert_type:
|
||||
return alert
|
||||
|
||||
def loop(self):
|
||||
self.saveResumeDataTicker = Ticker(5)
|
||||
time_start = time.time()
|
||||
|
@ -859,16 +914,18 @@ class Pyrrent2http(object):
|
|||
except IOError as e:
|
||||
strerror = e.args
|
||||
logging.error(strerror)
|
||||
def saveResumeData(self, async = False):
|
||||
|
||||
def saveResumeData(self, async_=False):
|
||||
if not self.torrentHandle.status().need_save_resume or self.config.resumeFile == '':
|
||||
return False
|
||||
self.torrentHandle.save_resume_data(lt.save_resume_flags_t.flush_disk_cache)
|
||||
if not async:
|
||||
if not async_:
|
||||
alert = self.waitForAlert(lt.save_resume_data_alert, 5)
|
||||
if alert == None:
|
||||
if alert is None:
|
||||
return False
|
||||
self.processSaveResumeDataAlert(alert)
|
||||
return True
|
||||
|
||||
def saveSessionState(self):
|
||||
if self.config.stateFile == '':
|
||||
return
|
||||
|
@ -882,6 +939,7 @@ class Pyrrent2http(object):
|
|||
except IOError as e:
|
||||
strerror = e.args
|
||||
logging.error(strerror)
|
||||
|
||||
def removeFiles(self, files):
|
||||
for file in files:
|
||||
try:
|
||||
|
@ -897,6 +955,7 @@ class Pyrrent2http(object):
|
|||
os.remove(path)
|
||||
path_ = os.path.dirname(path)
|
||||
path = path_[-1] == os.path.sep and path_[:-1] or path_
|
||||
|
||||
def filesToRemove(self):
|
||||
files = []
|
||||
if self.TorrentFS.HasTorrentInfo():
|
||||
|
@ -907,6 +966,7 @@ class Pyrrent2http(object):
|
|||
if os.path.exists(path):
|
||||
files.append(path)
|
||||
return files
|
||||
|
||||
def removeTorrent(self):
|
||||
files = []
|
||||
flag = 0
|
||||
|
@ -922,6 +982,7 @@ class Pyrrent2http(object):
|
|||
logging.info('Waiting for files to be removed')
|
||||
self.waitForAlert(lt.torrent_deleted_alert, 15)
|
||||
self.removeFiles(files)
|
||||
|
||||
def shutdown(self):
|
||||
logging.info('Stopping pyrrent2http...')
|
||||
self.forceShutdown = True
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
class State:
|
||||
QUEUED_FOR_CHECKING = 0
|
||||
CHECKING_FILES = 1
|
||||
DOWNLOADING_METADATA = 2
|
||||
DOWNLOADING = 3
|
||||
FINISHED = 4
|
||||
SEEDING = 5
|
||||
ALLOCATING = 6
|
||||
CHECKING_RESUME_DATA = 7
|
||||
|
||||
|
||||
class MediaType:
|
||||
UNKNOWN = None
|
||||
AUDIO = 'audio'
|
||||
VIDEO = 'video'
|
||||
SUBTITLES = 'subtitles'
|
||||
|
||||
|
||||
class Encryption:
|
||||
FORCED = 0
|
||||
ENABLED = 1
|
||||
DISABLED = 2
|
|
@ -1,31 +1,41 @@
|
|||
import sys
|
||||
import socket
|
||||
import chardet
|
||||
import os
|
||||
from . import MediaType
|
||||
import mimetypes
|
||||
import urlparse, urllib
|
||||
import os
|
||||
import socket
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import chardet
|
||||
import sys
|
||||
import xbmc
|
||||
|
||||
from .structs import MediaType
|
||||
|
||||
SUBTITLES_FORMATS = ['.aqt', '.gsub', '.jss', '.sub', '.ttxt', '.pjs', '.psb', '.rt', '.smi', '.stl',
|
||||
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
|
||||
'.ssf', '.srt', '.ssa', '.ass', '.usf', '.idx']
|
||||
|
||||
|
||||
class Struct(dict):
|
||||
def __getattr__(self, attr):
|
||||
return self[attr]
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
self[attr] = value
|
||||
|
||||
|
||||
def uri2path(uri):
|
||||
if uri[1] == ':' and sys.platform.startswith('win'):
|
||||
uri = 'file:///' + uri
|
||||
fileUri = urlparse.urlparse(uri)
|
||||
fileUri = urllib.parse.urlparse(uri)
|
||||
if fileUri.scheme == 'file':
|
||||
uriPath = fileUri.path
|
||||
if uriPath != '' and sys.platform.startswith('win') and (os.path.sep == uriPath[0] or uriPath[0] == '/'):
|
||||
uriPath = uriPath[1:]
|
||||
absPath = os.path.abspath(urllib.unquote(uriPath))
|
||||
absPath = os.path.abspath(urllib.parse.unquote(uriPath))
|
||||
return localize_path(absPath)
|
||||
|
||||
|
||||
def detect_media_type(name):
|
||||
ext = os.path.splitext(name)[1]
|
||||
if ext in SUBTITLES_FORMATS:
|
||||
|
@ -41,24 +51,31 @@ def detect_media_type(name):
|
|||
return MediaType.VIDEO
|
||||
else:
|
||||
return MediaType.UNKNOWN
|
||||
|
||||
|
||||
def unicode_msg(tmpl, args):
|
||||
msg = isinstance(tmpl, unicode) and tmpl or tmpl.decode(chardet.detect(tmpl)['encoding'])
|
||||
msg = isinstance(tmpl, str) and tmpl or tmpl.decode(chardet.detect(tmpl)['encoding'])
|
||||
arg_ = []
|
||||
for a in args:
|
||||
arg_.append(isinstance(a, unicode) and a or a.decode(chardet.detect(a)['encoding']))
|
||||
arg_.append(isinstance(a, str) and a or a.decode(chardet.detect(a)['encoding']))
|
||||
return msg % tuple(arg_)
|
||||
|
||||
|
||||
def encode_msg(msg):
|
||||
msg = isinstance(msg, unicode) and msg.encode(sys.getfilesystemencoding() != 'ascii' and sys.getfilesystemencoding() or 'utf-8') or msg
|
||||
msg = isinstance(msg, str) and msg.encode(
|
||||
sys.getfilesystemencoding() != 'ascii' and sys.getfilesystemencoding() or 'utf-8') or msg
|
||||
return msg
|
||||
|
||||
|
||||
def localize_path(path):
|
||||
if not isinstance(path, unicode): path = path.decode(chardet.detect(path)['encoding'])
|
||||
if not sys.platform.startswith('win'):
|
||||
path = path.encode((sys.getfilesystemencoding() not in ('ascii', 'ANSI_X3.4-1968')) and sys.getfilesystemencoding() or 'utf-8')
|
||||
if isinstance(path, bytes):
|
||||
path = path.decode(chardet.detect(path)['encoding'])
|
||||
# if not sys.platform.startswith('win'):
|
||||
# path = path.encode(
|
||||
# (sys.getfilesystemencoding() not in ('ascii', 'ANSI_X3.4-1968')) and sys.getfilesystemencoding() or 'utf-8')
|
||||
return path
|
||||
|
||||
|
||||
def can_bind(host, port):
|
||||
"""
|
||||
Checks we can bind to specified host and port
|
||||
|
@ -94,6 +111,6 @@ def find_free_port(host):
|
|||
|
||||
|
||||
def ensure_fs_encoding(string):
|
||||
if isinstance(string, str):
|
||||
if isinstance(string, bytes):
|
||||
string = string.decode('utf-8')
|
||||
return string.encode(sys.getfilesystemencoding() or 'utf-8')
|
||||
|
|
Loading…
Reference in New Issue