Files
addon/plugin.video.alfa/lib/btserver/client.py
2017-08-13 17:07:36 +02:00

556 lines
20 KiB
Python
Executable File

# -*- coding: utf-8 -*-
try:
from python_libtorrent import get_libtorrent, get_platform
lt = get_libtorrent()
except Exception, e:
import libtorrent as lt
import os
import pickle
import random
import time
import urllib
from cache import Cache
from dispatcher import Dispatcher
from file import File
from handler import Handler
from monitor import Monitor
from platformcode import logger
from resume_data import ResumeData
from server import Server
class Client(object):
INITIAL_TRACKERS = ['udp://tracker.openbittorrent.com:80',
'udp://tracker.istole.it:80',
'udp://open.demonii.com:80',
'udp://tracker.coppersurfer.tk:80',
'udp://tracker.leechers-paradise.org:6969',
'udp://exodus.desync.com:6969',
'udp://tracker.publicbt.com:80']
VIDEO_EXTS = {'.avi': 'video/x-msvideo', '.mp4': 'video/mp4', '.mkv': 'video/x-matroska',
'.m4v': 'video/mp4', '.mov': 'video/quicktime', '.mpg': 'video/mpeg', '.ogv': 'video/ogg',
'.ogg': 'video/ogg', '.webm': 'video/webm', '.ts': 'video/mp2t', '.3gp': 'video/3gpp'}
def __init__(self, url=None, port=None, ip=None, auto_shutdown=True, wait_time=20, timeout=5, auto_delete=True,
temp_path=None, is_playing_fnc=None, print_status=False):
# server
if port:
self.port = port
else:
self.port = random.randint(8000, 8099)
if ip:
self.ip = ip
else:
self.ip = "127.0.0.1"
self.server = Server((self.ip, self.port), Handler, client=self)
# Options
if temp_path:
self.temp_path = temp_path
else:
self.temp_path = os.path.join(os.path.dirname(__file__), "tmp")
self.is_playing_fnc = is_playing_fnc
self.timeout = timeout
self.auto_delete = auto_delete
self.wait_time = wait_time
self.auto_shutdown = auto_shutdown
self.buffer_size = 15
self.last_pieces_priorize = 5
self.state_file = "state"
self.torrent_paramss = {'save_path': self.temp_path, 'storage_mode': lt.storage_mode_t.storage_mode_sparse}
# State
self.has_meta = False
self.meta = None
self.start_time = None
self.last_connect = 0
self.connected = False
self.closed = False
self.file = None
self.files = None
self._th = None
# Sesion
self._cache = Cache(self.temp_path)
self._ses = lt.session()
self._ses.listen_on(0, 0)
# Cargamos el archivo de estado (si esxiste)
if os.path.exists(os.path.join(self.temp_path, self.state_file)):
try:
f = open(os.path.join(self.temp_path, self.state_file), "rb")
state = pickle.load(f)
self._ses.load_state(state)
f.close()
except:
pass
self._start_services()
# Monitor & Dispatcher
self._monitor = Monitor(self)
if print_status:
self._monitor.add_listener(self.print_status)
self._monitor.add_listener(self._check_meta)
self._monitor.add_listener(self.save_state)
self._monitor.add_listener(self.priorize_start_file)
self._monitor.add_listener(self.announce_torrent)
if self.auto_shutdown:
self._monitor.add_listener(self._auto_shutdown)
self._dispatcher = Dispatcher(self)
self._dispatcher.add_listener(self._update_ready_pieces)
# Iniciamos la URL
if url:
self.start_url(url)
def get_play_list(self):
"""
Función encargada de generar el playlist
"""
# Esperamos a lo metadatos
while not self.has_meta:
time.sleep(1)
# Comprobamos que haya archivos de video
if self.files:
if len(self.files) > 1:
return "http://" + self.ip + ":" + str(self.port) + "/playlist.pls"
else:
return "http://" + self.ip + ":" + str(self.port) + "/" + urllib.quote(self.files[0].path)
def get_files(self):
"""
Función encargada de genera el listado de archivos
"""
# Esperamos a lo metadatos
while not self.has_meta:
time.sleep(1)
files = []
# Comprobamos que haya archivos de video
if self.files:
# Creamos el dict con los archivos
for file in self.files:
n = file.path
u = "http://" + self.ip + ":" + str(self.port) + "/" + urllib.quote(n)
s = file.size
files.append({"name": n, "url": u, "size": s})
return files
def _find_files(self, files, search=None):
"""
Función encargada de buscar los archivos reproducibles del torrent
"""
# Obtenemos los archivos que la extension este en la lista
videos = filter(lambda f: self.VIDEO_EXTS.has_key(os.path.splitext(f.path)[1]), files)
if not videos:
raise Exception('No video files in torrent')
for v in videos:
videos[videos.index(v)].index = files.index(v)
return videos
def set_file(self, f):
"""
Función encargada de seleccionar el archivo que vamos a servir y por tanto, priorizar su descarga
"""
# Seleccionamos el archivo que vamos a servir
fmap = self.meta.map_file(f.index, 0, 1)
self.file = File(f.path, self.temp_path, f.index, f.size, fmap, self.meta.piece_length(), self)
self.prioritize_file()
def prioritize_piece(self, pc, idx):
"""
Función encargada de priorizar una determinada pieza
"""
piece_duration = 1000
min_deadline = 2000
dl = idx * piece_duration + min_deadline
self._th.set_piece_deadline(pc, dl, lt.deadline_flags.alert_when_available)
if idx == 0:
tail_pieces = 9
# Piezas anteriores a la primera se desactivan
if (self.file.last_piece - pc) > tail_pieces:
for i in xrange(self.file.first_piece, pc):
self._th.piece_priority(i, 0)
self._th.reset_piece_deadline(i)
# Piezas siguientes a la primera se activan
for i in xrange(pc + 1, self.file.last_piece + 1):
self._th.piece_priority(i, 1)
def prioritize_file(self):
"""
Función encargada de priorizar las piezas correspondientes al archivo seleccionado en la funcion set_file()
"""
priorities = []
for i in xrange(self.meta.num_pieces()):
if i >= self.file.first_piece and i <= self.file.last_piece:
priorities.append(1)
else:
priorities.append(0)
self._th.prioritize_pieces(priorities)
def download_torrent(self, url):
"""
Función encargada de descargar un archivo .torrent
"""
from core import scrapertools
data = scrapertools.downloadpage(url)
return data
def start_url(self, uri):
"""
Función encargada iniciar la descarga del torrent desde la url, permite:
- Url apuntando a un .torrent
- Url magnet
- Archivo .torrent local
"""
if self._th:
raise Exception('Torrent is already started')
if uri.startswith('http://') or uri.startswith('https://'):
torrent_data = self.download_torrent(uri)
info = lt.torrent_info(lt.bdecode(torrent_data))
tp = {'ti': info}
resume_data = self._cache.get_resume(info_hash=str(info.info_hash()))
if resume_data:
tp['resume_data'] = resume_data
elif uri.startswith('magnet:'):
tp = {'url': uri}
resume_data = self._cache.get_resume(info_hash=Cache.hash_from_magnet(uri))
if resume_data:
tp['resume_data'] = resume_data
elif os.path.isfile(uri):
if os.access(uri, os.R_OK):
info = lt.torrent_info(uri)
tp = {'ti': info}
resume_data = self._cache.get_resume(info_hash=str(info.info_hash()))
if resume_data:
tp['resume_data'] = resume_data
else:
raise ValueError('Invalid torrent path %s' % uri)
else:
raise ValueError("Invalid torrent %s" % uri)
tp.update(self.torrent_paramss)
self._th = self._ses.add_torrent(tp)
for tr in self.INITIAL_TRACKERS:
self._th.add_tracker({'url': tr})
self._th.set_sequential_download(True)
self._th.force_reannounce()
self._th.force_dht_announce()
self._monitor.start()
self._dispatcher.do_start(self._th, self._ses)
self.server.run()
def stop(self):
"""
Función encargada de de detener el torrent y salir
"""
self._dispatcher.stop()
self._dispatcher.join()
self._monitor.stop()
self.server.stop()
self._dispatcher.stop()
if self._ses:
self._ses.pause()
if self._th:
self.save_resume()
self.save_state()
self._stop_services()
self._ses.remove_torrent(self._th, self.auto_delete)
del self._ses
self.closed = True
def _start_services(self):
"""
Función encargada de iniciar los servicios de libtorrent: dht, lsd, upnp, natpnp
"""
self._ses.add_dht_router("router.bittorrent.com", 6881)
self._ses.add_dht_router("router.bitcomet.com", 554)
self._ses.add_dht_router("router.utorrent.com", 6881)
self._ses.start_dht()
self._ses.start_lsd()
self._ses.start_upnp()
self._ses.start_natpmp()
def _stop_services(self):
"""
Función encargada de detener los servicios de libtorrent: dht, lsd, upnp, natpnp
"""
self._ses.stop_natpmp()
self._ses.stop_upnp()
self._ses.stop_lsd()
self._ses.stop_dht()
def save_resume(self):
"""
Función encargada guardar los metadatos para continuar una descarga mas rapidamente
"""
if self._th.need_save_resume_data() and self._th.is_valid() and self.meta:
r = ResumeData(self)
start = time.time()
while (time.time() - start) <= 5:
if r.data or r.failed:
break
time.sleep(0.1)
if r.data:
self._cache.save_resume(self.unique_file_id, lt.bencode(r.data))
@property
def status(self):
"""
Función encargada de devolver el estado del torrent
"""
if self._th:
s = self._th.status()
# Download Rate
s._download_rate = s.download_rate / 1000
# Progreso del archivo
if self.file:
pieces = s.pieces[self.file.first_piece:self.file.last_piece]
progress = float(sum(pieces)) / len(pieces)
else:
progress = 0
s.progress_file = progress * 100
# Tamaño del archivo
if self.file:
s.file_size = self.file.size / 1048576.0
else:
s.file_size = 0
# Estado del buffer
if self.file and self.file.cursor: # Con una conexion activa: Disponible vs Posicion del reproductor
percent = len(self.file.cursor.cache)
percent = percent * 100 / self.buffer_size
s.buffer = int(percent)
elif self.file: # Sin una conexion activa: Pre-buffer antes de iniciar
# El Pre-buffer consta de dos partes_
# 1. Buffer al inicio del archivo para que el reproductor empieze sin cortes
# 2. Buffer al final del archivo (en algunos archivos el reproductor mira el final del archivo antes de comenzar)
bp = []
# El tamaño del buffer de inicio es el tamaño del buffer menos el tamaño del buffer del final
first_pieces_priorize = self.buffer_size - self.last_pieces_priorize
# Comprobamos que partes del buffer del inicio estan disponibles
for x in range(first_pieces_priorize):
if self._th.have_piece(self.file.first_piece + x):
bp.append(True)
else:
bp.append(False)
# Comprobamos que partes del buffer del final estan disponibles
for x in range(self.last_pieces_priorize):
if self._th.have_piece(self.file.last_piece - x):
bp.append(True)
else:
bp.append(False)
s.buffer = int(sum(bp) * 100 / self.buffer_size)
else: # Si no hay ningun archivo seleccionado: No hay buffer
s.buffer = 0
# Tiempo restante para cerrar en caso de tener el timeout activo
if self.auto_shutdown:
if self.connected:
if self.timeout:
s.timeout = int(self.timeout - (time.time() - self.last_connect - 1))
if self.file and self.file.cursor:
s.timeout = self.timeout
if s.timeout < 0: s.timeout = "Cerrando"
else:
s.timeout = "---"
else:
if self.start_time and self.wait_time:
s.timeout = int(self.wait_time - (time.time() - self.start_time - 1))
if s.timeout < 0: s.timeout = "Cerrando"
else:
s.timeout = "---"
else:
s.timeout = "Off"
# Estado de la descarga
STATE_STR = ['En cola', 'Comprobando', 'Descargando metadata', \
'Descargando', 'Finalizado', 'Seeding', 'Allocating', 'Comprobando fastresume']
s.str_state = STATE_STR[s.state]
# Estado DHT
if self._ses.dht_state() is not None:
s.dht_state = "On"
s.dht_nodes = self._ses.status().dht_nodes
else:
s.dht_state = "Off"
s.dht_nodes = 0
# Cantidad de Trackers
s.trackers = len(self._th.trackers())
# Origen de los peers
s.dht_peers = 0
s.trk_peers = 0
s.pex_peers = 0
s.lsd_peers = 0
for peer in self._th.get_peer_info():
if peer.source & 1:
s.trk_peers += 1
if peer.source & 2:
s.dht_peers += 1
if peer.source & 4:
s.pex_peers += 1
if peer.source & 8:
s.lsd_peers += 1
return s
"""
Servicios:
- Estas funciones se ejecutan de forma automatica cada x tiempo en otro Thread.
- Estas funciones son ejecutadas mientras el torrent esta activo algunas pueden desactivarse
segun la configuracion como por ejemplo la escritura en el log
"""
def _auto_shutdown(self, *args, **kwargs):
"""
Servicio encargado de autoapagar el servidor
"""
if self.file and self.file.cursor:
self.last_connect = time.time()
self.connected = True
if self.is_playing_fnc and self.is_playing_fnc():
self.last_connect = time.time()
self.connected = True
if self.auto_shutdown:
# shudown por haber cerrado el reproductor
if self.connected and self.is_playing_fnc and not self.is_playing_fnc():
if time.time() - self.last_connect - 1 > self.timeout:
self.stop()
# shutdown por no realizar ninguna conexion
if (not self.file or not self.file.cursor) and self.start_time and self.wait_time and not self.connected:
if time.time() - self.start_time - 1 > self.wait_time:
self.stop()
# shutdown tras la ultima conexion
if (not self.file or not self.file.cursor) and self.timeout and self.connected and not self.is_playing_fnc:
if time.time() - self.last_connect - 1 > self.timeout:
self.stop()
def announce_torrent(self):
"""
Servicio encargado de anunciar el torrent
"""
self._th.force_reannounce()
self._th.force_dht_announce()
def save_state(self):
"""
Servicio encargado de guardar el estado
"""
state = self._ses.save_state()
f = open(os.path.join(self.temp_path, self.state_file), 'wb')
pickle.dump(state, f)
f.close()
def _update_ready_pieces(self, alert_type, alert):
"""
Servicio encargado de informar que hay una pieza disponible
"""
if alert_type == 'read_piece_alert' and self.file:
self.file.update_piece(alert.piece, alert.buffer)
def _check_meta(self):
"""
Servicio encargado de comprobar si los metadatos se han descargado
"""
if self.status.state >= 3 and self.status.state <= 5 and not self.has_meta:
# Guardamos los metadatos
self.meta = self._th.get_torrent_info()
# Obtenemos la lista de archivos del meta
fs = self.meta.files()
if isinstance(fs, list):
files = fs
else:
files = [fs.at(i) for i in xrange(fs.num_files())]
# Guardamos la lista de archivos
self.files = self._find_files(files)
# Marcamos el primer archivo como activo
self.set_file(self.files[0])
# Damos por iniciada la descarga
self.start_time = time.time()
# Guardamos el .torrent en el cahce
self._cache.file_complete(self._th.get_torrent_info())
self.has_meta = True
def priorize_start_file(self):
'''
Servicio encargado de priorizar el principio y final de archivo cuando no hay conexion
'''
if self.file and not self.file.cursor:
num_start_pieces = self.buffer_size - self.last_pieces_priorize # Cantidad de piezas a priorizar al inicio
num_end_pieces = self.last_pieces_priorize # Canridad de piezas a priorizar al final
pieces_count = 0
# Priorizamos las ultimas piezas
for y in range(self.file.last_piece - num_end_pieces, self.file.last_piece + 1):
if not self._th.have_piece(y):
self.prioritize_piece(y, pieces_count)
pieces_count += 1
# Priorizamos las primeras piezas
for y in range(self.file.first_piece, self.file.last_piece + 1):
if not self._th.have_piece(y):
if pieces_count == self.buffer_size:
break
self.prioritize_piece(y, pieces_count)
pieces_count += 1
def print_status(self):
'''
Servicio encargado de mostrar en el log el estado de la descarga
'''
s = self.status
if self.file:
archivo = self.file.index
else:
archivo = "N/D"
logger.info(
'%.2f%% de %.1fMB %s | %.1f kB/s | #%s %d%% | AutoClose: %s | S: %d(%d) P: %d(%d)) | TRK: %d DHT: %d PEX: %d LSD %d | DHT:%s (%d) | Trakers: %d' % \
(s.progress_file, s.file_size, s.str_state, s._download_rate, archivo, s.buffer, s.timeout, s.num_seeds, \
s.num_complete, s.num_peers, s.num_incomplete, s.trk_peers, s.dht_peers, s.pex_peers, s.lsd_peers,
s.dht_state, s.dht_nodes, s.trackers))