Версия для Kodi 19

master v1.1.0
Роман Бородин 2022-03-14 09:10:42 +03:00
parent f39749807f
commit 6e04f3fc14
7 changed files with 304 additions and 834 deletions

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?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> <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.libtorrent" />
<import addon="script.module.chardet" /> <import addon="script.module.chardet" />
</requires> </requires>
@ -17,5 +17,5 @@
<description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description> <description lang="ru">Обеспечивает последовательную (sequential) загрузку торрентов для потокового онлайн просмотра через HTTP. Основан на библиотеке LibTorrent.</description>
<description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description> <description lang="en">Provides sequential torrent downloading for online streaming video and other media over HTTP.</description>
<email>inpos@yandex.ru</email> <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> </addon>

View File

@ -3,33 +3,6 @@
from collections import namedtuple 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, " SessionStatus = namedtuple('SessionStatus', "name, state, state_str, error, progress, download_rate, upload_rate, "
"total_download, total_upload, num_peers, num_seeds, total_seeds, " "total_download, total_upload, num_peers, num_seeds, total_seeds, "
"total_peers") "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, " PeerInfo = namedtuple('PeerInfo', "ip, flags, source, up_speed, down_speed, total_upload, total_download, "
"country, client") "country, client")
from engine import Engine from .engine import Engine
from error import Error from .error import Error

View File

@ -1,31 +1,37 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import threading
import urllib.error
import urllib.parse
import urllib.request
import chardet
import sys import sys
import time import time
import pyrrent2http
import xbmc import xbmc
from error import Error
from . import SessionStatus, FileStatus, PeerInfo, Encryption from . import SessionStatus, FileStatus, PeerInfo
from util import can_bind, find_free_port, localize_path, uri2path, detect_media_type from . import pyrrent2http
import threading from .error import Error
import urllib from .structs import Encryption
import chardet from .util import can_bind, find_free_port, localize_path, uri2path, detect_media_type
LOGGING = True LOGGING = True
class Engine: class Engine:
""" """
This is python binding class to pyrrent2http client. This is python binding class to pyrrent2http client.
""" """
def _log(self, message): def _log(self, message):
if self.logger: if self.logger:
self.logger(message) self.logger(message)
else: else:
xbmc.log("[pyrrent2http] %s" % message) xbmc.log("[pyrrent2http] %s" % message)
def __init__(self, uri=None, platform=None, download_path=".", 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, 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, 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, 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, 'maxFailCount': self.max_failcount,
'showPiecesProgress': self.log_pieces_progress, 'showPiecesProgress': self.log_pieces_progress,
'idleTimeout': self.max_idle_timeout, 'idleTimeout': self.max_idle_timeout,
#'fileIndex': start_index, # 'fileIndex': start_index,
'connectionsLimit': self.connections_limit, 'connectionsLimit': self.connections_limit,
'enableScrape': self.enable_scrape, 'enableScrape': self.enable_scrape,
'enableUTP': self.enable_utp, 'enableUTP': self.enable_utp,
@ -208,25 +214,28 @@ class Engine:
} }
self._log("Invoking pyrrent2http") self._log("Invoking pyrrent2http")
class Logging(object): class Logging(object):
def __init__(self, _log): def __init__(self, _log):
self._log = _log self._log = _log
def info(self, message): def info(self, message):
if LOGGING: if LOGGING:
self._log('INFO: %s' % (message,)) self._log('INFO: %s' % (message,))
def error(self, message): def error(self, message):
if LOGGING: if LOGGING:
self._log('ERROR: %s' % (message,)) self._log('ERROR: %s' % (message,))
pyrrent2http.logging = Logging(self._log) pyrrent2http.logging = Logging(self._log)
self.pyrrent2http = pyrrent2http.Pyrrent2http(**kwargs) self.pyrrent2http = pyrrent2http.Pyrrent2http(**kwargs)
self.pyrrent2http.startSession() self.pyrrent2http.startSession()
self.pyrrent2http.startServices() self.pyrrent2http.startServices()
self.pyrrent2http.addTorrent() self.pyrrent2http.addTorrent()
self.pyrrent2http.startHTTP() 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() self.pyrrent2http_loop.start()
start = time.time() start = time.time()
self.started = True self.started = True
@ -236,7 +245,7 @@ class Engine:
if not self.is_alive(): if not self.is_alive():
raise Error("Can't start pyrrent2http, see log for details", Error.PROCESS_ERROR) raise Error("Can't start pyrrent2http, see log for details", Error.PROCESS_ERROR)
try: try:
#self.status(1) # self.status(1)
initialized = True initialized = True
break break
except Error: except Error:
@ -249,9 +258,10 @@ class Engine:
def activate_file(self, index): def activate_file(self, index):
self.pyrrent2http.TorrentFS.file(index) self.pyrrent2http.TorrentFS.file(index)
def pause(self): def pause(self):
self.pyrrent2http.pause = True self.pyrrent2http.pause = True
def resume(self): def resume(self):
self.pyrrent2http.pause = False self.pyrrent2http.pause = False
@ -279,8 +289,6 @@ class Engine:
status = SessionStatus(**status) status = SessionStatus(**status)
return status return status
def list(self, media_types=None, timeout=10): def list(self, media_types=None, timeout=10):
""" """
Returns list of files in the torrent (see FileStatus named tuple). Returns list of files in the torrent (see FileStatus named tuple).
@ -296,8 +304,9 @@ class Engine:
if files: if files:
res = [FileStatus(index=index, **f) for index, f in enumerate(files)] res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
if media_types is not None: 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 return res
def list_from_info(self, media_types=None): def list_from_info(self, media_types=None):
try: try:
info = pyrrent2http.lt.torrent_info(uri2path(self.uri)) info = pyrrent2http.lt.torrent_info(uri2path(self.uri))
@ -306,21 +315,21 @@ class Engine:
files = [] files = []
for i in range(info.num_files()): for i in range(info.num_files()):
f = info.file_at(i) 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({ files.append({
'name': localize_path(f.path), 'name': localize_path(f.path),
'size': f.size, 'size': f.size,
'offset': f.offset, 'offset': f.offset,
'media_type': media_types and detect_media_type(f.path.decode(chardet.detect(f.path)['encoding'])) or '', 'media_type': media_types is not None and detect_media_type(f.path) or '',
'download': 0, 'download': 0,
'progress': 0.0, 'progress': 0.0,
'save_path': '', 'save_path': '',
'url': Url 'url': Url
}) })
if files: if len(files) > 0:
res = [FileStatus(index=index, **f) for index, f in enumerate(files)] res = [FileStatus(index=index, **f) for index, f in enumerate(files)]
if media_types is not None: 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 return res
def file_status(self, file_index, timeout=10): def file_status(self, file_index, timeout=10):
@ -339,7 +348,7 @@ class Engine:
return FileStatus(**filestatus) return FileStatus(**filestatus)
except: except:
raise Error("Requested file index (%d) is invalid" % (file_index,), Error.INVALID_FILE_INDEX, 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): def peers(self, timeout=10):
""" """
@ -355,6 +364,7 @@ class Engine:
def is_alive(self): def is_alive(self):
return self.pyrrent2http_loop.is_alive() return self.pyrrent2http_loop.is_alive()
def wait_on_close(self, wait_timeout=10): def wait_on_close(self, wait_timeout=10):
""" """
By default, close() method sends shutdown command to pyrrent2http, stops logging and returns immediately, not By default, close() method sends shutdown command to pyrrent2http, stops logging and returns immediately, not

View File

@ -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

View File

@ -4,12 +4,13 @@ import chardet
try: try:
from python_libtorrent import get_libtorrent # @UnresolvedImport from python_libtorrent import get_libtorrent # @UnresolvedImport
lt=get_libtorrent()
print('Imported libtorrent v%s from python_libtorrent' %(lt.version, )) lt = get_libtorrent()
except Exception, e: print(('Imported libtorrent v%s from python_libtorrent' % (lt.version,)))
print('Error importing python_libtorrent.Exception: %s' %(str(e),)) except Exception as e:
print(('Error importing python_libtorrent.Exception: %s' % (str(e),)))
try: try:
import libtorrent as lt # @UnresolvedImport import libtorrent as lt # @UnresolvedImport
except Exception as e: except Exception as e:
strerror = e.args strerror = e.args
print(strerror) print(strerror)
@ -17,17 +18,16 @@ except Exception, e:
from random import SystemRandom from random import SystemRandom
import time import time
import urllib import urllib.request, urllib.parse, urllib.error
import BaseHTTPServer import http.server
import SocketServer import socketserver
import threading import threading
import io 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'): if os.getenv('ANDROID_ROOT'):
from ctypes import * from ctypes import *
libc = CDLL('/system/lib/libc.so') libc = CDLL('/system/lib/libc.so')
libc.lseek64.restype = c_ulonglong libc.lseek64.restype = c_ulonglong
libc.lseek64.argtypes = [c_uint, c_ulonglong, c_uint] libc.lseek64.argtypes = [c_uint, c_ulonglong, c_uint]
@ -41,15 +41,16 @@ if os.getenv('ANDROID_ROOT'):
if not hasattr(os, 'getppid'): if not hasattr(os, 'getppid'):
import ctypes import ctypes
TH32CS_SNAPPROCESS = 0x02L TH32CS_SNAPPROCESS = 0x02
CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot # @UndefinedVariable CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot # @UndefinedVariable
GetCurrentProcessId = ctypes.windll.kernel32.GetCurrentProcessId # @UndefinedVariable GetCurrentProcessId = ctypes.windll.kernel32.GetCurrentProcessId # @UndefinedVariable
MAX_PATH = 260 MAX_PATH = 260
_kernel32dll = ctypes.windll.Kernel32 _kernel32dll = ctypes.windll.Kernel32
CloseHandle = _kernel32dll.CloseHandle CloseHandle = _kernel32dll.CloseHandle
class PROCESSENTRY32(ctypes.Structure): class PROCESSENTRY32(ctypes.Structure):
_fields_ = [ _fields_ = [
("dwSize", ctypes.c_ulong), ("dwSize", ctypes.c_ulong),
@ -65,9 +66,11 @@ if not hasattr(os, 'getppid'):
("szExeFile", ctypes.c_wchar * MAX_PATH) ("szExeFile", ctypes.c_wchar * MAX_PATH)
] ]
Process32First = _kernel32dll.Process32FirstW Process32First = _kernel32dll.Process32FirstW
Process32Next = _kernel32dll.Process32NextW Process32Next = _kernel32dll.Process32NextW
def getppid(): def getppid():
''' '''
:return: The pid of the parent of this process. :return: The pid of the parent of this process.
@ -93,27 +96,31 @@ if not hasattr(os, 'getppid'):
return result return result
os.getppid = getppid os.getppid = getppid
################################################################################# #################################################################################
AVOID_HTTP_SERVER_EXCEPTION_OUTPUT = True AVOID_HTTP_SERVER_EXCEPTION_OUTPUT = True
VERSION = "0.6.0" 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' USER_AGENT = 'libtorrent/1.0.9.0'
VIDEO_EXTS={'.avi':'video/x-msvideo','.mp4':'video/mp4','.mkv':'video/x-matroska', 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', '.m4v': 'video/mp4', '.mov': 'video/quicktime', '.mpg': 'video/mpeg', '.ogv': 'video/ogg',
'.ogg':'video/ogg', '.webm':'video/webm', '.ts': 'video/mp2t', '.3gp':'video/3gpp'} '.ogg': 'video/ogg', '.webm': 'video/webm', '.ts': 'video/mp2t', '.3gp': 'video/3gpp'}
###################################################################################### ######################################################################################
class Ticker(object): class Ticker(object):
def __init__(self, interval): def __init__(self, interval):
self.tick = False self.tick = False
self._timer = None self._timer = None
self.interval = interval self.interval = interval
self.is_running = False self.is_running = False
self.start() self.start()
@property @property
def true(self): def true(self):
if self.tick: if self.tick:
@ -137,23 +144,25 @@ class Ticker(object):
self._timer.cancel() self._timer.cancel()
self.is_running = False self.is_running = False
####################################################################################### #######################################################################################
class TorrentFile(object): class TorrentFile(object):
tfs = None tfs = None
closed = True closed = True
save_path = str() save_path = str()
fileEntry = None fileEntry = None
index = 0 index = 0
filePtr = None filePtr = None
downloaded = 0 downloaded = 0
progress = 0.0 progress = 0.0
pdl_thread = None pdl_thread = None
def __init__(self, tfs, fileEntry, savePath, index): def __init__(self, tfs, fileEntry, savePath, index):
self.tfs = tfs self.tfs = tfs
self.fileEntry = fileEntry self.fileEntry = fileEntry
self.name = self.fileEntry.path 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.media_type = detect_media_type(self.unicode_name)
self.save_path = savePath self.save_path = savePath
self.index = index self.index = index
@ -162,11 +171,13 @@ class TorrentFile(object):
self.offset = self.fileEntry.offset self.offset = self.fileEntry.offset
self.startPiece, self.endPiece = self.Pieces() self.startPiece, self.endPiece = self.Pieces()
self.pieces_deadlined = [False] * (self.endPiece - self.startPiece) self.pieces_deadlined = [False] * (self.endPiece - self.startPiece)
def Downloaded(self): def Downloaded(self):
return self.downloaded return self.downloaded
def Progress(self): def Progress(self):
return self.progress return self.progress
def __fileptr_(self): def __fileptr_(self):
if self.closed: if self.closed:
return None return None
@ -180,35 +191,44 @@ class TorrentFile(object):
else: else:
self.filePtr = io.open(self.save_path, 'rb') self.filePtr = io.open(self.save_path, 'rb')
return self.filePtr return self.filePtr
def log(self, message): def log(self, message):
fnum = self.tfs.openedFiles.index(self) fnum = self.tfs.openedFiles.index(self)
logging.info("[Thread No.%d] %s\n" % (fnum, message)) logging.info("[Thread No.%d] %s\n" % (fnum, message))
def Pieces(self): def Pieces(self):
startPiece, _ = self.pieceFromOffset(1) startPiece, _ = self.pieceFromOffset(1)
endPiece, _ = self.pieceFromOffset(self.size - 1) endPiece, _ = self.pieceFromOffset(self.size - 1)
return startPiece, endPiece return startPiece, endPiece
def SetPriority(self, priority): def SetPriority(self, priority):
self.tfs.setPriority(self.index, priority) self.tfs.setPriority(self.index, priority)
def readOffset(self): def readOffset(self):
if os.getenv('ANDROID_ROOT'): if os.getenv('ANDROID_ROOT'):
return libc.lseek64(self.filePtr, 0, os.SEEK_CUR) return libc.lseek64(self.filePtr, 0, os.SEEK_CUR)
else: else:
return self.filePtr.seek(0, os.SEEK_CUR) return self.filePtr.seek(0, os.SEEK_CUR)
def havePiece(self, piece): def havePiece(self, piece):
return self.tfs.handle.have_piece(piece) return self.tfs.handle.have_piece(piece)
def pieceFromOffset(self, offset): def pieceFromOffset(self, offset):
piece = int((self.offset + offset) / self.piece_length) piece = int((self.offset + offset) / self.piece_length)
pieceOffset = int((self.offset + offset) % self.piece_length) pieceOffset = int((self.offset + offset) % self.piece_length)
return piece, pieceOffset return piece, pieceOffset
def waitForPiece(self, piece): def waitForPiece(self, piece):
def set_deadlines(p): def set_deadlines(p):
next_piece = p + 1 next_piece = p + 1
BUF_SIZE = 2 # Лучшее враг хорошего BUF_SIZE = 2 # Лучшее враг хорошего
for i in range(BUF_SIZE): for i in range(BUF_SIZE):
if (next_piece + i < self.endPiece and 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.tfs.handle.set_piece_deadline(next_piece + i, 70 + (20 * i))
self.pieces_deadlined[(next_piece + i) - self.startPiece] = True self.pieces_deadlined[(next_piece + i) - self.startPiece] = True
if not self.havePiece(piece): if not self.havePiece(piece):
self.log('Waiting for piece %d' % (piece,)) self.log('Waiting for piece %d' % (piece,))
self.tfs.handle.set_piece_deadline(piece, 50) self.tfs.handle.set_piece_deadline(piece, 50)
@ -217,9 +237,10 @@ class TorrentFile(object):
return False return False
time.sleep(0.1) time.sleep(0.1)
if not isinstance(self.pdl_thread, threading.Thread) or not self.pdl_thread.is_alive(): 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() self.pdl_thread.start()
return True return True
def Close(self): def Close(self):
if self.closed: return if self.closed: return
self.log('Closing %s...' % (self.name,)) self.log('Closing %s...' % (self.name,))
@ -231,6 +252,7 @@ class TorrentFile(object):
else: else:
self.filePtr.close() self.filePtr.close()
self.filePtr = None self.filePtr = None
def ShowPieces(self): def ShowPieces(self):
pieces = self.tfs.handle.status().pieces pieces = self.tfs.handle.status().pieces
str_ = '' str_ = ''
@ -240,6 +262,7 @@ class TorrentFile(object):
else: else:
str_ += "#" str_ += "#"
self.log(str_) self.log(str_)
def Read(self, buf): def Read(self, buf):
filePtr = self.__fileptr_() filePtr = self.__fileptr_()
if filePtr is None: if filePtr is None:
@ -250,7 +273,7 @@ class TorrentFile(object):
readOffset = self.readOffset() readOffset = self.readOffset()
startPiece, _ = self.pieceFromOffset(readOffset) startPiece, _ = self.pieceFromOffset(readOffset)
endPiece, _ = self.pieceFromOffset(readOffset + toRead) endPiece, _ = self.pieceFromOffset(readOffset + toRead)
for i in range(startPiece, endPiece + 1): for i in range(startPiece, endPiece + 1):
if not self.waitForPiece(i): if not self.waitForPiece(i):
raise IOError raise IOError
if os.getenv('ANDROID_ROOT'): if os.getenv('ANDROID_ROOT'):
@ -258,6 +281,7 @@ class TorrentFile(object):
else: else:
read = filePtr.readinto(buf) read = filePtr.readinto(buf)
return read return read
def Seek(self, offset, whence): def Seek(self, offset, whence):
filePtr = self.__fileptr_() filePtr = self.__fileptr_()
if filePtr is None: return if filePtr is None: return
@ -270,21 +294,23 @@ class TorrentFile(object):
newOffset = filePtr.seek(offset, whence) newOffset = filePtr.seek(offset, whence)
self.log('Seeking to %d/%d' % (newOffset, self.size)) self.log('Seeking to %d/%d' % (newOffset, self.size))
return newOffset return newOffset
def IsComplete(self): def IsComplete(self):
return self.downloaded == self.size return self.downloaded == self.size
####################################################################################### #######################################################################################
class TorrentFS(object): class TorrentFS(object):
handle = None handle = None
info = None info = None
priorities = list() priorities = list()
openedFiles = list() openedFiles = list()
lastOpenedFile = None lastOpenedFile = None
shuttingDown = False shuttingDown = False
fileCounter = int() fileCounter = int()
progresses = list() progresses = list()
save_path = None save_path = None
def __init__(self, root, handle): def __init__(self, root, handle):
self.root = root self.root = root
@ -296,8 +322,9 @@ class TorrentFS(object):
num_files = self.info.num_files() num_files = self.info.num_files()
for i in range(num_files): for i in range(num_files):
self.setPriority(i, 0) self.setPriority(i, 0)
def file(self, index): def file(self, index):
for name in self.files.keys(): for name in list(self.files.keys()):
if self.files[name].index == index: if self.files[name].index == index:
return self.files[name] return self.files[name]
file_ = self.__file_at_(index) file_ = self.__file_at_(index)
@ -311,22 +338,27 @@ class TorrentFS(object):
logging.info('Closing %d opened file(s)' % (len(self.openedFiles),)) logging.info('Closing %d opened file(s)' % (len(self.openedFiles),))
for f in self.openedFiles: for f in self.openedFiles:
f.Close() f.Close()
def addOpenedFile(self, file_): def addOpenedFile(self, file_):
self.openedFiles.append(file_) self.openedFiles.append(file_)
def setPriority(self, index, priority): def setPriority(self, index, priority):
if self.priorities[index] != priority: if self.priorities[index] != priority:
logging.info('Setting %s priority to %d' % (self.info.file_at(index).path, priority)) logging.info('Setting %s priority to %d' % (self.info.file_at(index).path, priority))
self.priorities[index] = priority self.priorities[index] = priority
self.handle.file_priority(index, priority) self.handle.file_priority(index, priority)
def findOpenedFile(self, file): def findOpenedFile(self, file):
for i, f in enumerate(self.openedFiles): for i, f in enumerate(self.openedFiles):
if f == file: if f == file:
return i return i
return -1 return -1
def removeOpenedFile(self, file): def removeOpenedFile(self, file):
pos = self.findOpenedFile(file) pos = self.findOpenedFile(file)
if pos >= 0: if pos >= 0:
del self.openedFiles[pos] del self.openedFiles[pos]
def waitForMetadata(self): def waitForMetadata(self):
if not self.handle.status().has_metadata: if not self.handle.status().has_metadata:
time.sleep(0.1) time.sleep(0.1)
@ -334,19 +366,24 @@ class TorrentFS(object):
self.info = self.handle.torrent_file() self.info = self.handle.torrent_file()
except: except:
self.info = self.handle.get_torrent_info() self.info = self.handle.get_torrent_info()
def HasTorrentInfo(self): def HasTorrentInfo(self):
return self.info is not None return self.info is not None
def LoadFileProgress(self): def LoadFileProgress(self):
self.progresses = self.handle.file_progress() 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) 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): def getFileDownloadedBytes(self, i):
try: try:
bytes_ = self.progresses[i] bytes_ = self.progresses[i]
except IndexError: except IndexError:
bytes_ = 0 bytes_ = 0
return bytes_ return bytes_
def __files_(self): def __files_(self):
info = self.info info = self.info
files_ = [] files_ = []
@ -354,29 +391,33 @@ class TorrentFS(object):
file_ = self.__file_at_(i) file_ = self.__file_at_(i)
file_.downloaded = self.getFileDownloadedBytes(i) file_.downloaded = self.getFileDownloadedBytes(i)
if file_.size > 0: if file_.size > 0:
file_.progress = float(file_.downloaded)/float(file_.size) file_.progress = float(file_.downloaded) / float(file_.size)
files_.append(file_) files_.append(file_)
return files_ return files_
def __file_at_(self, index): def __file_at_(self, index):
info = self.info info = self.info
fileEntry = info.file_at(index) fileEntry = info.file_at(index)
fe_path = fileEntry.path fe_path = fileEntry.path
path = os.path.abspath(os.path.join(self.save_path, localize_path(fe_path))) path = os.path.abspath(os.path.join(self.save_path, localize_path(fe_path)))
return TorrentFile( return TorrentFile(
self, self,
fileEntry, fileEntry,
path, path,
index index
) )
def FileByName(self, name): def FileByName(self, name):
for i, f in enumerate(self.info.files()): for i, f in enumerate(self.info.files()):
if f.path == name: if f.path == name:
return self.__file_at_(i) return self.__file_at_(i)
raise IOError raise IOError
def Open(self, name): def Open(self, name):
if self.shuttingDown or not self.HasTorrentInfo(): if self.shuttingDown or not self.HasTorrentInfo():
raise IOError raise IOError
return self.OpenFile(name) return self.OpenFile(name)
def checkPriorities(self): def checkPriorities(self):
for index, priority in enumerate(self.priorities): for index, priority in enumerate(self.priorities):
if priority == 0: if priority == 0:
@ -388,6 +429,7 @@ class TorrentFS(object):
break break
if not found: if not found:
self.setPriority(index, 0) self.setPriority(index, 0)
def OpenFile(self, name): def OpenFile(self, name):
try: try:
tf = self.FileByName(name) tf = self.FileByName(name)
@ -404,25 +446,28 @@ class TorrentFS(object):
self.files[tf.name] = tf self.files[tf.name] = tf
self.checkPriorities() self.checkPriorities()
return tf return tf
############################################################# #############################################################
class ThreadingHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): class ThreadingHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
def handle_error(self, *args, **kwargs): def handle_error(self, *args, **kwargs):
'''Обходим злосчастный "Broken Pipe" и прочие трейсы''' '''Обходим злосчастный "Broken Pipe" и прочие трейсы'''
if not AVOID_HTTP_SERVER_EXCEPTION_OUTPUT: if not AVOID_HTTP_SERVER_EXCEPTION_OUTPUT:
BaseHTTPServer.HTTPServer.handle_error(self, *args, **kwargs) http.server.HTTPServer.handle_error(self, *args, **kwargs)
def HttpHandlerFactory(): def HttpHandlerFactory():
class HttpHandler(BaseHTTPServer.BaseHTTPRequestHandler): class HttpHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
#print ('---Headers---\n%s\n' % (self.headers,)) # print ('---Headers---\n%s\n' % (self.headers,))
#print ('---Request---\n%s\n' % (self.path,)) # print ('---Request---\n%s\n' % (self.path,))
if self.path.startswith('/files/'): if self.path.startswith('/files/'):
self.filesHandler() self.filesHandler()
else: else:
self.send_error(404, 'Not found') self.send_error(404, 'Not found')
self.end_headers() self.end_headers()
def filesHandler(self): def filesHandler(self):
f, start_range, end_range = self.send_head() f, start_range, end_range = self.send_head()
if not f.closed: if not f.closed:
@ -454,15 +499,16 @@ def HttpHandlerFactory():
total += chunk total += chunk
start_range += chunk start_range += chunk
f.Close() f.Close()
def send_head(self): def send_head(self):
fname = urllib.unquote(self.path.lstrip('/files/')) fname = urllib.parse.unquote(self.path.lstrip('/files/'))
try: try:
f = self.server.root_obj.TorrentFS.Open(fname) f = self.server.root_obj.TorrentFS.Open(fname)
except IOError: except IOError:
self.send_error(404, "File not found") self.send_error(404, "File not found")
return (None, 0, 0) return (None, 0, 0)
_, ext = os.path.splitext(fname) _, 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: if "Range" in self.headers:
self.send_response(206, 'Partial Content') self.send_response(206, 'Partial Content')
else: else:
@ -489,28 +535,32 @@ def HttpHandlerFactory():
self.send_header("Content-Length", end_range - start_range) self.send_header("Content-Length", end_range - start_range)
self.send_header("Last-Modified", self.date_time_string(f.fileEntry.mtime)) self.send_header("Last-Modified", self.date_time_string(f.fileEntry.mtime))
self.end_headers() self.end_headers()
#print "Sending Bytes ",start_range, " to ", end_range, "...\n" # print "Sending Bytes ",start_range, " to ", end_range, "...\n"
return (f, start_range, end_range) return f, start_range, end_range
# Вырубаем access-log # Вырубаем access-log
def log_message(self, format, *args): def log_message(self, fmt, *args):
return return
return HttpHandler return HttpHandler
class Pyrrent2http(object): class Pyrrent2http(object):
pause = False pause = False
def __init__(self, uri = '', bindAddress = 'localhost:5001', downloadPath = '.',
idleTimeout = -1, keepComplete = False, def __init__(self, uri='', bindAddress='localhost:5001', downloadPath='.',
keepIncomplete = False, keepFiles = False, showAllStats = False, idleTimeout=-1, keepComplete=False,
showOverallProgress = False, showFilesProgress = False, keepIncomplete=False, keepFiles=False, showAllStats=False,
showPiecesProgress = False, debugAlerts = False, showOverallProgress=False, showFilesProgress=False,
exitOnFinish = False, resumeFile = '', stateFile = '', showPiecesProgress=False, debugAlerts=False,
userAgent = USER_AGENT, dhtRouters = '', trackers = '', exitOnFinish=False, resumeFile='', stateFile='',
listenPort = 6881, torrentConnectBoost = 50, connectionSpeed = 50, userAgent=USER_AGENT, dhtRouters='', trackers='',
peerConnectTimeout = 15, requestTimeout = 20, maxDownloadRate = -1, listenPort=6881, torrentConnectBoost=50, connectionSpeed=50,
maxUploadRate = -1, connectionsLimit = 200, encryption = 1, peerConnectTimeout=15, requestTimeout=20, maxDownloadRate=-1,
minReconnectTime = 60, maxFailCount = 3, noSparseFile = False, maxUploadRate=-1, connectionsLimit=200, encryption=1,
randomPort = False, enableScrape = False, enableDHT = True, minReconnectTime=60, maxFailCount=3, noSparseFile=False,
enableLSD = True, enableUPNP = True, enableNATPMP = True, enableUTP = True, enableTCP = True, proxy=None): randomPort=False, enableScrape=False, enableDHT=True,
enableLSD=True, enableUPNP=True, enableNATPMP=True, enableUTP=True, enableTCP=True, proxy=None):
self.torrentHandle = None self.torrentHandle = None
self.forceShutdown = False self.forceShutdown = False
self.session = None self.session = None
@ -563,7 +613,7 @@ class Pyrrent2http(object):
if self.config.resumeFile is None: self.config.resumeFile = '' if self.config.resumeFile is None: self.config.resumeFile = ''
if self.config.resumeFile != '' and not self.config.keepFiles: if self.config.resumeFile != '' and not self.config.keepFiles:
raise Exception('Не должно быть файла восстановления, если мы не храним файлы') raise Exception('Не должно быть файла восстановления, если мы не храним файлы')
def buildTorrentParams(self, uri): def buildTorrentParams(self, uri):
try: try:
absPath = uri2path(uri) absPath = uri2path(uri)
@ -577,7 +627,7 @@ class Pyrrent2http(object):
torrentParams['ti'] = torrent_info torrentParams['ti'] = torrent_info
logging.info('Setting save path: %s' % (encode_msg(self.config.downloadPath),)) logging.info('Setting save path: %s' % (encode_msg(self.config.downloadPath),))
torrentParams['save_path'] = self.config.downloadPath torrentParams['save_path'] = self.config.downloadPath
if os.path.exists(self.config.resumeFile): if os.path.exists(self.config.resumeFile):
logging.info('Loading resume file: %s' % (encode_msg(self.config.resumeFile),)) logging.info('Loading resume file: %s' % (encode_msg(self.config.resumeFile),))
try: try:
@ -591,7 +641,7 @@ class Pyrrent2http(object):
logging.info('Disabling sparse file support...') logging.info('Disabling sparse file support...')
torrentParams["storage_mode"] = lt.storage_mode_t.storage_mode_allocate torrentParams["storage_mode"] = lt.storage_mode_t.storage_mode_allocate
return torrentParams return torrentParams
def addTorrent(self): def addTorrent(self):
self.torrentParams = self.buildTorrentParams(self.config.uri) self.torrentParams = self.buildTorrentParams(self.config.uri)
logging.info('Adding torrent') logging.info('Adding torrent')
@ -604,11 +654,11 @@ class Pyrrent2http(object):
# #
self.torrentHandle.set_max_connections(60) self.torrentHandle.set_max_connections(60)
if self.config.trackers != '': if self.config.trackers != '':
trackers = self.config.trackers.split(',') trackers = self.config.trackers.split(',')
startTier = 256 - len(trackers) startTier = 256 - len(trackers)
for n in range(len(trackers)): for n in range(len(trackers)):
tracker = trackers[n].strip() tracker = trackers[n].strip()
logging.info('Adding tracker: %s' % (tracker,) ) logging.info('Adding tracker: %s' % (tracker,))
self.torrentHandle.add_tracker({'url': tracker}) self.torrentHandle.add_tracker({'url': tracker})
if self.config.enableScrape: if self.config.enableScrape:
logging.info('Sending scrape request to tracker') logging.info('Sending scrape request to tracker')
@ -623,8 +673,8 @@ class Pyrrent2http(object):
except Exception as e: except Exception as e:
logging.error(e.args) logging.error(e.args)
name = self.TorrentFS.info.name() name = self.TorrentFS.info.name()
self.torrent_name = name.decode(chardet.detect(name)['encoding']) self.torrent_name = name
def startHTTP(self): def startHTTP(self):
logging.info('Starting HTTP Server...') logging.info('Starting HTTP Server...')
handler = HttpHandlerFactory() handler = HttpHandlerFactory()
@ -635,9 +685,9 @@ class Pyrrent2http(object):
srv_port = int(strport) srv_port = int(strport)
self.httpListener = ThreadingHTTPServer((host, srv_port), handler) self.httpListener = ThreadingHTTPServer((host, srv_port), handler)
self.httpListener.root_obj = self 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() self.listener_thread.start()
def startServices(self): def startServices(self):
if self.config.enableDHT: if self.config.enableDHT:
logging.info('Starting DHT...') logging.info('Starting DHT...')
@ -651,19 +701,19 @@ class Pyrrent2http(object):
if self.config.enableNATPMP: if self.config.enableNATPMP:
logging.info('Starting NATPMP...') logging.info('Starting NATPMP...')
self.session.start_natpmp() self.session.start_natpmp()
def startSession(self): def startSession(self):
logging.info('Starting session...') logging.info('Starting session...')
self.session = lt.session(lt.fingerprint('LT', lt.version_major, lt.version_minor, 0, 0), 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 | alertMask = (lt.alert.category_t.error_notification |
lt.alert.category_t.storage_notification | lt.alert.category_t.storage_notification |
lt.alert.category_t.tracker_notification | lt.alert.category_t.tracker_notification |
lt.alert.category_t.status_notification) lt.alert.category_t.status_notification)
if self.config.debugAlerts: if self.config.debugAlerts:
alertMask |= lt.alert.category_t.debug_notification alertMask |= lt.alert.category_t.debug_notification
self.session.set_alert_mask(alertMask) self.session.set_alert_mask(alertMask)
settings = self.session.get_settings() settings = self.session.get_settings()
settings["request_timeout"] = self.config.requestTimeout settings["request_timeout"] = self.config.requestTimeout
settings["peer_connect_timeout"] = self.config.peerConnectTimeout settings["peer_connect_timeout"] = self.config.peerConnectTimeout
@ -681,19 +731,19 @@ class Pyrrent2http(object):
### Непонятно, как заставить использовать прокси только для подключения к трекеру? ### Непонятно, как заставить использовать прокси только для подключения к трекеру?
if self.config.proxy is not None: if self.config.proxy is not None:
ps = lt.proxy_settings() ps = lt.proxy_settings()
#peer_ps = lt.proxy_settings() # peer_ps = lt.proxy_settings()
#peer_ps.type = lt.proxy_type.none # peer_ps.type = lt.proxy_type.none
ps.hostname = self.config.proxy['host'] ps.hostname = self.config.proxy['host']
ps.port = self.config.proxy['port'] ps.port = self.config.proxy['port']
ps.type = lt.proxy_type.socks5 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) self.session.set_proxy(ps)
settings['force_proxy'] = False settings['force_proxy'] = False
settings['proxy_peer_connections'] = False settings['proxy_peer_connections'] = False
settings['anonymous_mode'] = False settings['anonymous_mode'] = False
settings['proxy_tracker_connections'] = True settings['proxy_tracker_connections'] = True
self.session.set_settings(settings) self.session.set_settings(settings)
if self.config.stateFile != '': if self.config.stateFile != '':
logging.info('Loading session state from %s' % (self.config.stateFile,)) logging.info('Loading session state from %s' % (self.config.stateFile,))
try: try:
@ -704,7 +754,7 @@ class Pyrrent2http(object):
logging.error(strerror) logging.error(strerror)
else: else:
self.session.load_state(lt.bdecode(bytes__)) self.session.load_state(lt.bdecode(bytes__))
rand = SystemRandom(time.time()) rand = SystemRandom(time.time())
portLower = self.config.listenPort portLower = self.config.listenPort
if self.config.randomPort: if self.config.randomPort:
@ -716,7 +766,7 @@ class Pyrrent2http(object):
strerror = e.args strerror = e.args
logging.error(strerror) logging.error(strerror)
raise raise
settings = self.session.get_settings() settings = self.session.get_settings()
if self.config.userAgent != '': if self.config.userAgent != '':
settings['user_agent'] = self.config.userAgent settings['user_agent'] = self.config.userAgent
@ -731,7 +781,7 @@ class Pyrrent2http(object):
settings['enable_incoming_utp'] = self.config.enableUTP settings['enable_incoming_utp'] = self.config.enableUTP
settings['enable_outgoing_utp'] = self.config.enableUTP settings['enable_outgoing_utp'] = self.config.enableUTP
self.session.set_settings(settings) self.session.set_settings(settings)
if self.config.dhtRouters != '': if self.config.dhtRouters != '':
routers = self.config.dhtRouters.split(',') routers = self.config.dhtRouters.split(',')
for router in routers: for router in routers:
@ -757,70 +807,74 @@ class Pyrrent2http(object):
self.session.set_pe_settings(encryptionSettings) self.session.set_pe_settings(encryptionSettings)
except Exception as e: except Exception as e:
logging.info('Encryption not supported: %s' % (e.args,)) logging.info('Encryption not supported: %s' % (e.args,))
def Status(self): def Status(self):
info = self.TorrentFS.info info = self.TorrentFS.info
tstatus = self.torrentHandle.status() tstatus = self.torrentHandle.status()
status = { status = {
'name' : self.torrent_name, 'name': self.torrent_name,
'state' : int(tstatus.state), 'state': int(tstatus.state),
'state_str' : str(tstatus.state), 'state_str': str(tstatus.state),
'error' : tstatus.error, 'error': tstatus.error,
'progress' : tstatus.progress, 'progress': tstatus.progress,
'download_rate' : tstatus.download_rate / 1024, 'download_rate': tstatus.download_rate // 1024,
'upload_rate' : tstatus.upload_rate / 1024, 'upload_rate': tstatus.upload_rate // 1024,
'total_download' : tstatus.total_download, 'total_download': tstatus.total_download,
'total_upload' : tstatus.total_upload, 'total_upload': tstatus.total_upload,
'num_peers' : tstatus.num_peers, 'num_peers': tstatus.num_peers,
'num_seeds' : tstatus.num_seeds, 'num_seeds': tstatus.num_seeds,
'total_seeds' : tstatus.num_complete, 'total_seeds': tstatus.num_complete,
'total_peers' : tstatus.num_incomplete 'total_peers': tstatus.num_incomplete
} }
return status return status
def Ls(self, index): def Ls(self, index):
fi = {} fi = {}
if self.TorrentFS.HasTorrentInfo(): 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] name = x[0]
files = self.TorrentFS.files files = self.TorrentFS.files
Url = 'http://' + self.config.bindAddress + '/files/' + urllib.quote(name) Url = 'http://' + self.config.bindAddress + '/files/' + urllib.parse.quote(name)
fi = { fi = {
'index': files[name].index, 'index': files[name].index,
'name': files[name].unicode_name, 'name': files[name].unicode_name,
'media_type': files[name].media_type, 'media_type': files[name].media_type,
'size': files[name].size, 'size': files[name].size,
'offset': files[name].offset, 'offset': files[name].offset,
'download': files[name].downloaded, 'download': files[name].downloaded,
'progress': files[name].progress, 'progress': files[name].progress,
'save_path': files[name].save_path, 'save_path': files[name].save_path,
'url': Url 'url': Url
} }
return fi return fi
def Peers(self): def Peers(self):
peers = {'peers': []} peers = {'peers': []}
for peer in self.torrentHandle.get_peer_info(): for peer in self.torrentHandle.get_peer_info():
if peer.flags & peer.connecting or peer.flags & peer.handshake: if peer.flags & peer.connecting or peer.flags & peer.handshake:
continue continue
pi = { pi = {
'Ip': peer.ip, 'Ip': peer.ip,
'Flags': peer.flags, 'Flags': peer.flags,
'Source': peer.source, 'Source': peer.source,
'UpSpeed': peer.up_speed/1024, 'UpSpeed': peer.up_speed // 1024,
'DownSpeed': peer.down_speed/1024, 'DownSpeed': peer.down_speed // 1024,
'TotalDownload': peer.total_download, 'TotalDownload': peer.total_download,
'TotalUpload': peer.total_upload, 'TotalUpload': peer.total_upload,
'Country': peer.country, 'Country': peer.country,
'Client': peer.client 'Client': peer.client
} }
peers['peers'].append(pi) peers['peers'].append(pi)
return peers return peers
def consumeAlerts(self): def consumeAlerts(self):
alerts = self.session.pop_alerts() alerts = self.session.pop_alerts()
for alert in alerts: for alert in alerts:
if type(alert) == lt.save_resume_data_alert: if type(alert) == lt.save_resume_data_alert:
self.processSaveResumeDataAlert(alert) self.processSaveResumeDataAlert(alert)
break break
def waitForAlert(self, alert_type, timeout): def waitForAlert(self, alert_type, timeout):
start = time.time() start = time.time()
while True: while True:
@ -831,6 +885,7 @@ class Pyrrent2http(object):
alert = self.session.pop_alert() alert = self.session.pop_alert()
if type(alert) == alert_type: if type(alert) == alert_type:
return alert return alert
def loop(self): def loop(self):
self.saveResumeDataTicker = Ticker(5) self.saveResumeDataTicker = Ticker(5)
time_start = time.time() time_start = time.time()
@ -859,16 +914,18 @@ class Pyrrent2http(object):
except IOError as e: except IOError as e:
strerror = e.args strerror = e.args
logging.error(strerror) 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 == '': if not self.torrentHandle.status().need_save_resume or self.config.resumeFile == '':
return False return False
self.torrentHandle.save_resume_data(lt.save_resume_flags_t.flush_disk_cache) 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) alert = self.waitForAlert(lt.save_resume_data_alert, 5)
if alert == None: if alert is None:
return False return False
self.processSaveResumeDataAlert(alert) self.processSaveResumeDataAlert(alert)
return True return True
def saveSessionState(self): def saveSessionState(self):
if self.config.stateFile == '': if self.config.stateFile == '':
return return
@ -882,6 +939,7 @@ class Pyrrent2http(object):
except IOError as e: except IOError as e:
strerror = e.args strerror = e.args
logging.error(strerror) logging.error(strerror)
def removeFiles(self, files): def removeFiles(self, files):
for file in files: for file in files:
try: try:
@ -897,6 +955,7 @@ class Pyrrent2http(object):
os.remove(path) os.remove(path)
path_ = os.path.dirname(path) path_ = os.path.dirname(path)
path = path_[-1] == os.path.sep and path_[:-1] or path_ path = path_[-1] == os.path.sep and path_[:-1] or path_
def filesToRemove(self): def filesToRemove(self):
files = [] files = []
if self.TorrentFS.HasTorrentInfo(): if self.TorrentFS.HasTorrentInfo():
@ -907,6 +966,7 @@ class Pyrrent2http(object):
if os.path.exists(path): if os.path.exists(path):
files.append(path) files.append(path)
return files return files
def removeTorrent(self): def removeTorrent(self):
files = [] files = []
flag = 0 flag = 0
@ -922,6 +982,7 @@ class Pyrrent2http(object):
logging.info('Waiting for files to be removed') logging.info('Waiting for files to be removed')
self.waitForAlert(lt.torrent_deleted_alert, 15) self.waitForAlert(lt.torrent_deleted_alert, 15)
self.removeFiles(files) self.removeFiles(files)
def shutdown(self): def shutdown(self):
logging.info('Stopping pyrrent2http...') logging.info('Stopping pyrrent2http...')
self.forceShutdown = True self.forceShutdown = True

View File

@ -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

View File

@ -1,31 +1,41 @@
import sys
import socket
import chardet
import os
from . import MediaType
import mimetypes 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', 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): class Struct(dict):
def __getattr__(self, attr): def __getattr__(self, attr):
return self[attr] return self[attr]
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
self[attr] = value self[attr] = value
def uri2path(uri): def uri2path(uri):
if uri[1] == ':' and sys.platform.startswith('win'): if uri[1] == ':' and sys.platform.startswith('win'):
uri = 'file:///' + uri uri = 'file:///' + uri
fileUri = urlparse.urlparse(uri) fileUri = urllib.parse.urlparse(uri)
if fileUri.scheme == 'file': if fileUri.scheme == 'file':
uriPath = fileUri.path uriPath = fileUri.path
if uriPath != '' and sys.platform.startswith('win') and (os.path.sep == uriPath[0] or uriPath[0] == '/'): if uriPath != '' and sys.platform.startswith('win') and (os.path.sep == uriPath[0] or uriPath[0] == '/'):
uriPath = uriPath[1:] uriPath = uriPath[1:]
absPath = os.path.abspath(urllib.unquote(uriPath)) absPath = os.path.abspath(urllib.parse.unquote(uriPath))
return localize_path(absPath) return localize_path(absPath)
def detect_media_type(name): def detect_media_type(name):
ext = os.path.splitext(name)[1] ext = os.path.splitext(name)[1]
if ext in SUBTITLES_FORMATS: if ext in SUBTITLES_FORMATS:
@ -41,24 +51,31 @@ def detect_media_type(name):
return MediaType.VIDEO return MediaType.VIDEO
else: else:
return MediaType.UNKNOWN return MediaType.UNKNOWN
def unicode_msg(tmpl, args): 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_ = [] arg_ = []
for a in args: 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_) return msg % tuple(arg_)
def encode_msg(msg): 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 return msg
def localize_path(path): def localize_path(path):
if not isinstance(path, unicode): path = path.decode(chardet.detect(path)['encoding']) if isinstance(path, bytes):
if not sys.platform.startswith('win'): path = path.decode(chardet.detect(path)['encoding'])
path = path.encode((sys.getfilesystemencoding() not in ('ascii', 'ANSI_X3.4-1968')) and sys.getfilesystemencoding() or 'utf-8') # 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 return path
def can_bind(host, port): def can_bind(host, port):
""" """
Checks we can bind to specified host and port Checks we can bind to specified host and port
@ -94,6 +111,6 @@ def find_free_port(host):
def ensure_fs_encoding(string): def ensure_fs_encoding(string):
if isinstance(string, str): if isinstance(string, bytes):
string = string.decode('utf-8') string = string.decode('utf-8')
return string.encode(sys.getfilesystemencoding() or 'utf-8') return string.encode(sys.getfilesystemencoding() or 'utf-8')