Compare commits
22 Commits
Author | SHA1 | Date |
---|---|---|
Роман Бородин | 5a640c4ae4 | |
Роман Бородин | 6e04f3fc14 | |
Роман Бородин | f39749807f | |
Бородин Роман | 037e32ae9b | |
Бородин Роман | 228714810a | |
inpos | 0e81ccf6bc | |
Роман | 04e63d29a0 | |
Роман | d3baa70ceb | |
Роман | 1733e20b0e | |
Sergey Kolosovskiy | 7683867a4e | |
inpos | 824daed99f | |
inpos | 32777fa959 | |
inpos | c594801130 | |
inpos | 7557ed8223 | |
inpos | a65987feea | |
inpos | e23e447a1b | |
inpos | 0f5c8de27b | |
inpos | 0e541e9a68 | |
inpos | 2f4b1c9577 | |
inpos | efbfd8beff | |
inpos | 3ecaa41dbb | |
inpos | ad26862286 |
|
@ -4,3 +4,6 @@ script.module.pyrrent2http
|
||||||
This add-on is engine for [plugin.video.torrenter](https://github.com/DiMartinoXBMC/plugin.video.torrenter)
|
This add-on is engine for [plugin.video.torrenter](https://github.com/DiMartinoXBMC/plugin.video.torrenter)
|
||||||
|
|
||||||
This add-on can be used to stream media files from torrents without need to download entire files.
|
This add-on can be used to stream media files from torrents without need to download entire files.
|
||||||
|
|
||||||
|
Данный движок для модуля Torrenter v2 медиа-центра KODI (XBMC) появился из-за отсутствия рабочего решения для Raspberry Pi.
|
||||||
|
Сейчас pyrrent2http работает на множестве платформ.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<?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="0.9.0" provider-name="inpos">
|
<addon id="script.module.pyrrent2http" name="pyrrent2http" version="1.1.1" 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" version="1.2.0"/>
|
||||||
<import addon="script.module.chardet" />
|
<import addon="script.module.chardet" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.module" library="lib"/>
|
<extension point="xbmc.python.module" library="lib"/>
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
[B]Version 1.0.0[/B]
|
||||||
|
+ Исправлен механизм добавления трекеров
|
||||||
|
+ Добавлена поддержка прокси SOCKS длч подключения к трекерам
|
||||||
|
|
||||||
[B]Version 0.9.0[/B]
|
[B]Version 0.9.0[/B]
|
||||||
+ Изменён принцип работы с торрентом
|
+ Изменён принцип работы с торрентом
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -33,7 +39,7 @@ class Engine:
|
||||||
user_agent=None, startup_timeout=5, state_file='', enable_utp=True, enable_tcp=True,
|
user_agent=None, startup_timeout=5, state_file='', enable_utp=True, enable_tcp=True,
|
||||||
debug_alerts=False, logger=None, torrent_connect_boost=50, connection_speed=50,
|
debug_alerts=False, logger=None, torrent_connect_boost=50, connection_speed=50,
|
||||||
peer_connect_timeout=15, request_timeout=20, min_reconnect_time=60, max_failcount=3,
|
peer_connect_timeout=15, request_timeout=20, min_reconnect_time=60, max_failcount=3,
|
||||||
dht_routers=None, trackers=None):
|
dht_routers=None, trackers=None, proxy=None):
|
||||||
"""
|
"""
|
||||||
Creates engine instance. It doesn't do anything except initializing object members. For starting engine use
|
Creates engine instance. It doesn't do anything except initializing object members. For starting engine use
|
||||||
start() method.
|
start() method.
|
||||||
|
@ -126,6 +132,7 @@ class Engine:
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.uri = uri
|
self.uri = uri
|
||||||
self.started = False
|
self.started = False
|
||||||
|
self.proxy = proxy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_save_path(path):
|
def _validate_save_path(path):
|
||||||
|
@ -166,6 +173,7 @@ class Engine:
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'torrentConnectBoost': self.torrent_connect_boost,
|
'torrentConnectBoost': self.torrent_connect_boost,
|
||||||
'trackers': ",".join(self.trackers),
|
'trackers': ",".join(self.trackers),
|
||||||
|
'proxy': self.proxy,
|
||||||
'resumeFile': self.resume_file,
|
'resumeFile': self.resume_file,
|
||||||
'minReconnectTime': self.min_reconnect_time,
|
'minReconnectTime': self.min_reconnect_time,
|
||||||
'enableUPNP': self.enable_upnp,
|
'enableUPNP': self.enable_upnp,
|
||||||
|
@ -196,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,
|
||||||
|
@ -206,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
|
||||||
|
@ -234,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:
|
||||||
|
@ -245,6 +256,15 @@ class Engine:
|
||||||
raise Error("Can't start pyrrent2http, time is out", Error.TIMEOUT)
|
raise Error("Can't start pyrrent2http, time is out", Error.TIMEOUT)
|
||||||
self._log("pyrrent2http successfully started.")
|
self._log("pyrrent2http successfully started.")
|
||||||
|
|
||||||
|
def activate_file(self, index):
|
||||||
|
self.pyrrent2http.TorrentFS.file(index)
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.pyrrent2http.pause = True
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
self.pyrrent2http.pause = False
|
||||||
|
|
||||||
def check_torrent_error(self, status=None):
|
def check_torrent_error(self, status=None):
|
||||||
"""
|
"""
|
||||||
It is recommended to call this method periodically to check if any libtorrent errors occurred.
|
It is recommended to call this method periodically to check if any libtorrent errors occurred.
|
||||||
|
@ -269,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).
|
||||||
|
@ -286,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))
|
||||||
|
@ -296,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):
|
||||||
|
@ -324,13 +343,12 @@ class Engine:
|
||||||
:return: File with specified index
|
:return: File with specified index
|
||||||
:rtype: FileStatus
|
:rtype: FileStatus
|
||||||
"""
|
"""
|
||||||
files = self.pyrrent2http.Ls()['files']
|
filestatus = self.pyrrent2http.Ls(file_index)
|
||||||
if files:
|
try:
|
||||||
for f in files:
|
return FileStatus(**filestatus)
|
||||||
if f['index'] == file_index:
|
except:
|
||||||
return FileStatus(**f)
|
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
|
@ -346,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
|
||||||
|
|
|
@ -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
|
|
|
@ -1,16 +1,16 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import os
|
import os
|
||||||
import chardet
|
import chardet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from python_libtorrent import get_libtorrent
|
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
|
import libtorrent as lt # @UnresolvedImport
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
strerror = e.args
|
strerror = e.args
|
||||||
print(strerror)
|
print(strerror)
|
||||||
|
@ -18,28 +18,39 @@ 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'):
|
||||||
|
from ctypes import *
|
||||||
|
|
||||||
|
libc = CDLL('/system/lib/libc.so')
|
||||||
|
libc.lseek64.restype = c_ulonglong
|
||||||
|
libc.lseek64.argtypes = [c_uint, c_ulonglong, c_uint]
|
||||||
|
libc.read.restype = c_long
|
||||||
|
libc.read.argtypes = [c_uint, c_void_p, c_long]
|
||||||
|
O_RDONLY = 0
|
||||||
|
O_LARGEFILE = 0x8000
|
||||||
|
|
||||||
######################################################################################
|
######################################################################################
|
||||||
|
|
||||||
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
|
CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot # @UndefinedVariable
|
||||||
GetCurrentProcessId = ctypes.windll.kernel32.GetCurrentProcessId
|
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),
|
||||||
|
@ -55,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.
|
||||||
|
@ -83,28 +96,32 @@ 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
|
||||||
def true(self):
|
def true(self):
|
||||||
if self.tick:
|
if self.tick:
|
||||||
self.tick = False
|
self.tick = False
|
||||||
|
@ -127,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
|
||||||
|
@ -152,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
|
||||||
|
@ -165,34 +186,49 @@ class TorrentFile(object):
|
||||||
logging.info('Waiting for file: %s' % (self.save_path,))
|
logging.info('Waiting for file: %s' % (self.save_path,))
|
||||||
self.tfs.handle.flush_cache()
|
self.tfs.handle.flush_cache()
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
self.filePtr = io.open(self.save_path, 'rb')
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
self.filePtr = libc.open(self.save_path, O_RDONLY | O_LARGEFILE, 755)
|
||||||
|
else:
|
||||||
|
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("[%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):
|
||||||
return self.filePtr.seek(0, io.SEEK_CUR)
|
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):
|
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)
|
||||||
|
@ -201,17 +237,22 @@ 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,))
|
||||||
self.tfs.removeOpenedFile(self)
|
self.tfs.removeOpenedFile(self)
|
||||||
self.closed = True
|
self.closed = True
|
||||||
if self.filePtr is not None:
|
if self.filePtr is not None:
|
||||||
self.filePtr.close()
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
libc.close(self.filePtr)
|
||||||
|
else:
|
||||||
|
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_ = ''
|
||||||
|
@ -221,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:
|
||||||
|
@ -231,55 +273,64 @@ 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
|
||||||
read = filePtr.readinto(buf)
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
read = libc.read(self.filePtr, addressof(buf), toRead)
|
||||||
|
else:
|
||||||
|
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
|
||||||
if whence == os.SEEK_END:
|
if whence == os.SEEK_END:
|
||||||
offset = self.size - offset
|
offset = self.size - offset
|
||||||
whence = os.SEEK_SET
|
whence = os.SEEK_SET
|
||||||
newOffset = filePtr.seek(offset, whence)
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
newOffset = libc.lseek64(self.filePtr, offset, whence)
|
||||||
|
else:
|
||||||
|
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, startIndex):
|
def __init__(self, root, handle):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.waitForMetadata()
|
self.waitForMetadata()
|
||||||
self.save_path = localize_path(self.root.torrentParams['save_path'])
|
self.save_path = localize_path(self.root.torrentParams['save_path'])
|
||||||
self.priorities = list(self.handle.file_priorities())
|
self.priorities = list(self.handle.file_priorities())
|
||||||
file_ = self.__file_at_(startIndex)
|
self.files = {}
|
||||||
self.files = {file_.name: file_}
|
|
||||||
#self.handle.set_piece_deadline(self.files[startIndex].startPiece, 50)
|
|
||||||
if startIndex < 0:
|
|
||||||
logging.info('No -file-index specified, downloading will be paused until any file is requested')
|
|
||||||
|
|
||||||
num_files = self.info.num_files()
|
num_files = self.info.num_files()
|
||||||
|
|
||||||
for i in range(num_files):
|
for i in range(num_files):
|
||||||
if startIndex == i:
|
self.setPriority(i, 0)
|
||||||
self.setPriority(i, 1)
|
|
||||||
else:
|
def file(self, index):
|
||||||
self.setPriority(i, 0)
|
for name in list(self.files.keys()):
|
||||||
|
if self.files[name].index == index:
|
||||||
|
return self.files[name]
|
||||||
|
file_ = self.__file_at_(index)
|
||||||
|
self.files[file_.name] = file_
|
||||||
|
self.setPriority(index, 1)
|
||||||
|
return file_
|
||||||
|
|
||||||
def Shutdown(self):
|
def Shutdown(self):
|
||||||
self.shuttingDown = True
|
self.shuttingDown = True
|
||||||
|
@ -287,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)
|
||||||
|
@ -310,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_ = []
|
||||||
|
@ -330,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:
|
||||||
|
@ -364,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)
|
||||||
|
@ -380,53 +446,69 @@ 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:
|
||||||
f.Seek(start_range, 0)
|
f.Seek(start_range, 0)
|
||||||
chunk = f.piece_length
|
chunk = f.piece_length
|
||||||
total = 0
|
total = 0
|
||||||
buf = bytearray(chunk)
|
if os.getenv('ANDROID_ROOT'):
|
||||||
while chunk > 0:
|
buf = create_string_buffer(chunk)
|
||||||
|
else:
|
||||||
|
buf = bytearray(chunk)
|
||||||
|
while chunk > 0 and not self.server.root_obj.forceShutdown:
|
||||||
if start_range + chunk > end_range:
|
if start_range + chunk > end_range:
|
||||||
chunk = end_range - start_range
|
chunk = end_range - start_range
|
||||||
buf = bytearray(chunk)
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
buf = create_string_buffer(chunk)
|
||||||
|
else:
|
||||||
|
buf = bytearray(chunk)
|
||||||
try:
|
try:
|
||||||
if f.Read(buf) < 1: break
|
if f.Read(buf) < 1: break
|
||||||
self.wfile.write(buf)
|
while self.server.root_obj.pause and not self.server.root_obj.forceShutdown:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
if os.getenv('ANDROID_ROOT'):
|
||||||
|
self.wfile.write(buf.raw)
|
||||||
|
else:
|
||||||
|
self.wfile.write(buf)
|
||||||
except:
|
except:
|
||||||
break
|
break
|
||||||
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:
|
||||||
|
@ -453,27 +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):
|
||||||
def __init__(self, uri = '', bindAddress = 'localhost:5001', downloadPath = '.',
|
pause = False
|
||||||
idleTimeout = -1, fileIndex = -1, keepComplete = False,
|
|
||||||
keepIncomplete = False, keepFiles = False, showAllStats = False,
|
def __init__(self, uri='', bindAddress='localhost:5001', downloadPath='.',
|
||||||
showOverallProgress = False, showFilesProgress = False,
|
idleTimeout=-1, keepComplete=False,
|
||||||
showPiecesProgress = False, debugAlerts = False,
|
keepIncomplete=False, keepFiles=False, showAllStats=False,
|
||||||
exitOnFinish = False, resumeFile = '', stateFile = '',
|
showOverallProgress=False, showFilesProgress=False,
|
||||||
userAgent = USER_AGENT, dhtRouters = '', trackers = '',
|
showPiecesProgress=False, debugAlerts=False,
|
||||||
listenPort = 6881, torrentConnectBoost = 50, connectionSpeed = 50,
|
exitOnFinish=False, resumeFile='', stateFile='',
|
||||||
peerConnectTimeout = 15, requestTimeout = 20, maxDownloadRate = -1,
|
userAgent=USER_AGENT, dhtRouters='', trackers='',
|
||||||
maxUploadRate = -1, connectionsLimit = 200, encryption = 1,
|
listenPort=6881, torrentConnectBoost=50, connectionSpeed=50,
|
||||||
minReconnectTime = 60, maxFailCount = 3, noSparseFile = False,
|
peerConnectTimeout=15, requestTimeout=20, maxDownloadRate=-1,
|
||||||
randomPort = False, enableScrape = False, enableDHT = True,
|
maxUploadRate=-1, connectionsLimit=200, encryption=1,
|
||||||
enableLSD = True, enableUPNP = True, enableNATPMP = True, enableUTP = True, enableTCP = True):
|
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.torrentHandle = None
|
||||||
self.forceShutdown = False
|
self.forceShutdown = False
|
||||||
self.session = None
|
self.session = None
|
||||||
|
@ -484,7 +571,6 @@ class Pyrrent2http(object):
|
||||||
self.config.bindAddress = bindAddress
|
self.config.bindAddress = bindAddress
|
||||||
self.config.downloadPath = downloadPath
|
self.config.downloadPath = downloadPath
|
||||||
self.config.idleTimeout = idleTimeout
|
self.config.idleTimeout = idleTimeout
|
||||||
self.config.fileIndex = fileIndex
|
|
||||||
self.config.keepComplete = keepComplete
|
self.config.keepComplete = keepComplete
|
||||||
self.config.keepIncomplete = keepIncomplete
|
self.config.keepIncomplete = keepIncomplete
|
||||||
self.config.keepFiles = keepFiles
|
self.config.keepFiles = keepFiles
|
||||||
|
@ -519,6 +605,7 @@ class Pyrrent2http(object):
|
||||||
self.config.enableNATPMP = enableNATPMP
|
self.config.enableNATPMP = enableNATPMP
|
||||||
self.config.enableUTP = enableUTP
|
self.config.enableUTP = enableUTP
|
||||||
self.config.enableTCP = enableTCP
|
self.config.enableTCP = enableTCP
|
||||||
|
self.config.proxy = proxy
|
||||||
if self.config.uri == '':
|
if self.config.uri == '':
|
||||||
raise Exception("uri is empty string")
|
raise Exception("uri is empty string")
|
||||||
if self.config.uri.startswith('magnet:'):
|
if self.config.uri.startswith('magnet:'):
|
||||||
|
@ -526,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)
|
||||||
|
@ -540,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:
|
||||||
|
@ -554,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')
|
||||||
|
@ -567,12 +654,12 @@ 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(tracker, startTier + n)
|
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')
|
||||||
self.torrentHandle.scrape_tracker()
|
self.torrentHandle.scrape_tracker()
|
||||||
|
@ -582,31 +669,25 @@ class Pyrrent2http(object):
|
||||||
info = self.torrentHandle.get_torrent_info()
|
info = self.torrentHandle.get_torrent_info()
|
||||||
logging.info('Downloading torrent: %s' % (info.name(),))
|
logging.info('Downloading torrent: %s' % (info.name(),))
|
||||||
try:
|
try:
|
||||||
self.TorrentFS = TorrentFS(self, self.torrentHandle, self.config.fileIndex)
|
self.TorrentFS = TorrentFS(self, self.torrentHandle)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(e.args)
|
logging.error(e.args)
|
||||||
|
name = self.TorrentFS.info.name()
|
||||||
|
self.torrent_name = name
|
||||||
|
|
||||||
def startHTTP(self):
|
def startHTTP(self):
|
||||||
#def http_server_loop(listener, alive):
|
|
||||||
# while alive.is_set():
|
|
||||||
# print('+++handle request+++')
|
|
||||||
# listener.handle_request()
|
|
||||||
# listener.server_close()
|
|
||||||
#self.main_alive = threading.Event()
|
|
||||||
#self.main_alive.set()
|
|
||||||
logging.info('Starting HTTP Server...')
|
logging.info('Starting HTTP Server...')
|
||||||
handler = HttpHandlerFactory()
|
handler = HttpHandlerFactory()
|
||||||
|
handler.protocol_version = 'HTTP/1.1'
|
||||||
logging.info('Listening HTTP on %s...\n' % (self.config.bindAddress,))
|
logging.info('Listening HTTP on %s...\n' % (self.config.bindAddress,))
|
||||||
host, strport = self.config.bindAddress.split(':')
|
host, strport = self.config.bindAddress.split(':')
|
||||||
if len(strport) > 0:
|
if len(strport) > 0:
|
||||||
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.httpListener.timeout = 0.5
|
self.listener_thread = threading.Thread(target=self.httpListener.serve_forever)
|
||||||
#thread = threading.Thread(target = http_server_loop, args = (self.httpListener, self.main_alive))
|
self.listener_thread.start()
|
||||||
thread = threading.Thread(target = self.httpListener.serve_forever)
|
|
||||||
thread.start()
|
|
||||||
|
|
||||||
def startServices(self):
|
def startServices(self):
|
||||||
if self.config.enableDHT:
|
if self.config.enableDHT:
|
||||||
logging.info('Starting DHT...')
|
logging.info('Starting DHT...')
|
||||||
|
@ -620,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
|
||||||
|
@ -647,8 +728,22 @@ class Pyrrent2http(object):
|
||||||
settings["rate_limit_ip_overhead"] = True
|
settings["rate_limit_ip_overhead"] = True
|
||||||
settings["min_announce_interval"] = 60
|
settings["min_announce_interval"] = 60
|
||||||
settings["tracker_backoff"] = 0
|
settings["tracker_backoff"] = 0
|
||||||
|
### Непонятно, как заставить использовать прокси только для подключения к трекеру?
|
||||||
|
if self.config.proxy is not None:
|
||||||
|
ps = lt.proxy_settings()
|
||||||
|
# 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_proxy(ps)
|
||||||
|
settings['force_proxy'] = False
|
||||||
|
settings['proxy_peer_connections'] = False
|
||||||
|
settings['anonymous_mode'] = False
|
||||||
|
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:
|
||||||
|
@ -659,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:
|
||||||
|
@ -671,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
|
||||||
|
@ -686,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:
|
||||||
|
@ -712,101 +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.TorrentFS.handle.status()
|
|
||||||
tstatus = self.torrentHandle.status()
|
tstatus = self.torrentHandle.status()
|
||||||
|
|
||||||
status = {
|
status = {
|
||||||
'name' : info.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):
|
|
||||||
retFiles = {'files': []}
|
def Ls(self, index):
|
||||||
|
fi = {}
|
||||||
if self.TorrentFS.HasTorrentInfo():
|
if self.TorrentFS.HasTorrentInfo():
|
||||||
|
x = [n for n in list(self.TorrentFS.files.keys()) if self.TorrentFS.files[n].index == index]
|
||||||
|
name = x[0]
|
||||||
files = self.TorrentFS.files
|
files = self.TorrentFS.files
|
||||||
for name in files.keys():
|
Url = 'http://' + self.config.bindAddress + '/files/' + urllib.parse.quote(name)
|
||||||
Url = 'http://' + self.config.bindAddress + '/files/' + urllib.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
|
||||||
retFiles['files'].append(fi)
|
|
||||||
return retFiles
|
|
||||||
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 stats(self):
|
|
||||||
status = self.torrentHandle.status()
|
|
||||||
dhtStatusStr = ''
|
|
||||||
if not status.has_metadata:
|
|
||||||
return
|
|
||||||
if self.config.showAllStats or self.config.showOverallProgress:
|
|
||||||
sessionStatus = self.session.status()
|
|
||||||
if self.session.is_dht_running():
|
|
||||||
dhtStatusStr = ', DHT nodes: %d' % (sessionStatus.dht_nodes,)
|
|
||||||
errorStr = ''
|
|
||||||
if len(status.error) > 0:
|
|
||||||
errorStr = ' (%s)' % (status.error,)
|
|
||||||
logging.info('%s, overall progress: %.2f%%, dl/ul: %.3f/%.3f kbps, peers/seeds: %d/%d' % (
|
|
||||||
str(status.state),
|
|
||||||
status.progress * 100,
|
|
||||||
float(status.download_rate)/1024,
|
|
||||||
float(status.upload_rate)/1024,
|
|
||||||
status.num_peers,
|
|
||||||
status.num_seeds
|
|
||||||
) + dhtStatusStr + errorStr
|
|
||||||
)
|
|
||||||
if self.config.showFilesProgress or self.config.showAllStats:
|
|
||||||
str_ = 'Files: '
|
|
||||||
for i, f in enumerate(self.TorrentFS.files):
|
|
||||||
str_ += '[%d] %.2f%% ' % (i, f.Progress()*100)
|
|
||||||
logging.info(str_)
|
|
||||||
if (self.config.showPiecesProgress or self.config.showAllStats) and self.TorrentFS.lastOpenedFile != None:
|
|
||||||
self.TorrentFS.lastOpenedFile.ShowPieces()
|
|
||||||
'''
|
|
||||||
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:
|
||||||
|
@ -817,8 +885,8 @@ 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.statsTicker = Ticker(30)
|
|
||||||
self.saveResumeDataTicker = Ticker(5)
|
self.saveResumeDataTicker = Ticker(5)
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
while True:
|
while True:
|
||||||
|
@ -833,8 +901,6 @@ class Pyrrent2http(object):
|
||||||
if os.getppid() == 1:
|
if os.getppid() == 1:
|
||||||
self.forceShutdown = True
|
self.forceShutdown = True
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
#if self.statsTicker.true:
|
|
||||||
# self.stats()
|
|
||||||
if self.saveResumeDataTicker.true:
|
if self.saveResumeDataTicker.true:
|
||||||
self.saveResumeData(True)
|
self.saveResumeData(True)
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
@ -848,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
|
||||||
|
@ -871,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:
|
||||||
|
@ -886,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():
|
||||||
|
@ -896,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
|
||||||
|
@ -911,12 +982,13 @@ 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
|
||||||
#self.statsTicker.stop()
|
|
||||||
self.saveResumeDataTicker.stop()
|
self.saveResumeDataTicker.stop()
|
||||||
self.httpListener.shutdown()
|
self.httpListener.shutdown()
|
||||||
|
self.httpListener.socket.close()
|
||||||
self.TorrentFS.Shutdown()
|
self.TorrentFS.Shutdown()
|
||||||
if self.session != None:
|
if self.session != None:
|
||||||
self.session.pause()
|
self.session.pause()
|
||||||
|
@ -926,5 +998,5 @@ class Pyrrent2http(object):
|
||||||
self.saveSessionState()
|
self.saveSessionState()
|
||||||
self.removeTorrent()
|
self.removeTorrent()
|
||||||
logging.info('Aborting the session')
|
logging.info('Aborting the session')
|
||||||
del self.session
|
self.session = None
|
||||||
logging.info('Bye bye')
|
logging.info('Bye bye')
|
||||||
|
|
|
@ -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 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(True 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(True 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')
|
||||||
|
|
Loading…
Reference in New Issue