286 lines
10 KiB
Python
286 lines
10 KiB
Python
import base64, json, random, struct, time, sys, traceback
|
|
if sys.version_info[0] >= 3:
|
|
PY3 = True
|
|
import urllib.request as urllib
|
|
xrange = range
|
|
else:
|
|
PY3 = False
|
|
import urllib
|
|
|
|
from core import httptools, jsontools, support
|
|
from threading import Thread
|
|
|
|
import re
|
|
|
|
from lib.streamingcommunity.handler import Handler
|
|
from platformcode import logger
|
|
from lib.streamingcommunity.server import Server
|
|
|
|
|
|
class Client(object):
|
|
|
|
def __init__(self, url, port=None, ip=None, auto_shutdown=True, wait_time=20, timeout=5, isPlaying_fnc=None, video_id=None):
|
|
|
|
self.port = port if port else random.randint(8000,8099)
|
|
self.ip = ip if ip else "127.0.0.1"
|
|
self.connected = False
|
|
self.start_time = None
|
|
self.last_connect = None
|
|
self.isPlaying_fnc = isPlaying_fnc
|
|
self.auto_shutdown = auto_shutdown
|
|
self.wait_time = wait_time
|
|
self.timeout = timeout
|
|
self.running = False
|
|
self.file = None
|
|
self.files = []
|
|
|
|
|
|
# video_id is the ID in the webpage path
|
|
self._video_id = video_id
|
|
|
|
# Get json_data for entire details from video page
|
|
jsonDataStr = httptools.downloadpage('https://streamingcommunityws.com/videos/1/{}'.format(self._video_id), CF=False ).data
|
|
logger.debug( jsonDataStr )
|
|
self._jsonData = jsontools.load( jsonDataStr )
|
|
|
|
# going to calculate token and expiration time
|
|
# These values will be used for manifests request
|
|
self._token, self._expires = self.calculateToken( self._jsonData['client_ip'] )
|
|
|
|
# Starting web server
|
|
self._server = Server((self.ip, self.port), Handler, client=self)
|
|
self.start()
|
|
|
|
|
|
|
|
def start(self):
|
|
"""
|
|
" Starting client and server in a separated thread
|
|
"""
|
|
self.start_time = time.time()
|
|
self.running = True
|
|
self._server.run()
|
|
t= Thread(target=self._auto_shutdown)
|
|
t.setDaemon(True)
|
|
t.start()
|
|
logger.info("SC Server Started", (self.ip, self.port))
|
|
|
|
def _auto_shutdown(self):
|
|
while self.running:
|
|
time.sleep(1)
|
|
if self.file and self.file.cursor:
|
|
self.last_connect = time.time()
|
|
|
|
if self.isPlaying_fnc and self.isPlaying_fnc():
|
|
self.last_connect = time.time()
|
|
|
|
if self.auto_shutdown:
|
|
#shudown por haber cerrado el reproductor
|
|
if self.connected and self.last_connect and self.isPlaying_fnc and not self.isPlaying_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 self.last_connect and not self.isPlaying_fnc:
|
|
if time.time() - self.last_connect - 1 > self.timeout:
|
|
self.stop()
|
|
|
|
def stop(self):
|
|
self.running = False
|
|
self._server.stop()
|
|
logger.info("SC Server Stopped")
|
|
|
|
|
|
def get_manifest_url(self):
|
|
# remap request path for main manifest
|
|
# it must point to local server ip:port
|
|
return "http://" + self.ip + ":" + str(self.port) + "/manifest.m3u8"
|
|
|
|
|
|
def get_main_manifest_content(self):
|
|
# get the manifest file for entire video/audio chunks
|
|
# it must remap each urls in order to catch all chunks
|
|
|
|
url = 'https://streamingcommunityws.com/master/{}?token={}&expires={}'.format(self._video_id, self._token, self._expires)
|
|
|
|
m3u8_original = httptools.downloadpage(url, CF=False).data
|
|
|
|
logger.debug('CLIENT: m3u8:', m3u8_original);
|
|
|
|
# remap video/audio manifests url
|
|
# they must point to local server:
|
|
# /video/RES.m3u8
|
|
# /audio/RES.m3u8
|
|
|
|
r_video = re.compile(r'(\.\/video\/(\d+p)\/playlist.m3u8)', re.MULTILINE)
|
|
r_audio = re.compile(r'(\.\/audio\/(\d+k)\/playlist.m3u8)', re.MULTILINE)
|
|
|
|
|
|
for match in r_video.finditer(m3u8_original):
|
|
line = match.groups()[0]
|
|
res = match.groups()[1]
|
|
videoUrl = "/video/" + res + ".m3u8"
|
|
|
|
# logger.info('replace', match.groups(), line, res, videoUrl)
|
|
|
|
m3u8_original = m3u8_original.replace( line, videoUrl )
|
|
|
|
|
|
for match in r_audio.finditer(m3u8_original):
|
|
line = match.groups()[0]
|
|
res = match.groups()[1]
|
|
audio_url = "/audio/" + res + ".m3u8"
|
|
|
|
# logger.info('replace', match.groups(), line, res, audio_url)
|
|
|
|
m3u8_original = m3u8_original.replace( line, audio_url )
|
|
|
|
|
|
# m_video = re.search(, m3u8_original)
|
|
# self._video_res = m_video.group(1)
|
|
# m_audio = re.search(r'\.\/audio\/(\d+k)\/playlist.m3u8', m3u8_original)
|
|
# self._audio_res = m_audio.group(1)
|
|
|
|
# videoUrl = "/video/" + self._video_res + ".m3u8"
|
|
# audio_url = "/audio/" + self._audio_res + ".m3u8"
|
|
|
|
# m3u8_original = m3u8_original.replace( m_video.group(0), videoUrl )
|
|
# m3u8_original = m3u8_original.replace( m_audio.group(0), audio_url )
|
|
|
|
return m3u8_original
|
|
|
|
|
|
def get_video_manifest_content(self, url):
|
|
"""
|
|
" Based on `default_start`, `default_count` and `default_domain`
|
|
" this method remap each video chunks url in order to make them point to
|
|
" the remote domain switching from `default_start` to `default_count` values
|
|
"""
|
|
|
|
m_video = re.search( r'\/video\/(\d+p)\.m3u8', url)
|
|
video_res = m_video.groups()[0]
|
|
|
|
logger.info('Video res: ', video_res)
|
|
|
|
# get the original manifest file for video chunks
|
|
url = 'https://streamingcommunityws.com/master/{}?token={}&expires={}&type=video&rendition={}'.format(self._video_id, self._token, self._expires, video_res)
|
|
original_manifest = httptools.downloadpage(url, CF=False).data
|
|
|
|
manifest_to_parse = original_manifest
|
|
|
|
# remap each chunks
|
|
r = re.compile(r'^(\w+\.ts)$', re.MULTILINE)
|
|
|
|
default_start = self._jsonData[ "proxies" ]["default_start"]
|
|
default_count = self._jsonData[ "proxies" ]["default_count"]
|
|
default_domain = self._jsonData[ "proxies" ]["default_domain"]
|
|
storage_id = self._jsonData[ "storage_id" ]
|
|
folder_id = self._jsonData[ "folder_id" ]
|
|
|
|
for match in r.finditer(manifest_to_parse):
|
|
# getting all single chunks and replace in the original manifest file content
|
|
ts = match.groups()[0]
|
|
|
|
# compute final url pointing to given domain
|
|
url = 'https://au-{default_start}.{default_domain}/hls/{storage_id}/{folder_id}/video/{video_res}/{ts}'.format(
|
|
default_start = default_start,
|
|
default_domain = default_domain,
|
|
storage_id = storage_id,
|
|
folder_id = folder_id,
|
|
video_res = video_res,
|
|
ts = ts
|
|
)
|
|
|
|
original_manifest = original_manifest.replace( ts, url )
|
|
|
|
default_start = default_start + 1
|
|
if default_start > default_count:
|
|
default_start = 1
|
|
|
|
# replace the encryption file url pointing to remote streamingcommunity server
|
|
original_manifest = re.sub(r'"(\/.*[enc]?\.key)"', '"https://streamingcommunityws.com\\1"', original_manifest)
|
|
|
|
return original_manifest
|
|
|
|
|
|
|
|
def get_audio_manifest_content(self, url):
|
|
"""
|
|
" Based on `default_start`, `default_count` and `default_domain`
|
|
" this method remap each video chunks url in order to make them point to
|
|
" the remote domain switching from `default_start` to `default_count` values
|
|
"""
|
|
m_audio = re.search( r'\/audio\/(\d+k)\.m3u8', url)
|
|
audio_res = m_audio.groups()[0]
|
|
|
|
logger.info('Audio res: ', audio_res)
|
|
|
|
# get the original manifest file for video chunks
|
|
url = 'https://streamingcommunityws.com/master/{}?token={}&expires={}&type=audio&rendition={}'.format(self._video_id, self._token, self._expires, audio_res)
|
|
original_manifest = httptools.downloadpage(url, CF=False).data
|
|
|
|
manifest_to_parse = original_manifest
|
|
|
|
# remap each chunks
|
|
r = re.compile(r'^(\w+\.ts)$', re.MULTILINE)
|
|
|
|
default_start = self._jsonData[ "proxies" ]["default_start"]
|
|
default_count = self._jsonData[ "proxies" ]["default_count"]
|
|
default_domain = self._jsonData[ "proxies" ]["default_domain"]
|
|
storage_id = self._jsonData[ "storage_id" ]
|
|
folder_id = self._jsonData[ "folder_id" ]
|
|
|
|
for match in r.finditer(manifest_to_parse):
|
|
# getting all single chunks and replace in the original manifest file content
|
|
ts = match.groups()[0]
|
|
|
|
# compute final url pointing to given domain
|
|
url = 'https://au-{default_start}.{default_domain}/hls/{storage_id}/{folder_id}/audio/{audio_res}/{ts}'.format(
|
|
default_start = default_start,
|
|
default_domain = default_domain,
|
|
storage_id = storage_id,
|
|
folder_id = folder_id,
|
|
audio_res = audio_res,
|
|
ts = ts
|
|
)
|
|
|
|
original_manifest = original_manifest.replace( ts, url )
|
|
|
|
default_start = default_start + 1
|
|
if default_start > default_count:
|
|
default_start = 1
|
|
|
|
|
|
# replace the encryption file url pointing to remote streamingcommunity server
|
|
original_manifest = re.sub(r'"(\/.*[enc]?\.key)"', '"https://streamingcommunityws.com\\1"', original_manifest)
|
|
|
|
return original_manifest
|
|
|
|
|
|
def calculateToken(self, ip):
|
|
"""
|
|
" Compute the `token` and the `expires` values in order to perform each next requests
|
|
"""
|
|
|
|
from time import time
|
|
from base64 import b64encode as b64
|
|
import hashlib
|
|
o = 48
|
|
|
|
# NOT USED: it has been computed by `jsondata` in the constructor method
|
|
# n = support.match('https://au-1.scws-content.net/get-ip').data
|
|
|
|
i = 'Yc8U6r8KjAKAepEA'
|
|
t = int(time() + (3600 * o))
|
|
l = '{}{} {}'.format(t, ip, i)
|
|
md5 = hashlib.md5(l.encode())
|
|
#s = '?token={}&expires={}'.format(, t)
|
|
token = b64( md5.digest() ).decode().replace( '=', '' ).replace( '+', "-" ).replace( '\\', "_" )
|
|
expires = t
|
|
return token, expires
|