diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8b377b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +.project +.pydevproject +.settings/ +.idea/ diff --git a/aceclient/aceclient.py b/aceclient/aceclient.py index a6ff115..69c8463 100644 --- a/aceclient/aceclient.py +++ b/aceclient/aceclient.py @@ -4,8 +4,14 @@ import telnetlib import logging import json +import time +import threading +import traceback +import Queue +from collections import deque from acemessages import * - +import aceconfig +from aceconfig import AceConfig class AceException(Exception): @@ -49,15 +55,24 @@ def __init__(self, host, port, connect_timeout=5, result_timeout=10): self._authevent = Event() # Result for getURL() self._urlresult = AsyncResult() + # Result for GETCID() + self._cidresult = AsyncResult() # Event for resuming from PAUSE self._resumeevent = Event() # Seekback seconds. self._seekback = 0 # Did we get START command again? For seekback. self._started_again = False + + self._idleSince = time.time() + self._lock = threading.Condition(threading.Lock()) + self._streamReaderConnection = None + self._streamReaderState = None + self._streamReaderQueue = deque() + self._engine_version_code = 0; # Logger - logger = logging.getLogger('AceClient_init') + logger = logging.getLogger('AceClieimport tracebacknt_init') try: self._socket = telnetlib.Telnet(host, port, connect_timeout) @@ -100,8 +115,15 @@ def destroy(self): finally: self._shuttingDown.set() + def reset(self): + self._started_again = False + self._idleSince = time.time() + self._streamReaderState = None + def _write(self, message): try: + logger = logging.getLogger("AceClient_write") + logger.debug(message) self._socket.write(message + "\r\n") except EOFError as e: raise AceException("Write error! " + repr(e)) @@ -136,18 +158,12 @@ def aceInit(self, gender=AceConst.SEX_MALE, age=AceConst.AGE_18_24, product_key= def _getResult(self): # Logger - logger = logging.getLogger("AceClient_START") - try: result = self._result.get(timeout=self._resulttimeout) if not result: - errmsg = "START error!" - logger.error(errmsg) - raise AceException(errmsg) + raise AceException("Result not received") except gevent.Timeout: - errmsg = "START timeout!" - logger.error(errmsg) - raise AceException(errmsg) + raise AceException("Timeout") return result @@ -155,16 +171,37 @@ def START(self, datatype, value): ''' Start video method ''' - self._result = AsyncResult() + if self._engine_version_code >= 3010500 and AceConfig.vlcuse: + stream_type = 'output_format=hls' + ' transcode_audio=' + str(AceConfig.transcode_audio) \ + + ' transcode_mp3=' + str(AceConfig.transcode_mp3) \ + + ' transcode_ac3=' + str(AceConfig.transcode_ac3) + else: + stream_type = 'output_format=http' + self._urlresult = AsyncResult() + self._write(AceMessage.request.START(datatype.upper(), value, stream_type)) + self._getResult() - self._write(AceMessage.request.LOADASYNC(datatype.upper(), 0, value)) - contentinfo = self._getResult() + def STOP(self): + ''' + Stop video method + ''' + if self._state is not None and self._state != '0': + self._result = AsyncResult() + self._write(AceMessage.request.STOP) + self._getResult() - self._write(AceMessage.request.START(datatype.upper(), value)) - self._getResult() + def LOADASYNC(self, datatype, url): + self._result = AsyncResult() + self._write(AceMessage.request.LOADASYNC(datatype.upper(), 0, {'url': url})) + return self._getResult() - return contentinfo + def GETCID(self, datatype, url): + contentinfo = self.LOADASYNC(datatype, url) + self._cidresult = AsyncResult() + self._write(AceMessage.request.GETCID(contentinfo.get('checksum'), contentinfo.get('infohash'), 0, 0, 0)) + cid = self._cidresult.get(True, 5) + return '' if not cid or cid == '' else cid[2:] def getUrl(self, timeout=40): # Logger @@ -178,6 +215,79 @@ def getUrl(self, timeout=40): logger.error(errmsg) raise AceException(errmsg) + def startStreamReader(self, url, cid, counter): + logger = logging.getLogger("StreamReader") + self._streamReaderState = 1 + logger.debug("Opening video stream: %s" % url) + + try: + connection = self._streamReaderConnection = urllib2.urlopen(url) + + if url.endswith('.m3u8'): + logger.debug("Can't stream HLS in non VLC mode: %s" % url) + return + + if connection.getcode() != 200: + logger.error("Failed to open video stream %s" % connection) + return + + with self._lock: + self._streamReaderState = 2 + self._lock.notifyAll() + + while True: + data = None + clients = counter.getClients(cid) + + try: + data = connection.read(AceConfig.readchunksize) + except: + break; + + if data and clients: + with self._lock: + if len(self._streamReaderQueue) == AceConfig.readcachesize: + self._streamReaderQueue.popleft() + self._streamReaderQueue.append(data) + + for c in clients: + try: + c.addChunk(data, 5.0) + except Queue.Full: + if len(clients) > 1: + logger.debug("Disconnecting client: %s" % str(c)) + c.destroy() + elif not clients: + logger.debug("All clients disconnected - closing video stream") + break + else: + logger.warning("No data received") + break + except urllib2.URLError: + logger.error("Failed to open video stream") + logger.error(traceback.format_exc()) + except: + logger.error(traceback.format_exc()) + if counter.getClients(cid): + logger.error("Failed to read video stream") + finally: + self.closeStreamReader() + with self._lock: + self._streamReaderState = 3 + self._lock.notifyAll() + counter.deleteAll(cid) + + def closeStreamReader(self): + logger = logging.getLogger("StreamReader") + c = self._streamReaderConnection + + if c: + self._streamReaderConnection = None + c.close() + logger.debug("Video stream closed") + + self._streamReaderQueue.clear() + def getPlayEvent(self, timeout=None): ''' Blocking while in PAUSE, non-blocking while in RESUME @@ -201,7 +311,7 @@ def _recvData(self): try: self._recvbuffer = self._socket.read_until("\r\n") self._recvbuffer = self._recvbuffer.strip() - #logger.debug('<<< ' + self._recvbuffer) + # logger.debug('<<< ' + self._recvbuffer) except: # If something happened during read, abandon reader. if not self._shuttingDown.isSet(): @@ -213,10 +323,14 @@ def _recvData(self): # Parsing everything only if the string is not empty if self._recvbuffer.startswith(AceMessage.response.HELLO): # Parse HELLO + if 'version_code=' in self._recvbuffer: + v = self._recvbuffer.find('version_code=') + self._engine_version_code = int(self._recvbuffer[v+13:v+20]) + if 'key=' in self._recvbuffer: self._request_key_begin = self._recvbuffer.find('key=') self._request_key = \ - self._recvbuffer[self._request_key_begin+4:self._request_key_begin+14] + self._recvbuffer[self._request_key_begin + 4:self._request_key_begin + 14] try: self._write(AceMessage.request.READY_key( self._request_key, self._product_key)) @@ -246,12 +360,11 @@ def _recvData(self): self._result.set(False) else: logger.debug("Content info: %s", _contentinfo) - _filename = urllib2.unquote(_contentinfo.get('files')[0][0]) - self._result.set(_filename) + self._result.set(_contentinfo) elif self._recvbuffer.startswith(AceMessage.response.START): # START - if not self._seekback or (self._seekback and self._started_again): + if not self._seekback or self._started_again or not self._recvbuffer.endswith(' stream=1'): # If seekback is disabled, we use link in first START command. # If seekback is enabled, we wait for first START command and # ignore it, then do seeback in first EVENT position command @@ -263,6 +376,8 @@ def _recvData(self): self._resumeevent.set() except IndexError as e: self._url = None + else: + logger.debug("START received. Waiting for %s." % AceMessage.response.LIVEPOS) elif self._recvbuffer.startswith(AceMessage.response.STOP): pass @@ -290,10 +405,7 @@ def _recvData(self): self._position_last = self._position[2].split('=')[1] self._position_buf = self._position[9].split('=')[1] self._position = self._position[4].split('=')[1] - logger.debug('Current position/last/buf: %s/%s/%s' % (self._position, - self._position_last, - self._position_buf) - ) + if self._seekback and not self._started_again: self._write(AceMessage.request.SEEK(str(int(self._position_last) - \ self._seekback))) @@ -318,6 +430,8 @@ def _recvData(self): AceException(self._status + ' with message ' + self._recvbuffer.split(';')[2])) elif self._status == 'main:starting': self._result.set(True) + elif self._status == 'main:idle': + self._result.set(True) elif self._recvbuffer.startswith(AceMessage.response.PAUSE): logger.debug("PAUSE event") @@ -327,3 +441,7 @@ def _recvData(self): logger.debug("RESUME event") gevent.sleep(self._pausedelay) self._resumeevent.set() + + elif self._recvbuffer.startswith('##') or len(self._recvbuffer) == 0: + self._cidresult.set(self._recvbuffer) + logger.debug("CID: %s" %self._recvbuffer) diff --git a/aceclient/acemessages.py b/aceclient/acemessages.py index 3994605..1b0827c 100644 --- a/aceclient/acemessages.py +++ b/aceclient/acemessages.py @@ -5,8 +5,7 @@ import hashlib import platform import urllib2 - - + class AceConst(object): APIVERSION = 3 @@ -78,7 +77,7 @@ def LOADASYNC(command, request_id, params_dict): # End LOADASYNC @staticmethod - def START(command, params_dict): + def START(command, params_dict, stream_type): if command == 'TORRENT': return 'START TORRENT ' + str(params_dict.get('url')) + ' ' + \ str(params_dict.get('file_indexes', '0')) + ' ' + \ @@ -92,12 +91,11 @@ def START(command, params_dict): str(params_dict.get('file_indexes', '0')) + ' ' + \ str(params_dict.get('developer_id', '0')) + ' ' + \ str(params_dict.get('affiliate_id', '0')) + ' ' + \ - str(params_dict.get('zone_id', '0')) + ' ' + \ - str(params_dict.get('stream_id', '0')) + str(params_dict.get('zone_id', '0')) elif command == 'PID': return 'START PID ' + str(params_dict.get('content_id')) + ' ' + \ - str(params_dict.get('file_indexes', '0')) + str(params_dict.get('file_indexes', '0')) + ' ' + stream_type elif command == 'RAW': return 'START RAW ' + str(params_dict.get('data')) + ' ' + \ diff --git a/aceclient/clientcounter.py b/aceclient/clientcounter.py index 01ad6f5..bb5af56 100644 --- a/aceclient/clientcounter.py +++ b/aceclient/clientcounter.py @@ -1,55 +1,143 @@ ''' Simple Client Counter for VLC VLM ''' - +import threading +import logging +import time +import aceclient +import gevent +from aceconfig import AceConfig class ClientCounter(object): def __init__(self): + self.lock = threading.RLock() self.clients = dict() - self.aces = dict() + self.idleace = None self.total = 0 + gevent.spawn(self.checkIdle) + + def count(self, cid): + with self.lock: + clients = self.clients.get(cid) + return len(clients) if clients else 0 + + def getClients(self, cid): + with self.lock: + return self.clients.get(cid) - def get(self, id): - return self.clients.get(id, (False,))[0] - - def add(self, id, ip): - if self.clients.has_key(id): - self.clients[id][0] += 1 - self.clients[id][1].append(ip) - else: - self.clients[id] = [1, [ip]] - - self.total += 1 - return self.clients[id][0] - - def delete(self, id, ip): - if self.clients.has_key(id): - self.total -= 1 - if self.clients[id][0] == 1: - del self.clients[id] - return False + def add(self, cid, client): + with self.lock: + clients = self.clients.get(cid) + + if clients: + client.ace = clients[0].ace + with client.ace._lock: + client.queue.extend(client.ace._streamReaderQueue) + clients.append(client) else: - self.clients[id][0] -= 1 - self.clients[id][1].remove(ip) - else: - return False - - return self.clients[id][0] - - def getAce(self, id): - return self.aces.get(id, False) + if self.idleace: + client.ace = self.idleace + self.idleace = None + else: + try: + client.ace = self.createAce() + except Exception as e: + logging.error('Failed to create AceClient: ' + repr(e)) + raise e + + clients = [client] + self.clients[cid] = clients + + self.total += 1 + return len(clients) - def addAce(self, id, value): - if self.aces.has_key(id): - return False + def delete(self, cid, client): + with self.lock: + if not self.clients.has_key(cid): + return 0 + + clients = self.clients[cid] + + if client not in clients: + return len(clients) + + try: + if len(clients) > 1: + clients.remove(client) + return len(clients) + else: + del self.clients[cid] + clients[0].ace.closeStreamReader() + + if self.idleace: + client.ace.destroy() + else: + try: + client.ace.STOP() + self.idleace = client.ace + self.idleace.reset() + except: + client.ace.destroy() + + return 0 + finally: + self.total -= 1 - self.aces[id] = value - return True + def deleteAll(self, cid): + clients = None + + try: + with self.lock: + if not self.clients.has_key(cid): + return + + clients = self.clients[cid] + del self.clients[cid] + self.total -= len(clients) + clients[0].ace.closeStreamReader() + + if self.idleace: + clients[0].ace.destroy() + else: + try: + clients[0].ace.STOP() + self.idleace = clients[0].ace + self.idleace.reset() + except: + clients[0].ace.destroy() + finally: + if clients: + for c in clients: + c.destroy() - def deleteAce(self, id): - if not self.aces.has_key(id): - return False + def destroyIdle(self): + with self.lock: + try: + if self.idleace: + self.idleace.destroy() + finally: + self.idleace = None - del self.aces[id] - return True + def createAce(self): + logger = logging.getLogger('createAce') + ace = aceclient.AceClient( + AceConfig.acehost, AceConfig.aceport, connect_timeout=AceConfig.aceconntimeout, + result_timeout=AceConfig.aceresulttimeout) + logger.debug("AceClient created") + ace.aceInit( + gender=AceConfig.acesex, age=AceConfig.aceage, + product_key=AceConfig.acekey, pause_delay=AceConfig.videopausedelay, + seekback=AceConfig.videoseekback) + logger.debug("AceClient inited") + return ace + + def checkIdle(self): + while(True): + gevent.sleep(60.0) + with self.lock: + ace = self.idleace + if ace and (ace._idleSince + 60.0 <= time.time()): + self.idleace = None + ace.destroy() + diff --git a/aceconfig.py b/aceconfig.py index 3689f6e..ff8882b 100644 --- a/aceconfig.py +++ b/aceconfig.py @@ -42,9 +42,7 @@ class AceConfig(acedefconfig.AceDefConfig): # Ace Stream Engine connection timeout aceconntimeout = 5 # Ace Stream Engine authentication result timeout - aceresulttimeout = 10 - # Message level (DEBUG, INFO, WARNING, ERROR, CRITICAL) - debug = logging.DEBUG + aceresulttimeout = 5 # # ---------------------------------------------------- # AceProxy configuration @@ -54,6 +52,10 @@ class AceConfig(acedefconfig.AceDefConfig): httphost = '0.0.0.0' # HTTP Server port httpport = 8000 + # Read the video input stream in chunks of the following size + readchunksize = 8192 + # Cache the following number of the tailing chunks + readcachesize = 1000 # If started as root, drop privileges to this user. # Leave empty to disable. aceproxyuser = '' @@ -69,10 +71,6 @@ class AceConfig(acedefconfig.AceDefConfig): ) # Maximum concurrent connections (video clients) maxconns = 10 - # Logging to a file - loggingtoafile = False - # Path for logs, default is current directory. For example '/tmp/' - logpath = '' # # ---------------------------------------------------- # VLC configuration @@ -95,10 +93,10 @@ class AceConfig(acedefconfig.AceDefConfig): # to point ace_player.exe, not vlc.exe!!! vlcuseaceplayer = False # Spawn VLC automaticaly - vlcspawn = False + vlcspawn = True # VLC cmd line (use `--file-logging --logfile=filepath` to write log) # Please use the full path to executable for Windows, for example - C:\\Program Files\\VideoLAN\\VLC\\vlc.exe - vlccmd = "vlc -I telnet --clock-jitter -1 --network-caching -1 --sout-mux-caching 2000 --telnet-password admin --telnet-port 4212" + vlccmd = 'vlc -I telnet --clock-jitter=0 --clock-synchro=0 --telnet-password admin --telnet-port 4212' # VLC spawn timeout # Adjust this if you get error 'Cannot spawn VLC!' vlcspawntimeout = 5 @@ -120,26 +118,59 @@ class AceConfig(acedefconfig.AceDefConfig): # ffmpeg{mux=NAME} (i.e. ffmpeg{mux=mpegts}) # VLC's ts muxer sometimes can work badly, but that's the best choice for # now. - vlcmux = 'ts' + vlcmux = 'ts{use-key-frames}' # Force ffmpeg INPUT demuxer in VLC. Sometimes can help. vlcforceffmpeg = False # Stream start delay for dumb players (in seconds) # !!! # PLEASE set this to 0 if you use VLC # !!! - videodelay = 2 + # + # ---------------------------------------------------- + # Transcoding configuration + # ---------------------------------------------------- + # Enable/disable transcoding + transcode = False + # Dictionary with a set of transcoding commands. Transcoding command is an + # executable commandline expression that reads an input stream from STDIN + # and writes a transcoded stream to STDOUT. The commands are selected + # according to the value of the 'fmt' request parameter. For example, the + # following url: + # http://loclahost:8000/channels/?type=m3u&fmt=mp2 + # contains the fmt=mp2. It means that the 'mp2' command will be used for + # transcoding. You may add any number of commands to this dictionary. + transcodecmd = dict() + # transcodecmd['100k'] = 'ffmpeg -i - -c:a copy -b 100k -f mpegts -' + # transcodecmd['mp2'] = 'ffmpeg -i - -c:a mp2 -c:v mpeg2video -f mpegts -qscale:v 2 -'.split() + # transcodecmd['mkv'] = 'ffmpeg -i - -c:a copy -c:v copy -f matroska -'.split() + # transcodecmd['default'] = 'ffmpeg -i - -c:a copy -c:v copy -f mpegts -'.split() + # ---------------------------------------------------- + # Transcoding configuration for HLS + # ---------------------------------------------------- + # If you use acestream engine ver >= 3.1.5 and vlcuse=True + # proxy automaticaly switch to HLS (HTTP Live Streaming) instead of HTTP Progressive Download + # You can use this settings for audio transcoding. This option applies only for Live-stream + # --------------------------------------------------- + # Transcode All audio to AAC + transcode_audio = 0 + # Transcode MP3 (use only when transcode_audio=1) + transcode_mp3 = 0 + # Transcode only AC3 to AAC (use only when transcode_audio=0) + transcode_ac3 = 0 + # ---------------------------------------------------- + videodelay = 0 # Obey PAUSE and RESUME commands from Engine # (stops sending data to client, should prevent annoying buffering) # !!! # PLEASE set this to False if you use VLC # !!! - videoobey = True + videoobey = False # Stream send delay after PAUSE/RESUME commands (works only if option # above is enabled) # !!! # PLEASE set this to 0 if you use VLC # !!! - videopausedelay = 2 + videopausedelay = 0 # Seek back feature. # Seeks stream back for specified amount of seconds. # Greatly helps fighing AceSteam lags, but introduces @@ -149,7 +180,7 @@ class AceConfig(acedefconfig.AceDefConfig): videoseekback = 0 # Delay before closing Ace Stream connection when client disconnects # In seconds. - videodestroydelay = 3 + videodestroydelay = 0 # Pre-buffering timeout. In seconds. videotimeout = 40 # @@ -165,3 +196,29 @@ class AceConfig(acedefconfig.AceDefConfig): fakeheaderuas = ('HLS Client/2.0 (compatible; LG NetCast.TV-2012)', 'Mozilla/5.0 (DirectFB; Linux armv7l) AppleWebKit/534.26+ (KHTML, like Gecko) Version/5.0 Safari/534.26+ LG Browser/5.00.00(+mouse+3D+SCREEN+TUNER; LGE; 42LM670T-ZA; 04.41.03; 0x00000001;); LG NetCast.TV-2012 0' ) + + # Logging configuration + # + # Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + loglevel = logging.DEBUG + # Log message format + logfmt = '%(filename)-20s [LINE:%(lineno)-4s]# %(levelname)-8s [%(asctime)s] %(message)s' + # Log date format + logdatefmt='%d.%m %H:%M:%S' + # Full path to a log file + logfile = None + + # This method is used to detect fake requests. Some players send such + # requests in order to detect the MIME type and/or check the stream availability. + @staticmethod + def isFakeRequest(path, params, headers): + useragent = headers.get('User-Agent') + + if not useragent: + return False + elif useragent in AceConfig.fakeuas: + return True + elif useragent == 'Lavf/55.33.100' and not headers.has_key('Range'): + return True + elif useragent == 'GStreamer souphttpsrc (compatible; LG NetCast.TV-2013) libsoup/2.34.2' and headers.get('icy-metadata') != '1': + return True diff --git a/acedefconfig.py b/acedefconfig.py index c9a35d1..745edfc 100644 --- a/acedefconfig.py +++ b/acedefconfig.py @@ -20,7 +20,6 @@ class AceDefConfig(object): acestartuptimeout = 10 aceconntimeout = 5 aceresulttimeout = 10 - debug = logging.DEBUG # httphost = '0.0.0.0' httpport = 8000 @@ -32,8 +31,6 @@ class AceDefConfig(object): '192.168.0.0/16', ) maxconns = 10 - loggingtoafile = False - logpath = '' vlcuse = False vlcuseaceplayer = False vlcspawn = False diff --git a/acehttp.py b/acehttp.py index 9c9446f..84e5d07 100755 --- a/acehttp.py +++ b/acehttp.py @@ -8,6 +8,7 @@ import traceback import gevent import gevent.monkey +from gevent.queue import Full # Monkeypatching and all the stuff gevent.monkey.patch_all() @@ -17,12 +18,20 @@ import sys import logging import psutil +from subprocess import PIPE import BaseHTTPServer import SocketServer from socket import error as SocketException from socket import SHUT_RDWR +from collections import deque +import base64 +import json +import time +import threading import urllib2 -import hashlib +import urllib +import urlparse +import Queue import aceclient import aceconfig from aceconfig import AceConfig @@ -42,7 +51,20 @@ class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler): requestlist = [] - + + def log_message(self, format, *args): + logger.info("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + urllib.unquote(format%args).decode('UTF-8'))) + + def log_request(self, code='-', size='-'): + logger.debug('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, format, *args): + logger.error(format, *args) + def handle_one_request(self): ''' Add request to requestlist, handle request and remove from the list @@ -55,8 +77,8 @@ def closeConnection(self): ''' Disconnecting client ''' - if self.clientconnected: - self.clientconnected = False + if self.connected: + self.connected = False try: self.wfile.close() self.rfile.close() @@ -64,15 +86,19 @@ def closeConnection(self): except: pass - def dieWithError(self, errorcode=500): + def dieWithError(self, errorcode=500, logmsg='Dying with error', loglevel=logging.WARN): ''' Close connection with error ''' - logging.warning("Dying with error") - if self.clientconnected: - self.send_error(errorcode) - self.end_headers() - self.closeConnection() + if logmsg: + logging.log(loglevel, logmsg) + if self.connected: + try: + self.send_error(errorcode) + self.end_headers() + self.closeConnection() + except: + pass def proxyReadWrite(self): ''' @@ -88,15 +114,15 @@ def proxyReadWrite(self): while True: if AceConfig.videoobey and not AceConfig.vlcuse: # Wait for PlayEvent if videoobey is enabled. Not for VLC - self.ace.getPlayEvent() - - if AceConfig.videoobey and AceConfig.vlcuse: - # For VLC - # Waiting 0.5 seconds. If timeout exceeded (and the Play event - # flag is not set), pause the stream if AceEngine says so and - # we should obey it. - # A bit ugly, huh? - self.streamstate = self.ace.getPlayEvent(0.5) + self.client.ace.getPlayEvent() + + if AceConfig.vlcuse: + # Ignor videoobey settings when use VLC. For VLC + # waiting 0.5 seconds. If timeout exceeded (and the Play event + # flag is not set), pause the stream if AceEngine says so and + # we should obey it. + # A bit ugly, huh? + self.streamstate = self.client.ace.getPlayEvent(0.5) if self.streamstate and not self.vlcstate: AceStuff.vlcclient.playBroadcast(self.vlcid) self.vlcstate = True @@ -106,22 +132,24 @@ def proxyReadWrite(self): AceStuff.vlcclient.pauseBroadcast(self.vlcid) self.vlcstate = False - if not self.clientconnected: + if not self.connected: logger.debug("Client is not connected, terminating") break data = self.video.read(4096) - if data and self.clientconnected: + if data and self.connected: self.wfile.write(data) else: - logger.warning("Video connection closed") + if self.connected: + logger.warning("Video connection closed") break except SocketException: # Video connection dropped - logger.warning("Video connection dropped") + if self.connected: + logger.warning("Video connection dropped") finally: self.video.close() - self.closeConnection() + self.client.destroy() def hangDetector(self): ''' @@ -136,15 +164,14 @@ def hangDetector(self): except: pass finally: - self.clientconnected = False logger.debug("Client disconnected") - try: - self.requestgreenlet.kill() - except: - pass - finally: - gevent.sleep() - return + client = self.client + video = self.video + + if client: + client.destroy() + if video: + video.close() def do_HEAD(self): return self.do_GET(headers_only=True) @@ -153,8 +180,9 @@ def do_GET(self, headers_only=False): ''' GET request handler ''' - logger = logging.getLogger('http_HTTPHandler') - self.clientconnected = True + logger = logging.getLogger('do_GET') + self.reqtime = time.time() + self.connected = True # Don't wait videodestroydelay if error happened self.errorhappened = True # Headers sent flag for fake headers UAs @@ -162,7 +190,8 @@ def do_GET(self, headers_only=False): # Current greenlet self.requestgreenlet = gevent.getcurrent() # Connected client IP address - self.clientip = self.request.getpeername()[0] + self.clientip = self.headers['X-Forwarded-For'] \ + if self.headers.has_key('X-Forwarded-For') else self.request.getpeername()[0] if AceConfig.firewall: # If firewall enabled @@ -176,7 +205,7 @@ def do_GET(self, headers_only=False): self.dieWithError(403) # 403 Forbidden return - logger.info("Accepted connection from " + self.clientip + " path " + self.path) + logger.info("Accepted connection from " + self.clientip + " path " + urllib.unquote(self.path).decode('UTF-8')) try: self.splittedpath = self.path.split('/') @@ -193,7 +222,7 @@ def do_GET(self, headers_only=False): # Handle request with plugin handler if self.reqtype in AceStuff.pluginshandlers: try: - AceStuff.pluginshandlers.get(self.reqtype).handle(self) + AceStuff.pluginshandlers.get(self.reqtype).handle(self, headers_only) except Exception as e: logger.error('Plugin exception: ' + repr(e)) logger.error(traceback.format_exc()) @@ -203,7 +232,13 @@ def do_GET(self, headers_only=False): return self.handleRequest(headers_only) - def handleRequest(self, headers_only): + def handleRequest(self, headers_only, channelName=None, channelIcon=None, fmt=None): + logger = logging.getLogger('handleRequest') + logger.debug("Headers:\n" + str(self.headers)) + self.requrl = urlparse.urlparse(self.path) + self.reqparams = urlparse.parse_qs(self.requrl.query) + self.path = self.requrl.path[:-1] if self.requrl.path.endswith('/') else self.requrl.path + # Check if third parameter exists # …/pid/blablablablabla/video.mpg # |_________| @@ -223,20 +258,19 @@ def handleRequest(self, headers_only): self.dieWithError(503) # 503 Service Unavailable return - # Pretend to work fine with Fake UAs or HEAD request. - useragent = self.headers.get('User-Agent') - fakeua = useragent and useragent in AceConfig.fakeuas - if headers_only or fakeua: - if fakeua: - logger.debug("Got fake UA: " + self.headers.get('User-Agent')) + # Pretend to work fine with Fake or HEAD request. + if headers_only or AceConfig.isFakeRequest(self.path, self.reqparams, self.headers): # Return 200 and exit + if headers_only: + logger.debug("Sending headers and closing connection") + else: + logger.debug("Fake request - closing connection") self.send_response(200) self.send_header("Content-Type", "video/mpeg") self.end_headers() self.closeConnection() return - self.path_unquoted = urllib2.unquote(self.splittedpath[2]) # Make list with parameters self.params = list() for i in xrange(3, 8): @@ -244,87 +278,39 @@ def handleRequest(self, headers_only): self.params.append(int(self.splittedpath[i])) except (IndexError, ValueError): self.params.append('0') - - # Adding client to clientcounter - clients = AceStuff.clientcounter.add(self.path_unquoted, self.clientip) - # If we are the one client, but sucessfully got ace from clientcounter, - # then somebody is waiting in the videodestroydelay state - self.ace = AceStuff.clientcounter.getAce(self.path_unquoted) - if not self.ace: - shouldcreateace = True - else: - shouldcreateace = False - - # Use PID as VLC ID if PID requested - # Or torrent url MD5 hash if torrent requested - if self.reqtype == 'pid': - self.vlcid = self.path_unquoted - else: - self.vlcid = hashlib.md5(self.path_unquoted).hexdigest() - - # If we don't use VLC and we're not the first client - if clients != 1 and not AceConfig.vlcuse: - AceStuff.clientcounter.delete(self.path_unquoted, self.clientip) - logger.error( - "Not the first client, cannot continue in non-VLC mode") - self.dieWithError(503) # 503 Service Unavailable - return - - if shouldcreateace: - # If we are the only client, create AceClient - try: - self.ace = aceclient.AceClient( - AceConfig.acehost, AceConfig.aceport, connect_timeout=AceConfig.aceconntimeout, - result_timeout=AceConfig.aceresulttimeout) - # Adding AceClient instance to pool - AceStuff.clientcounter.addAce(self.path_unquoted, self.ace) - logger.debug("AceClient created") - except aceclient.AceException as e: - logger.error("AceClient create exception: " + repr(e)) - AceStuff.clientcounter.delete( - self.path_unquoted, self.clientip) - self.dieWithError(502) # 502 Bad Gateway - return - - # Send fake headers if this User-Agent is in fakeheaderuas tuple - if fakeua: - logger.debug( - "Sending fake headers for " + useragent) - self.send_response(200) - self.send_header("Content-Type", "video/mpeg") - self.end_headers() - # Do not send real headers at all - self.headerssent = True + + self.url = None + self.video = None + self.path_unquoted = urllib2.unquote(self.splittedpath[2]) + contentid = self.getCid(self.reqtype, self.path_unquoted) + cid = contentid if contentid else self.path_unquoted + logger.debug("CID: " + cid) + self.vlcid = urllib2.quote(cid, '') + self.client = Client(cid, self, channelName, channelIcon) + shouldStart = AceStuff.clientcounter.add(cid, self.client) == 1 try: - self.hanggreenlet = gevent.spawn(self.hangDetector) - logger.debug("hangDetector spawned") - gevent.sleep() - # Initializing AceClient - if shouldcreateace: - self.ace.aceInit( - gender=AceConfig.acesex, age=AceConfig.aceage, - product_key=AceConfig.acekey, pause_delay=AceConfig.videopausedelay, - seekback=AceConfig.videoseekback) - logger.debug("AceClient inited") - if self.reqtype == 'pid': - contentinfo = self.ace.START( + if shouldStart: + if contentid: + self.client.ace.START('PID', {'content_id': contentid}) + elif self.reqtype == 'pid': + self.client.ace.START( self.reqtype, {'content_id': self.path_unquoted, 'file_indexes': self.params[0]}) elif self.reqtype == 'torrent': paramsdict = dict( zip(aceclient.acemessages.AceConst.START_TORRENT, self.params)) paramsdict['url'] = self.path_unquoted - contentinfo = self.ace.START(self.reqtype, paramsdict) + self.client.ace.START(self.reqtype, paramsdict) logger.debug("START done") + # Getting URL + self.url = self.client.ace.getUrl(AceConfig.videotimeout) + # Rewriting host for remote Ace Stream Engine + self.url = self.url.replace('127.0.0.1', AceConfig.acehost) - # Getting URL - self.url = self.ace.getUrl(AceConfig.videotimeout) - # Rewriting host for remote Ace Stream Engine - self.url = self.url.replace('127.0.0.1', AceConfig.acehost) self.errorhappened = False - if shouldcreateace: + if shouldStart: logger.debug("Got url " + self.url) # If using VLC, add this url to VLC if AceConfig.vlcuse: @@ -333,61 +319,59 @@ def handleRequest(self, headers_only): self.vlcprefix = 'http/ffmpeg://' else: self.vlcprefix = '' + AceStuff.vlcclient.startBroadcast( + self.vlcid, self.vlcprefix + self.url, AceConfig.vlcmux, AceConfig.vlcpreaccess) + # Sleep a bit, because sometimes VLC doesn't open port in time + gevent.sleep(0.5) - self.ace.pause() + if not AceConfig.vlcuse: + self.client.ace.pause() # Sleeping videodelay gevent.sleep(AceConfig.videodelay) - self.ace.play() - AceStuff.vlcclient.startBroadcast( - self.vlcid, self.vlcprefix + self.url, AceConfig.vlcmux, AceConfig.vlcpreaccess) - # Sleep a bit, because sometimes VLC doesn't open port in - # time - gevent.sleep(0.5) + self.client.ace.play() + + self.hanggreenlet = gevent.spawn(self.hangDetector) + logger.debug("hangDetector spawned") + gevent.sleep() # Building new VLC url if AceConfig.vlcuse: self.url = 'http://' + AceConfig.vlchost + \ ':' + str(AceConfig.vlcoutport) + '/' + self.vlcid logger.debug("VLC url " + self.url) - - # Sending client headers to videostream - self.video = urllib2.Request(self.url) - for key in self.headers.dict: - self.video.add_header(key, self.headers.dict[key]) - - self.video = urllib2.urlopen(self.video) - - # Sending videostream headers to client - if not self.headerssent: - self.send_response(self.video.getcode()) - if self.video.info().dict.has_key('connection'): - del self.video.info().dict['connection'] - if self.video.info().dict.has_key('server'): - del self.video.info().dict['server'] - if self.video.info().dict.has_key('transfer-encoding'): - del self.video.info().dict['transfer-encoding'] - if self.video.info().dict.has_key('keep-alive'): - del self.video.info().dict['keep-alive'] - - for key in self.video.info().dict: - self.send_header(key, self.video.info().dict[key]) - # End headers. Next goes video data - self.end_headers() - logger.debug("Headers sent") - - if not AceConfig.vlcuse: - self.ace.pause() - # Sleeping videodelay - gevent.sleep(AceConfig.videodelay) - self.ace.play() - - # Run proxyReadWrite - self.proxyReadWrite() - - # Waiting until hangDetector is joined - self.hanggreenlet.join() - logger.debug("Request handler finished") + + # Sending client headers to videostream + self.video = urllib2.Request(self.url) + for key in self.headers.dict: + self.video.add_header(key, self.headers.dict[key]) + + self.video = urllib2.urlopen(self.video) + + # Sending videostream headers to client + if not self.headerssent: + self.send_response(self.video.getcode()) + if self.video.info().dict.has_key('connection'): + del self.video.info().dict['connection'] + if self.video.info().dict.has_key('server'): + del self.video.info().dict['server'] + if self.video.info().dict.has_key('transfer-encoding'): + del self.video.info().dict['transfer-encoding'] + if self.video.info().dict.has_key('keep-alive'): + del self.video.info().dict['keep-alive'] + + for key in self.video.info().dict: + self.send_header(key, self.video.info().dict[key]) + # End headers. Next goes video data + self.end_headers() + logger.debug("Headers sent") + + # Run proxyReadWrite + self.proxyReadWrite() + else: + if not fmt: + fmt = self.reqparams.get('fmt')[0] if self.reqparams.has_key('fmt') else None + self.client.handle(shouldStart, self.url, fmt) except (aceclient.AceException, vlcclient.VlcException, urllib2.URLError) as e: logger.error("Exception: " + repr(e)) @@ -402,36 +386,185 @@ def handleRequest(self, headers_only): self.errorhappened = True self.dieWithError() finally: - logger.debug("END REQUEST") - AceStuff.clientcounter.delete(self.path_unquoted, self.clientip) - if not self.errorhappened and not AceStuff.clientcounter.get(self.path_unquoted): + if AceConfig.videodestroydelay and not self.errorhappened and AceStuff.clientcounter.count(cid) == 1: # If no error happened and we are the only client - logger.debug("Sleeping for " + str( - AceConfig.videodestroydelay) + " seconds") - gevent.sleep(AceConfig.videodestroydelay) - if not AceStuff.clientcounter.get(self.path_unquoted): - logger.debug("That was the last client, destroying AceClient") - if AceConfig.vlcuse: + try: + logger.debug("Sleeping for " + str(AceConfig.videodestroydelay) + " seconds") + gevent.sleep(AceConfig.videodestroydelay) + except: + pass + + try: + remaining = AceStuff.clientcounter.delete(cid, self.client) + self.client.destroy() + self.ace = None + self.client = None + if AceConfig.vlcuse and remaining == 0: try: AceStuff.vlcclient.stopBroadcast(self.vlcid) except: pass - self.ace.destroy() - AceStuff.clientcounter.deleteAce(self.path_unquoted) + logger.debug("END REQUEST") + except: + logger.error(traceback.format_exc()) + + def getCid(self, reqtype, url): + cid = '' + + if reqtype == 'torrent': + if url.startswith('http'): + if url.endswith('.acelive') or url.endswith('.acestream'): + try: + req = urllib2.Request(url, headers={'User-Agent' : "Magic Browser"}) + f = base64.b64encode(urllib2.urlopen(req, timeout=5).read()) + req = urllib2.Request('http://api.torrentstream.net/upload/raw', f) + req.add_header('Content-Type', 'application/octet-stream') + cid = json.loads(urllib2.urlopen(req, timeout=3).read())['content_id'] + except: + pass + + if cid == '': + logging.debug("Failed to get CID from WEB API") + try: + with AceStuff.clientcounter.lock: + if not AceStuff.clientcounter.idleace: + AceStuff.clientcounter.idleace = AceStuff.clientcounter.createAce() + cid = AceStuff.clientcounter.idleace.GETCID(reqtype, url) + except: + logging.debug("Failed to get CID from engine") + + return None if not cid or cid == '' else cid class HTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): + + def process_request(self, request, client_address): + checkVlc() + checkAce() + SocketServer.ThreadingMixIn.process_request(self, request, client_address) def handle_error(self, request, client_address): - # Do not print HTTP tracebacks + # logging.debug(traceback.format_exc()) pass +class Client: + + def __init__(self, cid, handler, channelName, channelIcon): + self.cid = cid + self.handler = handler + self.channelName = channelName + self.channelIcon = channelIcon + self.ace = None + self.lock = threading.Condition(threading.Lock()) + self.connectionTime = time.time() + self.queue = deque() + + def handle(self, shouldStart, url, fmt=None): + logger = logging.getLogger("ClientHandler") + self.connectionTime = time.time() + + if shouldStart: + self.ace._streamReaderState = 1 + gevent.spawn(self.ace.startStreamReader, url, self.cid, AceStuff.clientcounter) + gevent.sleep() + + with self.ace._lock: + start = time.time() + while self.handler.connected and self.ace._streamReaderState == 1: + remaining = start + 5.0 - time.time() + if remaining > 0: + self.ace._lock.wait(remaining) + else: + logger.warning("Video stream not opened in 5 seconds - disconnecting") + self.handler.dieWithError() + return + + if self.handler.connected and self.ace._streamReaderState != 2: + logger.warning("No video stream found") + self.handler.dieWithError() + return + + if self.handler.connected: + self.handler.send_response(200) + self.handler.send_header("Content-Type", "video/mpeg") + self.handler.end_headers() + + if AceConfig.transcode: + if not fmt or not AceConfig.transcodecmd.has_key(fmt): + fmt = 'default' + if AceConfig.transcodecmd.has_key(fmt): + stderr = None if AceConfig.loglevel == logging.DEBUG else DEVNULL + transcoder = psutil.Popen(AceConfig.transcodecmd[fmt], bufsize=AceConfig.readchunksize, + stdin=PIPE, stdout=self.handler.wfile, stderr=stderr, shell=True) + out = transcoder.stdin + else: + transcoder = None + out = self.handler.wfile + else: + transcoder = None + out = self.handler.wfile + + try: + while self.handler.connected and self.ace._streamReaderState == 2: + try: + data = self.getChunk(60.0) + + if data and self.handler.connected: + try: + out.write(data) + except: + break + else: + break + except Queue.Empty: + logger.debug("No data received in 60 seconds - disconnecting") + finally: + if transcoder: + transcoder.kill() + + def addChunk(self, chunk, timeout): + start = time.time() + with self.lock: + while(self.handler.connected and (len(self.queue) == AceConfig.readcachesize)): + remaining = start + timeout + time.time() + if remaining > 0: + self.lock.wait(remaining) + else: + raise Queue.Full + if self.handler.connected: + self.queue.append(chunk) + self.lock.notifyAll() + + def getChunk(self, timeout): + start = time.time() + with self.lock: + while(self.handler.connected and (len(self.queue) == 0)): + remaining = start + timeout - time.time() + if remaining > 0: + self.lock.wait(remaining) + else: + raise Queue.Empty + if self.handler.connected: + chunk = self.queue.popleft() + self.lock.notifyAll() + return chunk + else: + return None + + def destroy(self): + with self.lock: + self.handler.closeConnection() + self.lock.notifyAll() + self.queue.clear() + + def __eq__(self, other): + return self is other + class AceStuff(object): ''' Inter-class interaction class ''' - # taken from http://stackoverflow.com/questions/2699907/dropping-root-permissions-in-python def drop_privileges(uid_name, gid_name='nogroup'): @@ -457,8 +590,10 @@ def drop_privileges(uid_name, gid_name='nogroup'): return False logging.basicConfig( - filename=AceConfig.logpath + 'acehttp.log' if AceConfig.loggingtoafile else None, - format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%d.%m.%Y %H:%M:%S', level=AceConfig.debug) + level=AceConfig.loglevel, + filename=AceConfig.logfile, + format=AceConfig.logfmt, + datefmt=AceConfig.logdatefmt) logger = logging.getLogger('INIT') # Loading plugins @@ -506,11 +641,11 @@ def drop_privileges(uid_name, gid_name='nogroup'): # Creating ClientCounter AceStuff.clientcounter = ClientCounter() -if AceConfig.vlcspawn or AceConfig.acespawn: +if AceConfig.vlcspawn or AceConfig.acespawn or AceConfig.transcode: DEVNULL = open(os.devnull, 'wb') # Spawning procedures -def spawnVLC(cmd, delay = 0): +def spawnVLC(cmd, delay=0): try: if AceConfig.osplatform == 'Windows' and AceConfig.vlcuseaceplayer: import _winreg @@ -524,7 +659,9 @@ def spawnVLC(cmd, delay = 0): dir = _winreg.QueryValueEx(key, 'InstallDir') playerdir = os.path.dirname(dir[0] + '\\player\\') cmd[0] = playerdir + '\\' + cmd[0] - AceStuff.vlc = psutil.Popen(cmd, stdout=DEVNULL, stderr=DEVNULL) + stdout = None if AceConfig.loglevel == logging.DEBUG else DEVNULL + stderr = None if AceConfig.loglevel == logging.DEBUG else DEVNULL + AceStuff.vlc = psutil.Popen(cmd, stdout=stdout, stderr=stderr) gevent.sleep(delay) return True except: @@ -540,7 +677,17 @@ def connectVLC(): print repr(e) return False -def spawnAce(cmd, delay = 0): +def checkVlc(): + if AceConfig.vlcuse and AceConfig.vlcspawn and not isRunning(AceStuff.vlc): + del AceStuff.vlc + if spawnVLC(AceStuff.vlcProc, AceConfig.vlcspawntimeout) and connectVLC(): + logger.info("VLC died, respawned it with pid " + str(AceStuff.vlc.pid)) + else: + logger.error("Cannot spawn VLC!") + clean_proc() + sys.exit(1) + +def spawnAce(cmd, delay=0): if AceConfig.osplatform == 'Windows': reg = _winreg.ConnectRegistry(None, _winreg.HKEY_CURRENT_USER) try: @@ -558,6 +705,22 @@ def spawnAce(cmd, delay = 0): except: return False +def checkAce(): + if AceConfig.acespawn and not isRunning(AceStuff.ace): + AceStuff.clientcounter.destroyIdle() + if hasattr(AceStuff, 'ace'): + del AceStuff.ace + if spawnAce(AceStuff.aceProc, 1): + logger.info("Ace Stream died, respawned it with pid " + str(AceStuff.ace.pid)) + if AceConfig.osplatform == 'Windows': + # Wait some time because ace engine refreshes the acestream.port file only after full loading... + gevent.sleep(AceConfig.acestartuptimeout) + detectPort() + else: + logger.error("Cannot spawn Ace Stream!") + clean_proc() + sys.exit(1) + def detectPort(): try: if not isRunning(AceStuff.ace): @@ -611,7 +774,7 @@ def findProcess(name): def clean_proc(): # Trying to close all spawned processes gracefully - if AceConfig.vlcspawn and isRunning(AceStuff.vlc): + if AceConfig.vlcuse and AceConfig.vlcspawn and isRunning(AceStuff.vlc): AceStuff.vlcclient.destroy() gevent.sleep(1) if isRunning(AceStuff.vlc): @@ -627,7 +790,7 @@ def clean_proc(): os.remove(AceStuff.acedir + '\\acestream.port') # This is what we call to stop the server completely -def shutdown(signum = 0, frame = 0): +def shutdown(signum=0, frame=0): logger.info("Stopping server...") # Closing all client connections for connection in server.RequestHandlerClass.requestlist: @@ -691,6 +854,7 @@ def _reloadconfig(signum=None, frame=None): else: name = 'acestreamengine' ace_pid = findProcess(name) +AceStuff.ace = None if not ace_pid: if AceConfig.acespawn: if AceConfig.osplatform == 'Windows': @@ -710,7 +874,7 @@ def _reloadconfig(signum=None, frame=None): # Wait some time because ace engine refreshes the acestream.port file only after full loading... gevent.sleep(AceConfig.acestartuptimeout) detectPort() - + try: logger.info("Using gevent %s" % gevent.__version__) logger.info("Using psutil %s" % psutil.__version__) @@ -718,28 +882,6 @@ def _reloadconfig(signum=None, frame=None): logger.info("Using VLC %s" % AceStuff.vlcclient._vlcver) logger.info("Server started.") while True: - if AceConfig.vlcuse and AceConfig.vlcspawn: - if not isRunning(AceStuff.vlc): - del AceStuff.vlc - if spawnVLC(AceStuff.vlcProc, AceConfig.vlcspawntimeout) and connectVLC(): - logger.info("VLC died, respawned it with pid " + str(AceStuff.vlc.pid)) - else: - logger.error("Cannot spawn VLC!") - clean_proc() - sys.exit(1) - if AceConfig.acespawn and not isRunning(AceStuff.ace): - del AceStuff.ace - if spawnAce(AceStuff.aceProc, 1): - logger.info("Ace Stream died, respawned it with pid " + str(AceStuff.ace.pid)) - if AceConfig.osplatform == 'Windows': - # Wait some time because ace engine refreshes the acestream.port file only after full loading... - gevent.sleep(AceConfig.acestartuptimeout) - detectPort() - else: - logger.error("Cannot spawn Ace Stream!") - clean_proc() - sys.exit(1) - # Return to our server tasks server.handle_request() except (KeyboardInterrupt, SystemExit): shutdown() diff --git a/plugins/allfon_plugin.py b/plugins/allfon_plugin.py old mode 100644 new mode 100755 index 4a62108..5365586 --- a/plugins/allfon_plugin.py +++ b/plugins/allfon_plugin.py @@ -4,7 +4,8 @@ ''' import re import logging -import urllib2 +import urlparse +import requests import time from modules.PluginInterface import AceProxyPlugin from modules.PlaylistGenerator import PlaylistGenerator @@ -16,7 +17,7 @@ class Allfon(AceProxyPlugin): # ttvplaylist handler is obsolete handlers = ('allfon',) - logger = logging.getLogger('plugin_allfon') + logger = logging.getLogger('Plugin_Allfon') playlist = None playlisttime = None @@ -25,19 +26,23 @@ def __init__(self, AceConfig, AceStuff): def downloadPlaylist(self): try: - Allfon.logger.debug('Trying to download playlist') - print(config.url) - req = urllib2.Request(config.url, headers={'User-Agent' : "Magic Browser"}) - Allfon.playlist = urllib2.urlopen( - req, timeout=10).read() + Allfon.logger.debug('Trying to download AllFonTV playlist') + self.headers = {'User-Agent' : "Magic Browser", + 'Accept-Encoding': 'gzip'} + if config.useproxy: + r = requests.get(config.url, headers=self.headers, proxies=config.proxies, timeout=30) + else: + r = requests.get(config.url, headers=self.headers, timeout=10) + Allfon.playlist = r.text.encode('UTF-8') + Allfon.logger.debug('AllFon playlist ' + r.url + ' downloaded !') Allfon.playlisttime = int(time.time()) except: - Allfon.logger.error("Can't download playlist!") + Allfon.logger.error("Can't download AllFonTV playlist!") return False return True - def handle(self, connection): + def handle(self, connection, headers_only=False): # 30 minutes cache if not Allfon.playlist or (int(time.time()) - Allfon.playlisttime > 30 * 60): if not self.downloadPlaylist(): @@ -50,21 +55,24 @@ def handle(self, connection): connection.send_header('Content-Type', 'application/x-mpegurl') connection.end_headers() - # Match playlist with regexp + if headers_only: + return; - matches = re.finditer(r'\#EXTINF\:0\,ALLFON\.TV (?P\S.+)\n.+\n.+\n(?P^acestream.+$)', + matches = re.finditer(r'\#EXTINF\:0\,(?P\S.+)\n.+\n.+\n(?P^acestream.+$)', Allfon.playlist, re.MULTILINE) - add_ts = False try: if connection.splittedpath[2].lower() == 'ts': add_ts = True except: pass - - playlistgen = PlaylistGenerator() + playlistgen = PlaylistGenerator(m3uchanneltemplate=config.m3uchanneltemplate) for match in matches: playlistgen.addItem(match.groupdict()) - connection.wfile.write(playlistgen.exportm3u(hostport, add_ts=add_ts)) + url = urlparse.urlparse(connection.path) + params = urlparse.parse_qs(url.query) + fmt = params['fmt'][0] if params.has_key('fmt') else None + header = '#EXTM3U url-tvg="%s" tvg-shift=%d deinterlace=1 m3uautoload=1 cache=1000\n' %(config.tvgurl, config.tvgshift) + connection.wfile.write(playlistgen.exportm3u(hostport, header=header, add_ts=add_ts, fmt=fmt)) diff --git a/plugins/config/allfon.py b/plugins/config/allfon.py index 75d336b..ade6c8b 100644 --- a/plugins/config/allfon.py +++ b/plugins/config/allfon.py @@ -1,6 +1,20 @@ ''' Allfon.tv Playlist Downloader Plugin configuration file ''' +# Proxy settings. +# For example you can install tor browser and add in torrc SOCKSPort 9050 +# proxies = {'http' : 'socks5://127.0.0.1:9050','https' : 'socks5://127.0.0.1:9050'} +# If your http-proxy need authentification - proxies = { 'https' : 'https://user:password@ip:port' } +useproxy = False +proxies = {'http' : 'socks5://127.0.0.1:9050', + 'https' : 'socks5://127.0.0.1:9050'} # Insert your allfon.tv playlist URL here -url = 'http://allfon.tv/autogenplaylist/allfontv.m3u' +url = 'http://allfon-tv.pro/autogenplaylist/allfontv.m3u' + +# EPG urls & EPG timeshift +tvgurl = 'http://www.teleguide.info/download/new3/jtv.zip' +tvgshift = 0 + +# Channel template +m3uchanneltemplate = '#EXTINF:-1 tvg-name="%(tvg)s",%(name)s\n%(url)s\n' diff --git a/plugins/config/p2pproxy.py b/plugins/config/p2pproxy.py index b12e97b..d2f2163 100644 --- a/plugins/config/p2pproxy.py +++ b/plugins/config/p2pproxy.py @@ -22,6 +22,9 @@ # Insert your torrent-tv account password password = 'ReplaceMe' +# Session timeout +sessiontimeout = 1800 + # Generate logo with full path (e.g. http://torrent-tv.ru/uploads/ornzQpk6WCW6xk0lyBhlwqH8u2QyU7.png) # or put only the logo file name (e.g. ornzQpk6WCW6xk0lyBhlwqH8u2QyU7.png) # This option is only for m3u playlists. @@ -34,4 +37,8 @@ tvgshift = 0 # Format of the tvg-id tag or empty string -tvgid='ttv%(id)s' \ No newline at end of file +tvgid='ttv%(id)s' + +# Zone id - AUTO (1), MSK (2), SPB (3), SAM (4), AMS (5), ISR (6) etc. +# For more details see http://api.torrent-tv.ru/v3/api_v3.html#toc-35- +zoneid = '1' diff --git a/plugins/config/playlist.py b/plugins/config/playlist.py new file mode 100755 index 0000000..c188b95 --- /dev/null +++ b/plugins/config/playlist.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +class PlaylistConfig(): + + # Default playlist format + m3uemptyheader = '#EXTM3U\n' + m3uheader = '#EXTM3U url-tvg="http://1ttvapi.top/ttv.xmltv.xml.gz"\n' + # If you need the #EXTGRP field put this #EXTGRP:%(group)s\n after %(name)s\n. + m3uchanneltemplate = \ + '#EXTINF:-1 group-title="%(group)s" tvg-name="%(tvg)s" tvg-id="%(tvgid)s" tvg-logo="%(logo)s",%(name)s\n%(url)s\n' + + # Channel names mapping. You may use this to rename channels. + # Examples: + # m3uchannelnames['Canal+ HD (France)'] = 'Canal+ HD' + # m3uchannelnames['Sky Sport 1 HD (Italy)'] = 'Sky Sport 1 HD' + m3uchannelnames = dict() + + # Similar to m3uchannelnames but for groups. + m3ugroupnames = dict() + + # Channel name to tvg name mappings. + m3utvgnames = dict() + # m3utvgnames['Channel name'] = 'Tvg_name' + + # Playlist sorting options. + sort = False + sortByName = False + sortByGroupName = False + + # This method can be used to change a channel info such as name, group etc. + # The following fields can be changed: + # + # name - channel name + # url - channel URL + # tvg - channel tvg name + # tvgid - channel tvg id + # group - channel group + # logo - channel logo + @staticmethod + def changeItem(item): + PlaylistConfig._changeItemByDict(item, 'name', PlaylistConfig.m3uchannelnames) + PlaylistConfig._changeItemByDict(item, 'group', PlaylistConfig.m3ugroupnames) + PlaylistConfig._changeItemByDict(item, 'name', PlaylistConfig.m3utvgnames, 'tvg') + + @staticmethod + def _changeItemByDict(item, key, replacementsDict, setKey=None): + if len(replacementsDict) > 0: + value = item[key] + if not setKey: + setKey = key + + if isinstance(value, str): + value = replacementsDict.get(value) + if value: + item[setKey] = value + elif isinstance(value, unicode): + value = replacementsDict.get(value.encode('utf8')) + if value: + item[setKey] = value.decode('utf8') + + # This comparator is used for the playlist sorting. + @staticmethod + def compareItems(i1, i2): + result = -1 + if PlaylistConfig.sortByGroupName: + result = cmp(i1.get('group', ''), i2.get('group', '')) + if result != 0: + return result + if PlaylistConfig.sortByName: + result = cmp(i1.get('name', ''), i2.get('name', '')) + return result diff --git a/plugins/config/torrentfilms.py b/plugins/config/torrentfilms.py new file mode 100644 index 0000000..a13b04d --- /dev/null +++ b/plugins/config/torrentfilms.py @@ -0,0 +1,9 @@ +''' +Torrent Films Playlist Plugin configuration file +(C) Pepsik +''' + +# Insert your path to *.torrent files here +# In *nix based systems use '/path1/path2/path3' in windows 'C:\\path1\\path2\\path3' +directory = '' + diff --git a/plugins/config/torrenttelik.py b/plugins/config/torrenttelik.py index 3ce57df..81cd953 100644 --- a/plugins/config/torrenttelik.py +++ b/plugins/config/torrenttelik.py @@ -2,8 +2,18 @@ ''' Torrent-telik.com Playlist Downloader Plugin configuration file ''' - +# Proxy settings. +# For example you can install tor browser and add in torrc SOCKSPort 9050 +# if you use tor on the same machine with AceProxy - proxies = { 'https' : 'socks5://127.0.0.1:9050' } +# If your http-proxy need authentification - proxies = {https' : 'https://user:password@ip:port'} +useproxy = False +proxies = {'http' : 'socks5://127.0.0.1:9050', + 'https' : 'socks5://127.0.0.1:9050'} # Channels urls url_ttv = 'http://torrent-telik.com/channels/torrent-tv.json' url_mob_ttv = 'http://torrent-telik.com/channels/mob-torrent-tv.json' -url_allfon = 'http://torrent-telik.com/channels/allfon.json' \ No newline at end of file +url_allfon = 'http://torrent-telik.com/channels/allfon.json' + +# EPG urls & EPG timeshift +tvgurl = 'http://simple-tv.torrent-telik.com/teleprograma' +tvgshift = 0 diff --git a/plugins/config/torrenttv.py b/plugins/config/torrenttv.py index d44d032..01dbe8b 100644 --- a/plugins/config/torrenttv.py +++ b/plugins/config/torrenttv.py @@ -1,14 +1,21 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- ''' Torrent-tv.ru Playlist Downloader Plugin configuration file ''' +# Proxy settings. +# For example you can install tor browser and add in torrc SOCKSPort 9050 +# proxies = {'http' : 'socks5://127.0.0.1:9050','https' : 'socks5://127.0.0.1:9050'} +# If your http-proxy need authentification - proxies = {'https' : 'https://user:password@ip:port'} +useproxy = False +proxies = {'http' : 'socks5://127.0.0.1:9050', + 'https' : 'socks5://127.0.0.1:9050'} # Insert your Torrent-tv.ru playlist URL here -url = '' +url='' # TV Guide URL -tvgurl = 'http://api.torrent-tv.ru/ttv.xmltv.xml.gz' +tvgurl = 'http://1ttvapi.top/ttv.xmltv.xml.gz' # Shift the TV Guide time to the specified number of hours tvgshift = 0 @@ -20,543 +27,780 @@ updateevery = 0 # Channel logos mapping -logobase = 'http://torrent-tv.ru/uploads/' +logobase = 'http://1ttv.org/uploads/' logomap = { + u'0x0 Fireplace HD': logobase + 'H1VboxDJC7sE7x3nKXoYT0X5r4LIqD.png', + u'0x0 Music HD': logobase + 'hFj4tnC5uqAgpod3doHnJGZxgZXaiP.png', u'1 HD': logobase + 'FtLnmUwjG18XJFEKYvLKjwUq1gwHVZ.png', - u'1 Балтийский Музыкальный': logobase + 'wLtopqioazWFEqSAGxC1D8ybC0KvHq.png', + u'1 MUSIC CHANNEL': logobase + '45atLSHAjViMISJinK8A0jQA34I0Fi.png', + u'1 tv Georgia': logobase + 'geM35mbIzNqzz3LBvxnViw2EviT2BW.png', u'1+1': logobase + 'omm2Xc8xSVIT6Od6ca4QqMrEXw3jaK.png', + u'1+1 International': logobase + '6fImIbJqJsF3pJQi2UuYRwtRyHMeaB.png', u'100% News': logobase + '9yEWvPmTcFS8lyQ5NjJ7vbYOa3bx1W.png', - u'112 Украина HD': logobase + '0AUjU7DOzZcpizC4UR19vDZqqthSe0.png', u'112 Украина': logobase + '0AUjU7DOzZcpizC4UR19vDZqqthSe0.png', + u'112 Украина HD': logobase + '0AUjU7DOzZcpizC4UR19vDZqqthSe0.png', + u'12 Канал (Омск)': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'2 канал (Израиль)': logobase + 'CSw0eG13VxmrfPXWwUOHyerOhd53UC.png', u'2+2': logobase + 'XHXBC3ghvhh100BNXylSgJLx5FVQgD.png', - u'24 Док': logobase + 'H1UXBai10DjYfScfv1sNAILV9EPDer.png', u'24 Украина': logobase + 'XfKEdfsy4S1zbE8n2tB1tNNe9IkrRP.png', - u'2x2 (+2)': logobase + 'hTpJUV15GSTxZ5kJQGLcn42kCzKyEH.png', + u'24 Украина HD !': logobase + 'fndLsRJ2T4AFEYqVxMkvz8teE2aTuw.png', + u'26 Регион HD (Ставрополь)': logobase + 'WdXmMmnehWo0vdzVwA4z1tGgRjFTWt.png', u'2x2': logobase + 'hTpJUV15GSTxZ5kJQGLcn42kCzKyEH.png', - u'360 градусов HD': logobase + 'K4tiqb5ajIgmxqh8X5moJuDjN7q5hx.png', + u'2x2 (+2)': logobase + 'ZPOhZle6vrDaulo2KMmyrCkkkLn7Ci.png', u'360 градусов': logobase + 'K4tiqb5ajIgmxqh8X5moJuDjN7q5hx.png', + u'360 градусов HD': logobase + 'K4tiqb5ajIgmxqh8X5moJuDjN7q5hx.png', + u'360 Новости HD': logobase + 'K7RGdQQaKY78l29J4Xfgk4F0t9a2jq.png', u'365 Дней': logobase + '0IZrVwoxtmjtgnWu5Dj4Hb8FRc8NIX.png', - u'43 Канал HD': logobase + '1mNoU1QXmcmK48eVTlnADtsEBmA2sN.png', - u'49 канал': logobase + 'liRZcIcNbzQTngm8CIkcnw7gLJfeIJ.png', + u'41 канал Домашний (+2)': logobase + '6vBXDKono9On9ND4WG49IeaXzdjWcX.png', + u'43 Канал HD (Туапсе)': logobase + '1mNoU1QXmcmK48eVTlnADtsEBmA2sN.png', + u'47 канал': logobase + '8aHwrc5KVFI4FXwgPbBf5vjBSIQ52B.png', + u'49 канал (Новосибирск)': logobase + 'liRZcIcNbzQTngm8CIkcnw7gLJfeIJ.png', + u'4Fun TV': logobase + 'MBRDVkacgvAn0zw4ceuI1ji9LxJFsb.png', + u'5 канал (Киргизия)': logobase + 'ftgWX6P9yWch8a2fF7qH7QdmhtWPu8.png', u'5 канал (Украина)': logobase + '9La0uS6S8rMKr0BOh6vSCQLNiCqN7N.png', + u'66 канал (Израиль)': logobase + 'ZNDGDdjbJMIB3owCIKosKGsUFe2En3.png', + u'78 канал': logobase + 'ZloJoTx0yurBDfEtB6V91vRuF8mxB4.png', + u'78 канал HD': logobase + 'ZloJoTx0yurBDfEtB6V91vRuF8mxB4.png', + u'7ТВ': logobase + 'vNvqlbSnA9yjRCEdcGV773QxysaOxm.png', u'8 канал': logobase + 'wKPUJjcpZIfI5zBSZwNayOlv63zRNV.png', + u'8 канал (Красноярск)': logobase + 'wKPUJjcpZIfI5zBSZwNayOlv63zRNV.png', u'9 волна': logobase + '8zDeTSBhsmJDbXo9dxHA1c9mDOP9sP.png', - u'A-One UA': logobase + '8rEKNCXhKeWnUvGhWCsrb2RN5FMqJi.png', - u'A-One': logobase + 'buHzdmouswQyLnhzwqnPUHdS9BMHTw.png', + u'9 канал (Израиль)': logobase + 'dsi2SD7Pmq3sxxaNPpRAUgKmxXh4s6.png', + u'9 канал (Рязань)': logobase + 'bO1hIXjoA3KytchKgshG1Bhzcz2DNT.png', + u'Abu Dhabi TV HD': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Al Jazeera English': logobase + '5ZfDU3kFx7mh5ntH0QZfagkjIWbZ4C.png', + u'Al Rayyan 2 HD': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Almaty HD': logobase + 'DKYJ9oBuRE2JmxrZNo4i4vM9s8dsUb.png', u'AMC': logobase + 'rozrC8ZPYA1YGajyow35doeVcaQzpa.png', - u'ATR': logobase + 'uggqxQxxDTuz1P1mY4PZdlRcAKow3F.png', - u'Al Jazeera English': logobase + 'B1AC3jl0CY8u4qxO0aIEHVMFQFvBUu.png', - u'Amazing Life': logobase + 'w8TNEcKxT64e1cJuN1b1DXvV1IumE0.png', u'Amedia 1': logobase + 'jT3vAEOG5jTd2t8GcC797Bw5W0kSl9.png', u'Amedia 2': logobase + 'fAvxTQbWu0DAcMkqej0m73KohAcQJw.png', - u'Amedia Premium HD (SD)': logobase + 'ornzQpk6WCW6xk0lyBhlwqH8u2QyU7.png', + u'Amedia Hit': logobase + 'HdnTfcZCgP7Odm1cOKNq9j4yJDRiFP.png', + u'Amedia Hit HD': logobase + 'HdnTfcZCgP7Odm1cOKNq9j4yJDRiFP.png', + u'Amedia Premium': logobase + 'ornzQpk6WCW6xk0lyBhlwqH8u2QyU7.png', u'Amedia Premium HD': logobase + 'ornzQpk6WCW6xk0lyBhlwqH8u2QyU7.png', u'Ani': logobase + 'vui1cRrE05CZv1N9Qb20jJ6mTFOJue.png', + u'Animal Family HD': logobase + '6Xrlt9W9PBeF99h1ayRh8zteAheFUQ.png', u'Animal Planet': logobase + '45.png', - u'Animаl Planеt HD': logobase + '9HZGan5rQItVQOfnB91FGqyJXjoqYV.png', - u'Armenia 1': logobase + 'YST19D8Vt4g7ck1MquvppzKy8Oa27D.png', + u'Animal Planet HD': logobase + '9HZGan5rQItVQOfnB91FGqyJXjoqYV.png', + u'Anyday 3D': logobase + 'Q9gPrtyM73PzW5Jlf0pXnjCJcT0Tq7.png', + u'Anyday HD': logobase + 'nZ0Aa5bcpThFFTGvBUfW65GTgVoH6c.png', + u'Ararat TV': logobase + 'HdJCAim5FV0bpwYQ3Fam5yxoMzFxAe.png', + u'Arirang TV': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', u'Armenia TV Satellite': logobase + 'M4UAMT6cEyL3zuim9fzy38vZRkIfKq.png', + u'Art-108 HD': logobase + 'RXU5Txc5ki40dg6pJUleIMIabCcltD.png', + u'Astana TV': logobase + 'R09FzjK0PWCE06kTL9obz4RpJ9SUl5.png', + u'ATR': logobase + 'uggqxQxxDTuz1P1mY4PZdlRcAKow3F.png', + u'ATV': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'ATV Azerbaycan': logobase + 'yx7GzFSH3BlxYYa0Nuq4eiVRqxZSa5.png', + u'Aurora HD': logobase + 'HYtru2e2Sh6VrZsnNxdTyNfO8m9eoM.png', u'Az TV': logobase + 'FB2qxgNm8xTU6YiR2fY1jH4PIRR2SF.png', - u'B4U Music UK': logobase + 'LlfDsq3FEU2D4P3lmj25fvl6zQnvNM.png', - u'BBC One': logobase + 'ofuDCyM7MgzwFbzG2pyavFu6xeRHwp.png', - u'BBC Two': logobase + 'o7LyIuTJT2C7wJG77Zx5cZhBYP2L8B.png', + u'Babestation': logobase + 'DbvHMminWOobAbD3IliRECsG8RH6uL.png', + u'Baby TV !': logobase + 'aE2CDNS2w6p5LA4OThfOWOzYT7jQno.png', u'BBC World News': logobase + '8cSYWwvq6BAzcGDjJODvCGQYGJ2c4S.png', - u'BT Sport 1': logobase + 'RNwudSF1Lys88WmIXhiS43g2urfknl.png', - u'BT Sport 2': logobase + 'NdEFtAJBS9EtR2rSEUZ1ZF15tQzH6y.png', - u'BT Sport ESPN': logobase + 'NA9uK6s2dqxbRAuhguG80gO03mGUOl.png', - u'BT Sport Europe': logobase + 'e3dk71TnjMRLy1A1FjvWnuwLDP3rM0.png', - u'BTV 1 HD': logobase + 's9FERVp6FKtSyMB0N4g17aaKLdxGl5.png', - u'Bein Sports 1 HD France': logobase + 'iFvQJFlFSlOnL7VN1KZ2qo5DD6lNgU.png', - u'Bein Sports 2 HD France': logobase + 'eYy8eQWvjK2lUBRCPXldM7DgOanhVZ.png', u'Bloomberg': logobase + 'OTsoNZRT8xjXz5nnTICPCQRvNLjBal.png', + u'Bollywood HD': logobase + 'HXzWxtMxmXkrpgr88Kh8Z1A8F6o5dK.png', + u'BOLT HD': logobase + 'EEZrUgR0IgyOp97DI8sGPT78J40Doq.png', u'Boomerang': logobase + 'tsP2U3zkp5o8B0TD6luRLg0leS9FvM.png', - u'Boxnation': logobase + '8mECbEuuVmoMl17GPq6hV8X7LDMlu5.png', + u'Boutique TV': logobase + 'bDSWVUfAEYN9VS25roxW52xO1pj6qp.png', + u'Brazzers TV': logobase + 'VPCmFsMfpnB0pg9VBzvUG5TkRcHTsR.png', u'Brazzers TV Europe': logobase + 'OmVBp3Kz4Lx722dq2e1OxE26QSxrDA.png', + u'Bridge HD': logobase + 'P7JEo7cGv1dtk6X69SkfEQeiVrQvXj.png', u'Bridge TV': logobase + 'dPhBiaViIznwjeWU3pTbIXFmS3iJkU.png', + u'Bridge TV Classic': logobase + 'NCuKRKNU2rwc8DkpF555fINSz5xaIc.png', + u'Bridge TV Dance': logobase + 'ofmgiZlRmwOZtTaYtUtfErZS3ODDDM.png', u'Brodilo TV HD': logobase + '0pLPWiGqZjDe2O4vbPd8QL0qGsA9lq.png', + u'BT Sport 1': logobase + 'RNwudSF1Lys88WmIXhiS43g2urfknl.png', u'Business': logobase + 'wvwXc2x8Bjev90GD6LO6t7TL2aKW1h.png', - u'C More Tennis': logobase + 'ql4qy7gHN4GtWhcqfd97HqXUs8HXI9.png', - u'CBS Drama': logobase + '1Mcq9jbl7qPeXpT0bbPTnf8aM3f7dq.png', - u'CCTV 4': logobase + 'PsrMNDhKrEgCJA5lAR9UUtBlOI4uJg.png', - u'CCTV News': logobase + 'TyWVFvKwV3lOYYXWVJSj8HMu5AodJr.png', - u'CNL': logobase + 'wwQ6u4lFXyaUDJLu2JOh6mtbkIz0Nf.png', - u'CNN International': logobase + 'lIwbRbM3ve4ixhFXp75Oap9nzcO71G.png', - u'CT 1': logobase + 'sgPGofRHRzOlNb6dBsk5QGiCdzJewS.png', - u'CT 2': logobase + 'dFDWTyqFwS3roF7Jb3JGZIfm1htuUv.png', - u'CT Decko': logobase + 'iZ4GvLdo9Gt4NiVLCuiRYtF8rtgYmi.png', - u'Canal+ Deportes 2': logobase + '4EUoskZorTXBkMxeR8ykSpyQOErvgu.png', - u'Canal+ Deportes': logobase + 'qoi5H4OujPPtCC1JJO1ozeyFLPqPk6.png', - u'Canal+ Futbol': logobase + '1En2KRyo8ywbMPWjiiri7TcEXJbqcE.png', - u'Canal+ Liga': logobase + '6GyapitmDBshZmCxXPJsQWJGhkcooJ.png', - u'Candy': logobase + 'YW8BJnImniuR7l1U85Khw31mX2XO0S.png', - u'Candyman': logobase + '8pCxUJ8TBWfvWCrIPN4a4Jimsv9onx.png', + u'C Music TV HD': logobase + 'YhtkJhsV8NKwm4FGWhQZDmYcW0TYQS.png', + u'Canal 3': logobase + 'rb39jMz38xLuXKGCuWD2GcHHMEzfX1.png', + u'Candy HD': logobase + 'YW8BJnImniuR7l1U85Khw31mX2XO0S.png', + u'Candyman HD': logobase + '6fgWPM2UHUiHic7UVLG8YCVq5bC7uw.png', u'Cartoon Network': logobase + 'NTNQLLri3Hh9iqYjW7VEkFYJsTLjk9.png', + u'CBC Sport HD': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'CBC TV': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'CBS Reality': logobase + 'QbWaD2vNgLRLY0Hsccg3nf9azAXRld.png', + u'CCTV 4': logobase + 'PsrMNDhKrEgCJA5lAR9UUtBlOI4uJg.png', u'CentoXCento TV': logobase + 'dPuPlqJtkLMRy7NmOhyHDfL0PJqYj4.png', - u'Cinemax 1': logobase + 'iVIDTNBhrsd8S7OUexIOE9lJJ0BGZU.png', - u'Cinemax 2': logobase + 'XLVuXXOFFSfsOVKEsFxarjmvLeA5WK.png', + u'CGTN': logobase + 'TyWVFvKwV3lOYYXWVJSj8HMu5AodJr.png', + u'CGTN Documentary': logobase + 'dqoArqO8dd09vzsSSQPqLUj5wM9O9M.png', + u'CGTN Русский': logobase + 'DcaE92lKRSDFBPlYUnsNRauFLbotKu.png', + u'Cinema': logobase + 'beyfqyeacrFG0PrOeKUQhzQ4bV6Q5d.png', + u'CNN International': logobase + 'lIwbRbM3ve4ixhFXp75Oap9nzcO71G.png', u'Da Vinci Learning': logobase + 'Yl6p1IDDkZxxiUa3p2JxI66mIlOPns.png', - u'Dange TV': logobase + 'ofmgiZlRmwOZtTaYtUtfErZS3ODDDM.png', - u'Deluxe Music': logobase + '3VnQoAyJh3RZM88USPszL1TZIE5t0u.png', u'Deutsche Welle': logobase + 'RnqBDfde1HP4OkZhXWlKB1xkHi6Io9.png', - u'Diema Sport BG': logobase + 'AA2hWbb0gyhc7nYtjWXJH9ZrB3D5EM.png', - u'Digi Sport 1': logobase + 'gk0ajzoQjMHlBKM298kWjcfNy1P8ec.png', - u'Digi Sport 2': logobase + 'x1HMd7tDoprxuZvq90Uj2Gb1eEOhTm.png', - u'Discovepy Science': logobase + 'GAaO3EfDwMuHAIelG4gYW6TDEYbLnS.png', - u'Discovery Channel HD': logobase + 'SmWnYlOvkJn8GzttT2UY0vmo8PYfMg.png', + u'Diema Sport 2': logobase + '4xUpzBxK0sL6Hc9KQADA1LhmFbE9Yi.png', u'Discovery Channel': logobase + 'oKx1ImWVRT3AK3DHYWUVc71JZUkwu5.png', + u'Discovery Channel HD': logobase + 'SmWnYlOvkJn8GzttT2UY0vmo8PYfMg.png', + u'Discovery Science': logobase + 'GAaO3EfDwMuHAIelG4gYW6TDEYbLnS.png', + u'Discovery Science HD': logobase + '5rlWsKfvt9jYnUht7vXqEqA47Y05wh.png', + u'Discovery World !': logobase + 'dMUIABPd7wmzLHryJ0IjVKRvWSZXkS.png', u'Disney Channel': logobase + 'JxEjTeXwExjnxutQGKJBmMI85tpNqK.png', - u'Dobro TV': logobase + 'tcHbtjkY9gYD4VqipgwLP2BYxgdrZb.png', - u'Dorcel': logobase + '9bzeA3zNtESvDVsiMyDlcrTAngs2af.png', - u'Dro TV HD': logobase + 'iV5n6aGpDLVpReaJz71u0XrIyAHTcG.png', - u'Dunya TV AZ': logobase + 'C9oEtk1vVd0yApuKTUve53ADQUhNFj.png', + u'DocuBox HD': logobase + '4ZjuKuTHFCuXqKkEmKYGcBFe6UK5z4.png', + u'Dog TV': logobase + 'NblfllG2mrqX3Zrb36rv7hLUe2sG1R.png', + u'DREAM PORN HD': logobase + 'rEdYcZis6CYk8QAFJzJRQQE1NPoZGj.png', + u'DTX': logobase + 'bx7MosYUikfuwwIOzx2elT6Dh7h50I.png', + u'DTX HD': logobase + 'bx7MosYUikfuwwIOzx2elT6Dh7h50I.png', + u'Dusk TV': logobase + '5j0MFmxPLBN36rJR2yYxgnOOHa23gh.png', u'English Club TV': logobase + 'Hf5RUQ91cEGtHoaK3GK6VIZlJ8Leql.png', u'Enter Film': logobase + '8FPA5SCEIj35fBO8yrULnefW4NzIzk.png', - u'EroXXX HD': logobase + 'VxkJjYZQqK3OcevhRKsz2L3OFiaEwt.png', + u'Epoque': logobase + 'eULbHv3DyublWF2rAhw38DV63cV3V9.png', + u'EPOQUЕ HD': logobase + 'EKfkYWRSkYiN76MxCZCjawxewjBOYY.png', u'Eska Best Music TV HD': logobase + 'wI3e672FQZpD8yr8aIV8Q2fL15zENv.png', - u'Eska Music Vox TV OldsCool HD': logobase + '7LW3s6C3D3yeciqTY0CKXpEmL3Tb3w.png', + u'Eska OldsCool TV HD': logobase + '7LW3s6C3D3yeciqTY0CKXpEmL3Tb3w.png', u'Eska Party TV HD': logobase + 'MaX9is65hlDpRDwgOikGE2RqMRxn8G.png', u'Eska Rock TV HD': logobase + 'VU2POxFhYCM4XhFAtOp8Y4sdlbu32M.png', - u'Eska TV': logobase + '1KX19DgurgoaSKNTTpjXCeSQr1epwv.png', u'Eska Wawa TV HD': logobase + 'r9d697qox4MFgypnVqeILYWKcfbwNc.png', + u'ETV': logobase + 'EXXEf0i7yGRDFttKSwBq4CLoGiuxbW.png', + u'ETV2': logobase + 'dR4fj3bOJ4PAqBEKskc9yLNj0unwAD.png', u'EuroNews': logobase + 'Vb3fP5gUK0q40WuzYeUhMT7RQmDg27.png', u'Europa Plus TV': logobase + 'PkatgpdmA4ArsgSG5shu0ZCQQ5RgMx.png', - u'Eurosport 2 British': logobase + 'syqFtqcYUfxX6t2wcfSGKNbKQ7vGgN.png', - u'Eurosport British': logobase + 'sKWbooLWxKfwjge6dMOTWi35gQvcG8.png', - u'Eurosport HD': logobase + 'DpFTzUEA3y67Z6ObTPF4xH0XLNRAZm.png', - u'Eurosport': logobase + 'QA9jgUaQRrE4vMno04eM3aUrklXOce.png', - u'Eurospоrt 2 North-East HD': logobase + '0tv5hm546AKIySs7cpj30LlCOcY0Mj.png', - u'Eurospоrt 2 North-East': logobase + 'qYbdkVFDkhGqTAjXlRtIj2Fg45bmrm.png', - u'Eurospоrt 2': logobase + 'qYbdkVFDkhGqTAjXlRtIj2Fg45bmrm.png', - u'Exotica TV': logobase + 'K5hm3mURkkDaSc7RI5RDti5edynMGl.png', + u'Eurosport 1': logobase + 'QA9jgUaQRrE4vMno04eM3aUrklXOce.png', + u'Eurosport 1 HD': logobase + 'DpFTzUEA3y67Z6ObTPF4xH0XLNRAZm.png', + u'Eurosport 2': logobase + 'qYbdkVFDkhGqTAjXlRtIj2Fg45bmrm.png', + u'Eurosport 2 HD': logobase + '0mo5WOW3xn7FmcIRM9PMZxb2Zgad86.png', + u'EvilAngel': logobase + 'FufZ2heFswzvAbRkTQZs8UJBYGsxuG.png', + u'EWTN UK and Ireland': logobase + 'maP6bdwOGv77xHPvRnKBHww9cmG2oV.png', u'Extreme Sports': logobase + '21FhIqWK82JDPNuLTEIC9hSO2EHfks.png', - u'F+ BG': logobase + 'DJZlMhUwCNcVypOBoytE7QQnKtMepM.png', + u'Extreme Sports (резерв)': logobase + 'F4fGoW5QeUXRJnNnfOnhY3ZejkVyPn.png', + u'FAP TV 2': logobase + 'YVmUBY8cBPO8IoL4djlyeKCDbs6f0p.png', + u'FAP TV 2 HD': logobase + 'KqVXT8GPTPuPEM87hZSqDwajqcBBgY.png', + u'FAP TV 3': logobase + 'S5gXPPcF53lFHQ3kgJofKMQudRQKPt.png', + u'FAP TV 4': logobase + 'xv1QyBV2OkaDbZ1ebRB2FWwfw40BR5.png', + u'FAP TV Amateur': logobase + 'LGfemDTp3Z3mvhn1aKSGVvtlNu5zMF.png', + u'FAP TV Anal': logobase + '1PVgf85c4DjIsiWEikpEHDV1j1m6ph.png', + u'FAP TV BBW': logobase + 'PHnwbskpP4oixpMjlJKgGvTAajXFSg.png', + u'FAP TV Compilation': logobase + 'FXT4PI3HHWIGlk49jRpHyGQBTTVbmt.png', + u'FAP TV Gay': logobase + 'vJB3tfzXuDX8R7eLXV63tCFgxnOgP1.png', + u'FAP TV Legal Porno': logobase + 'QMv8xcYgTaW9108ymib6XyWXRMI7dn.png', + u'FAP TV Lesbian': logobase + 'M3LLmt76GGyLBhpAEnRtn368pAeGH3.png', + u'FAP TV Older': logobase + 'G1L5Qh2QVnSu6zEVua6KOI4hxtUfcn.png', + u'FAP TV Parody': logobase + 'mu4NIjgq7Xu5nt32zTSxGvBOfk90xt.png', + u'FAP TV Pissing': logobase + '3QJ9sSAI90ALizvl1B2sbt8WxIVLSu.png', + u'FAP TV Shemale': logobase + 'd5ulbooZicKw3aMLwn2Ho0yD9c46T2.png', + u'FAP TV Teaching': logobase + '3MzvfwDLSX8zUmmuO7TpS8O2iD03Tj.png', + u'FAP TV Teens': logobase + '7Z9gyzosm1oUC82aFfA3mBG9YAjOrS.png', + u'FAP TV Trans': logobase + '05P2Z8lWWIgaEmvWcTRO1R9F0lMPZL.png', u'Fashion One HD': logobase + 'iPs2ptiBXm8h0KSnRmyqu45texHNig.png', u'Fashion TV': logobase + 'PSjqabjhYIBqcS8hUA8WNrZEjV4zZY.png', + u'Fashion TV HD': logobase + 'z6q34MCsDCntK8jbJXp4pBQiPlIiNx.png', + u'FILMUADRAMA': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'FilmUADrama HD': logobase + 'fWGkWZarY98h18LXSQwHmSAZNXhZW4.png', u'Fine Living': logobase + '22I1fK1aMCUgeYUCV0K3vwmlJKELZD.png', + u'Fitcult TV': logobase + 'XIwNf2TL3xUPnLD0L7Y5Q1FUwwpcSp.png', u'Food Network': logobase + 'W68LEGlOOMPtN0qoGXvdkWhHBjKet6.png', - u'Fox Life': logobase + 'rkksGUl3DstQSEyT26Q07hNCEwyNnd.png', - u'Fox Sports 2': logobase + '6huWEAg1M7y9bKk21UkIyJ3yw1RRHO.png', u'Fox': logobase + 'XGC77wQNeEyaJ2z2mDipyIPsoF0xc1.png', + u'Fox HD': logobase + 'Pl8S60EJ52htHxi1gAw1SS1y8i1p3z.png', + u'Fox Life': logobase + 'rkksGUl3DstQSEyT26Q07hNCEwyNnd.png', + u'Fox Life HD': logobase + 'Vou521VpOGAGqhp4HUfiG7BSbKNSk6.png', u'France 24': logobase + 'pViXgcMjLnB5WyOoQJ7sBxhHo5fTSB.png', - u'Fоx HD': logobase + 'Pl8S60EJ52htHxi1gAw1SS1y8i1p3z.png', - u'Fоx Life HD': logobase + 'Vou521VpOGAGqhp4HUfiG7BSbKNSk6.png', u'Galaxy TV': logobase + 'pU2NsRP9CVtEgQuDTi9jcTYV8iAD4a.png', + u'Gamanoid HD': logobase + 'DxEQkipHmJjSY0wHGKHDV6eAZKiwoG.png', + u'Game Show': logobase + 'Uc9sBHj0DAYzfXMZmdxeriCBZvUpeb.png', + u'GAME SHOW HD': logobase + 'x8rAmyXyw8Alq0OKwV9d8iB56lVxRn.png', + u'Ginger HD': logobase + 'eW7CqWW2bppbpKzhNctyElyv5Nzs30.png', + u'Glazella 3D': logobase + '7EM3eHww2EvOf9jgeFPdZrsQwO8Fz7.png', + u'Glazella HD': logobase + '7EM3eHww2EvOf9jgeFPdZrsQwO8Fz7.png', u'GlobalStar TV': logobase + 'Y4kNxgbAnPGCt753P73NzCIbcNz5lD.png', - u'Gulli': logobase + '2IynBdmw3mmdXt01r8DXKxeor7STnw.png', - u'HD Life (SD)': logobase + 'jUteUS0xRGdvLyBVqNjowEUDkOjT0t.png', + u'Gulli Girl': logobase + '2IynBdmw3mmdXt01r8DXKxeor7STnw.png', + u'H2 HD !': logobase + 'U7FUILutKUn8pF2W5JdMGMvcfrKIIb.png', + u'HardLife TV': logobase + 'fHPc5oaRdIzKpHTqBFnHA4O2LVf91D.png', u'HD Life': logobase + 'jUteUS0xRGdvLyBVqNjowEUDkOjT0t.png', u'HD Media': logobase + 'woAI3zcytfbyiX0LBRToKzErJNy1qF.png', - u'HD Кино 2': logobase + '8zMANTj9xekiWSig7DHFVSb1HlzJdw.png', - u'HD Кино': logobase + 'iHVs7YvUVlUvMTnlma7GMpX5p0Tpy1.png', - u'HD Спорт': logobase + 'dgBOid0Zm2uOU5zvI0ZBAKqUD3n0fE.png', - u'HardLife TV': logobase + 'fHPc5oaRdIzKpHTqBFnHA4O2LVf91D.png', - u'History Channel HD': logobase + 'VFgU260pmiIyPxCzD3f8R7Yc6DXClH.png', + u'HD Media 3D': logobase + 'takNty6pirY7TeCRPX5VyUW1mZHHxI.png', u'History Channel': logobase + '9cVifexiWW0qWDhhpnLNVydoZkeRqZ.png', - u'Hot': logobase + 'OjgEHQGo84KMtVReLDbO2zDGF4r2Jt.png', + u'History Channel HD': logobase + 'VFgU260pmiIyPxCzD3f8R7Yc6DXClH.png', + u'HiT Music Channel': logobase + 'n7tghbIGAAh8cyWe7Li0E2iNEJSqkl.png', + u'Hit TV': logobase + 'FnCnW1vmvm8gjAwMGXe2Q8qtN1C3zy.png', + u'HIТV HD': logobase + 'rx6sjyAVpjkYDNPEwX5XksWyxx5mVg.png', u'Hustler HD': logobase + 'LBz8ia8AASewVuLjMs5v4MDiVYfsJO.png', u'Hustler TV': logobase + 'wgAR4TI1xdd2LAtnbkpvFAfnnn5Uru.png', - u'ICG TV': logobase + 'seGXEcBUOeAmwcn2KezmYYuEwTOH8J.png', + u'i24 news (Израиль)': logobase + 'eIIDTFtWAOPOT8x5p5ICSucpe8JBZo.png', + u'Ictimai TV': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', u'ICTV': logobase + 'YuNtYxhj9vqgTU9kVuz9imhqviY4PZ.png', - u'IQ HD': logobase + 'tkCaOFZ7Xf1TmIzhazLXZlcWfJa8Od.png', - u'Info TV LT': logobase + 'Sv42hVuB4nb2y04ggrhadg9FYZI3Kw.png', + u'ID Xtra': logobase + 'fq3ovkZeu4R4YVYt1VMZN1NU7mMrH9.png', + u'Idman TV': logobase + 'UhhUZdtM7FjDFbmoleWalj7tp6nj5o.png', + u'Iland': logobase + 'S2eR34Z8l9ne1lteBOLvknOq64jMJN.png', u'InterAz': logobase + 'rdntbmpYEYtyRbW3nGa1FUNZPXF53O.png', - u'Inva Media TV': logobase + 'mGWpCUkls40liFDUVrxhaIAFwYXZSt.png', - u'Investigation Discovery Europe': logobase + 'QvF9d3DYyndsYsyfjC8aWeSzy6hpns.png', - u'Jahonnamo': logobase + 'OqNuJdvRTdBh5NeydnpmCfMnTJs9XZ.png', + u'Islam Channel': logobase + 'XqP875iZVDQXcyNB4AnAEZ4n3PX9xs.png', + u'Jasmin TV': logobase + 'MBRUFcRx5wtHLZmgahvcGdXB2ERdst.png', u'JimJam': logobase + 'BPDFCK5SQF3mXu5MsDNSdtvz4Gjawo.png', - u'Jurnal TV': logobase + 'qNBTlT1Dtnl0X87LipDLE7uJ0pgLj2.png', + u'JuCe TV HD': logobase + 'oDJZW3oCgEFhE5Cvrm7YfcS63iag5x.png', + u'KBS World': logobase + '0bbrB0JSsAxtzY3suHEeph2V6oJGng.png', + u'KBS World HD': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'KroneHIT TV HD': logobase + 'dq6m0UE9ffgAbXNt49qDlIAr7T8lud.png', u'Lale': logobase + '99AZ5VwFy9yZrgsdZ6kIxcoYqwSyUH.png', + u'Leomax': logobase + 'HGm0W3LbxRPeRcgEdYuuBEKp1zJ104.png', u'Lider TV': logobase + 'LByLhdeQ30Ln5pSZRYSpoLpXQS5Ytr.png', - u'Life78': logobase + 'ZloJoTx0yurBDfEtB6V91vRuF8mxB4.png', - u'LifeNews HD': logobase + 'Mvurp6cp7Sq2fV3tnFBwPtJy7Ifm1i.png', - u'LifeNews': logobase + 'K8dBCUNz1BSwbgUYYU04i2qJpQKLMc.png', - u'Liuks': logobase + 'niAMFUABvOhd1M42fmveGQcATd8lK7.png', + u'LTV World': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Lviv TV': logobase + 'VMl4S9yd5IYFAVFaR8XG6KEm5pgVu4.png', + u'Maxxi-TV': logobase + 'uqyYr07PPk2OuEdnNrbkMHVlGZCJp5.png', + u'MCM Top': logobase + 'b4SLIxXFbKUErZxBXfGoo6eQN5Mu38.png', + u'Mezzo': logobase + 'GUzJ4cyK50ZahOSIx6tTbwGLJiUnSA.png', + u'Mezzo Live HD': logobase + 'lGOtRiUoGyJUMCtAtUwevBerpQORD4.png', u'MGM HD': logobase + 'o4K0lgc2D1GkuFwMC3Cdg1uK6fMrG4.png', - u'MTV Adria': logobase + 'CiM7BJh1M8SscxNfX5bLhTsizFHubP.png', + u'Mi Lady': logobase + 'Fd556PD2ffD6zdVUiX0dvVfBJavBNd.png', + u'Moldova 1': logobase + 'OdWQq0RFoU0GODPHsR94zpTnyFoUX2.png', + u'Moldova 2': logobase + 'V0jtpwewWSjbPJS7KCkIbDXkQA5ne8.png', + u'MTV AM HD': logobase + 'tctwS97YUw7YyfwmeST8rXLHfrQC0v.png', u'MTV Dance': logobase + 'ZXKNRw6Ai8u4lY0wxeycQwj2dIkm66.png', u'MTV Hits': logobase + 'iLJhuLh9kFLQkG4ERvLGSjSgMfNiM4.png', u'MTV Live HD': logobase + 'WjyYXtYHhG5COxGab7luHb1bmvAioA.png', u'MTV Rocks': logobase + 'SEwn7rL2FxPcf5Ol9KRKedIwXlpsAP.png', u'MTV Россия': logobase + 'P5ijp2sRQsKVZkOJwjdqIVgYdJzhpZ.png', - u'Maxxi-TV': logobase + 'uqyYr07PPk2OuEdnNrbkMHVlGZCJp5.png', - u'Mezzo Live HD': logobase + 'lGOtRiUoGyJUMCtAtUwevBerpQORD4.png', - u'Mi Lady': logobase + 'Fd556PD2ffD6zdVUiX0dvVfBJavBNd.png', - u'Motors TV': logobase + '5t2PirCczEeIqzonCtgbmgpbyOZO5q.png', - u'Motorsport': logobase + '7fAmhbXY4MwyRxACHpWPIaNqNiW6Y8.png', - u'Movistar F1': logobase + 'no1c6DhrTCZVqnq9N4Z4SZ2dKx3DyU.png', - u'Movistar MotoGP': logobase + 'nvj6jOmluzhKXuUoXWePwrccFE3DO2.png', + u'MTV Россия HD': logobase + 'YOE9yEg2KwmItjHwMHcQDVvIMsfOTg.png', u'Music Box RU': logobase + 'zaHCW7nPCyGRnqHDkenCIXo7d6vR7v.png', u'Music Box TV': logobase + 'fvt4pris0lwnVhSyUrh8QlyzWBhbgz.png', u'Music Box UA': logobase + '8T7Rnct8q2VHhRm0BcGFxCjxuYEArc.png', - u'Music Box UK': logobase + 'KUbeGDe2HthXSDa8OqMhia0nhTSL61.png', - u'Music InTV': logobase + 'QTJnJokXeUpNksMV5cp0TiE9lyUesQ.png', - u'NHK World TV': logobase + 'JJ8Sh9c7zA3PaXlK1ZaNjy3GgWQFh2.png', - u'Nat Geo Wild HD': logobase + 'YYa1wyNA9prFK1APZ2ZSHGirPpm8kY.png', + u"Music Choice 90's": logobase + 'QN9CRHnBlwKPK555QLsUWwoyGEJmox.png', + u'My Zen TV HD': logobase + 'JkOLtB3RVvI3d2oOC2RSE2ZynIVltv.png', + u'N24 Austria': logobase + 'aQjAnNNoEEtgaFuBFQkBSfh8A5kwlX.png', + u'N4 TV': logobase + 'R1NDSrUKnB1RoT8iv1okSe2xY39W4J.png', u'Nat Geo Wild': logobase + 'ciHIDUuHEnkEuPghbcqkDQx4vadle3.png', - u'National Geographic HD': logobase + 'hK1waimMq9eAp0ugM19moSoQvUeve5.png', + u'Nat Geo Wild HD': logobase + 'YYa1wyNA9prFK1APZ2ZSHGirPpm8kY.png', u'National Geographic': logobase + 'i6STSw6Hg1wWP18yBAOyKoKpSMeKLu.png', - u'Nautical Channel': logobase + 't3fIrxEyf2vpizbuznXFKet6yOpEfh.png', - u'News One': logobase + 'WmOw8mtP3YhXptHbvayITx0TNJdB5Z.png', + u'National Geographic HD': logobase + 'hK1waimMq9eAp0ugM19moSoQvUeve5.png', + u'NEBISET TV': logobase + 'bugOHtRHyF9Nx5ba50Eyf547mn8kS1.png', + u'News Network': logobase + 'puoOIFeHu3ojMldKVG7VVObJji589p.png', + u'News One': logobase + 'SnaQqBa1YMS2b8b8EGdZSMCConLbDS.png', + u'News One HD': logobase + 'jlgF3yZBnmiBKmYCa20SmajQmPqqYQ.png', + u'NHK World TV': logobase + 'JJ8Sh9c7zA3PaXlK1ZaNjy3GgWQFh2.png', u'Nick Jr.': logobase + 'D87kJ3fIWIm5wKi5qxm24nbuPQv0U8.png', - u'Nickelodeon HD': logobase + 'CFYx7Bd1aDSgkqjMLLQjFE6xe3u1E0.png', u'Nickelodeon': logobase + 'j66xpaZbfiYIgQxv76QAPckPVjmLNs.png', - u'Nova Cinema': logobase + 'cgxxcQ2S8E2XMoSMlF5pLdQ9ySJD0e.png', - u'Nova Cz': logobase + 'Z3wXiyZqhbm9uRZDAJcVuaQjoESQtr.png', - u'Nova Sport BG': logobase + 'BZNQubMesUUPrPyQdhdUEICSjkoaXf.png', - u'Nova Sport': logobase + '8npwMe6SAEgj5nMBCoVPCemX9fKOvI.png', + u'Nickelodeon HD': logobase + 'CFYx7Bd1aDSgkqjMLLQjFE6xe3u1E0.png', + u'NRJ Hits HD': logobase + 'lSwvcnY5XfGEwyrKSvygH73fcYDZKI.png', + u'NTD TV': logobase + 'iy4LZ8PlkstKJ2uuyIELVMuCSz5yEy.png', u'Nuart TV': logobase + 'aqMIuUixqLQYmJITPnOGtFkRPuTqKa.png', - u'O-TV': logobase + 'UlIt4rE3FZs7IQmPN3tsGS6fqY5F1D.png', + u'NUART TV HD': logobase + '1Ivzqb3yf7X4nk8zQNkCZmW0vthj6g.png', u'O-la-la': logobase + '7ddvb8Tivq7yuEgffrKildHi2BQcCE.png', - u'ORF Sport +': logobase + 'JQlgjMTC1udkBdrHMiKd2UIV2sJlvw.png', + u'O-TV': logobase + 'UlIt4rE3FZs7IQmPN3tsGS6fqY5F1D.png', u'Ocean-TV': logobase + 'cvBAngU16nJU1bEzxAEcMPiPvf7fVT.png', + u'Ocko Expres': logobase + 'xh9xvE50KtXueT6WGZt7QBWZk7y0uL.png', + u'Ocko Gold': logobase + 'ZfRkKa5NkLQ0rxDI0Z4XHqTsJkQs2Y.png', + u'Ocko TV': logobase + 'GRn2qo7rOMBDzPPSBdF1NaWsLPEtbd.png', u'Outdoor Channel': logobase + 'SuxFoocUmay5G8jtfJaHL1yIbAT5Hr.png', - u'Paramount Comedy HD': logobase + 'AvH8i4NV780Q7PcHi5KkxnCDU0Y0yl.png', + u'Outdoor Channel HD': logobase + 'SuxFoocUmay5G8jtfJaHL1yIbAT5Hr.png', + u'Ovideo.Ru HD': logobase + 'DecuiVbBOWvLVg2VX3MXmegwxCdowF.png', + u'PanArmenian': logobase + 'Goi0hXa9CeThenpkvfPq7E961TMUAT.png', + u'Paramount Channel': logobase + 'Iyzjhb5BBRKfvEZdBwNJ785jzroODU.png', u'Paramount Comedy': logobase + '5EtvAWXB7VK1Yw82yvO28sY28dU4ZC.png', - u'Pingviniukas LT': logobase + '8X45gGlJq2Vx2jUts3EhYESzpwjmPo.png', + u'Paramount Comedy HD': logobase + 'm8YTKk5S5vVQ1rdmi1lsgTCOQH6lY7.png', + u'PassionXXX': logobase + 'U2oO3j1eda31nrTwHqaQ8psrLfH5CD.png', + u'Phoenix CNE': logobase + '4A3PI1xIjIeECNSMDDvOfwXd5n2sso.png', + u'Phoenix Marie TV': logobase + 'mC9hfoTbTEXlNXEVFrJvXaj78QGUhA.png', u'Pink O TV': logobase + 'aI99kH6bHY5Qt2ph4W1Nh0wxdzd1p8.png', u'Playboy TV': logobase + 'lIWBxmt5GDl9tg4KsQNtA0CuZWdOHH.png', u'Polonia 1': logobase + '3fzUyCBgUoJ6zqc92KSXh9g9utJEuO.png', - u'Polsat Sport Extra HD': logobase + 'AdIBJziuh63ffasA6KeDMAKV9DPBqL.png', - u'Polsat Sport HD': logobase + 'zAC1zjpeHrGa3vb5nPLvvNJ8SooQes.png', - u'Polsat Sport News': logobase + 'WhGmGGlQXEvepTfPBd3u1to4ekbtDN.png', - u'Premier Sports': logobase + 'dIKsh48t49lFlDcKPhHKsvYsKaOfNS.png', - u'Pro Все': logobase + 'F8P8nU4YKStbVP2CQD1iaTkcDBzaJn.png', - u'QTV': logobase + 'PahwxLR2J1qfVSGGr54hdPeUD5vpsy.png', - u'RMC TV': logobase + '3CiAgachWg7ohgoU1Gilcm73hXhT41.png', - u'RTVi': logobase + 'QBnba0xrWtpPWLL4yKDRixaCRQAmaP.png', - u'RU TV': logobase + '161.png', + u'Pro Все': logobase + 'OavKhOpBMn27qPDKJXXm5rgATgovgn.png', + u'Publika TV': logobase + 'hPRLrpD7ukXhAbmEYnLIJu3naFWWrX.png', + u'QTV': logobase + 'zmh1xStjBZGJ6U5g3xLoemD2oT3w3h.png', + u'Rai 1': logobase + 'jTchRtiY0FJ91k1YQxBVHgi0rJ2fJX.png', + u'Rai 2': logobase + 'NwtqcjZNlE2S71D47e4VQua263z0yw.png', + u'Rai 3': logobase + 'IpAZDnIoXaiRiteKWVWNRIVFIlf8Sy.png', + u'Rai News 24': logobase + 'CXDYq1FWPBJgCMUXcOmmfSA4hff9qe.png', + u'RaiNews 24': logobase + 'lSQGWWO5EgBMQYSzctZ6551jifnl8z.png', + u'Real Madrid TV': logobase + 'KfoLG7goywcRhMMCc3sO8IVwhuATLp.png', u'Redlight HD': logobase + '5tFZcJtKAZRbXtKGDuLe8FZ3lK9LI5.png', + u'Reshet (Israel)': logobase + 'Phkr2nC2VEYdqNjV4wDZ1cKRvm0IfM.png', u'Retro Music TV': logobase + 'zVS9G1oine56udlAK30gNut16iJ9Ft.png', - u'Ru Music': logobase + 'sAVGvLHkObOsYWh36zLIDjxTCWANlo.png', + u'Revelation TV': logobase + 'gK8K5dIwNW8YqrGZCilO42UX0NF03i.png', + u'RMC TV': logobase + '3CiAgachWg7ohgoU1Gilcm73hXhT41.png', + u'RTG HD': logobase + 'g4MDyw0yqXWkIar8eH0cgCz4xhiKON.png', + u'RTG TV': logobase + 'IeaOjwR6Q9eJjGZr0LYk2tpchM3ITZ.png', + u'RTL': logobase + 'Ho3hZD4j3ZPbSrbPPw55sUmty44Lqy.png', + u'RU TV': logobase + '161.png', + u'RU TV HD': logobase + 'YiMEY6LT8ePoGhXJ7lKz7Os4hY3DOP.png', u'Rusong TV': logobase + '186WxZMn3PGQyMlWsItM9JkSS4Tt29.png', + u'Russia Today': logobase + 'rL14fwCe8q10mKTchOwLkfwQVki9XK.png', u'Russia Today Arabic': logobase + 'OPbYfpQc4ShP8JmgPeiavUroY98H8L.png', u'Russia Today Doc HD': logobase + 'b8QePfFi6zsCDS7hfTeFWES5UN4SAk.png', - u'Russia Today Doc Rus': logobase + 'VOYx5PIhDPrWiZIcCYpm1xrDSQZnsN.png', + u'Russia Today Doc.': logobase + 'VOYx5PIhDPrWiZIcCYpm1xrDSQZnsN.png', u'Russia Today Espanol': logobase + 'zkPwjfFbktNO8EYDaHlYhwAeT88dmY.png', u'Russia Today HD': logobase + 'rL14fwCe8q10mKTchOwLkfwQVki9XK.png', - u'Russia Today': logobase + 'rL14fwCe8q10mKTchOwLkfwQVki9XK.png', - u'Russian Travel Guide HD': logobase + 'g4MDyw0yqXWkIar8eH0cgCz4xhiKON.png', - u'Russian Travel Guide': logobase + 'IeaOjwR6Q9eJjGZr0LYk2tpchM3ITZ.png', - u'SET HD': logobase + 'sX1unYoKj8JR7m8lbtkmPCClRrjAZ9.png', - u'SET': logobase + 'tKzOKIcOQYrFl1VL8B0QqERFXCAYfU.png', - u'STV': logobase + 'DbKEKL5gUOFHiruYRjY2H9gTLOV5mu.png', + u'Russian Music Box HD': logobase + 'CbPggS9SpKMrgFW5myDvhLEFvm0hwp.png', + u'SAT.1': logobase + 'eptvXkOPV7lRukQOEmjSvO0yv5aU29.png', u'Satisfaction HD': logobase + 'isdNgbfGENuaDPSMzsz8WMjBzc1rah.png', - u'Setanta Sports + HD': logobase + 'jW7pJhmebW2fZsXUTvDuRQLahgiLlU.png', - u'Setanta Sports Eurasia HD': logobase + '1wgHdJP76TCItF14FxDwBtak8tmxRv.png', + u'SCT': logobase + 'bDRN0guXHaNHruxtyFkpKYz8J09Xj3.png', + u'SET': logobase + 'tKzOKIcOQYrFl1VL8B0QqERFXCAYfU.png', + u'SET HD': logobase + 'sX1unYoKj8JR7m8lbtkmPCClRrjAZ9.png', u'Sextosenso': logobase + 'HXMvFMLO9weHUcolVmmMKpz98T0K4K.png', - u'Shant TV': logobase + 'IgJpH1JzyjI55Ki2G2Ybz6jzOkwG0T.png', - u'Shop 24': logobase + 'bCuxLyvoTk8l5cBRSyKFXjWjYucvlu.png', - u'Sky Sports 1': logobase + 'pzVzAnJJ90768a73nJXWFAni9yYPT3.png', - u'Sky Sports 3': logobase + 'UAUhQNqNqyEgXdUoeffjssUZ262Lsx.png', - u'Sky Sports 4': logobase + 'qqJzEqYys67VAtSt59keaQaJdM4LUg.png', - u'Sky Sports 5': logobase + 'yFt8HuKsDvuLii304FlhbEIPZ3gTvi.png', - u'Sky Sports F1': logobase + 'J9T7KUE84YobHuNThXN6Hl9UG21tD9.png', - u'Slagr TV': logobase + 'MGCm4Mz8Ggf7xrG9CpfB5QUUOcmfEq.png', + u'SGDF 24': logobase + 'zgX83hMJYukAhttCXvDa12o959Mt82.png', + u'Shop and Show': logobase + '7NlIUTEOE8fvLFVxXCRqpYzhuuSMmB.png', + u'Shop24': logobase + 'bCuxLyvoTk8l5cBRSyKFXjWjYucvlu.png', + u'Shopping Live': logobase + 'HVwxC489SYFr8Ttqs1he9RZjEmJjPn.png', + u'Shopping TV': logobase + 'Yhipu86vXMGf0hxCTXLezpGpcUbibW.png', + u'SHOW MAX': logobase + 'BwzG83cJD6do7yT2lUE1QKKWlhLPxh.png', + u'Silence TV HD': logobase + 'DoGC3WJCbY8YaqqDRNKozHipJbcv4w.png', u'Sony Sci-Fi': logobase + 'zLWEgf9BbxBr1TR2Qj2NxVQBuPecEP.png', u'Sony Turbo': logobase + 'CaPjVaQrpyN138TarQ7CYBqBOz0ZF7.png', - u'Sport 1 Cz': logobase + 'kCLlfkFz3Ba3BL9Jc9ZPgUKXh2piyv.png', - u'Sport 1 LT': logobase + 'nRFc87aOV1vRnjEqmQZUneZe4HiCqn.png', - u'Sport 1 Select': logobase + '899pteevSriMFxe4omDA4G6l9i0czY.png', - u'Sport 1 Voetbal HD': logobase + '0WEqpl3cqObcLs2J0l5DDhVPZalvXx.png', - u'Sport 2 Cz': logobase + 'YLmEjnczWQGJcZC0SxRcH4ifPcwYlx.png', - u'Sport Klub 1 HD': logobase + 'cLQ3uuWhQqxCQk5RUDwA9x7bLUHBwn.png', - u'Sport Klub 2 HD': logobase + 'LiIua5Nyy8xdHFYhwgrwbcajbKz6fH.png', - u'Sport Klub 3 HD': logobase + 'UkUGpf3hamDPGPtkqximS96rrts4jx.png', - u'Sport TV 1': logobase + '7u5sbYjzJdQopdQ6bAH7GLDUsPWnXc.png', - u'Sport TV 2': logobase + 'u6T8L5PPYKHCbBATjdzjLpTC8zzCdV.png', - u'Sport TV 3': logobase + 'dYTM6Oqhaqw18FI6uYPS5yhjCmc1nZ.png', - u'Sport TV 4': logobase + 'YfFhr0OCmbN8vHUuGCLp488dxGpKVw.png', - u'Sport TV 5': logobase + 'YyzfJTMsBcmTKptLzxZcAcLKFj52LT.png', + u'Space TV': logobase + '5WT7hzZe3qEzKV1Diqwyd37xS671BG.png', + u'Spike': logobase + 'KPNj0b4rZBtEB50yasdM72Th7UqrKm.png', + u'Stingray iConcerts HD': logobase + 'E5ATzWWTj9YEisp6P9Vhm6GaYzZb2N.png', + u'STV': logobase + 'DbKEKL5gUOFHiruYRjY2H9gTLOV5mu.png', u'Super Tennis HD': logobase + 'mjQW91VJdjIEhADvOO2s6OiKNeUdUK.png', - u'TGirls TV': logobase + 'FufZ2heFswzvAbRkTQZs8UJBYGsxuG.png', - u'TLC HD': logobase + 'gT4olUY9nFJbGRCdwd7hHJp1NJ5eJr.png', + u'Super TV': logobase + 'iRrgl4xiCvibdxWfiQcVUWuYCYCI9K.png', + u'TBN Europe': logobase + 'bZT2NYEtReHVOSFTcW8xuDTVDvcoUn.png', + u'Tele 5': logobase + 'BahbW0uBxpVOHayoLrr6uFcaBsiYSc.png', + u'Tele Vsesvit': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'TelePace HD': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Telesur': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Teletravel HD': logobase + '4ZlASq3oDpOjXfhwluOzY74sy9elaE.png', + u'Teve2 HD': logobase + 'tWjvTVsZwROaHw0WT4lajACjJ7IcKN.png', + u'The Word Network': logobase + 'R9UwwsopX0YntvnH7OmrENwZrKhuIn.png', + u'TiJi': logobase + 'mD3GW0E7rdPwc4stjk7xrLI2gZn4Hq.png', u'TLC': logobase + 'gT4olUY9nFJbGRCdwd7hHJp1NJ5eJr.png', + u'TLC HD': logobase + 'gT4olUY9nFJbGRCdwd7hHJp1NJ5eJr.png', + u'TMB RU': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'TOGGO plus': logobase + 'MgIYkMn6crZV90Bc8As0Ljg8L1p9uy.png', + u'Top Shop': logobase + 'uD257Lhw7Ko2YD1reC0nRqW7lpy93D.png', + u'torrent-tv.ru': logobase + '3LPFwCA0df8lBebrPhStZVoNWLMjvk.png', + u'Trace Urban HD': logobase + 'VL7CokCNOQomUM5v9Wmhv2EcnMHdvY.png', + u'Travel + adventure HD': logobase + 'b1HifWKMyefmDDvaDAJTwNNTaD8LF4.png', + u'Travel Channel': logobase + 'fhmrYjlpC0YMxFd2RqOolbjlXMr0tI.png', + u'Travel Channel HD': logobase + 'zfnAGLCvIu1fx9hfrITAZMoo9HYww4.png', + u'Trick HD': logobase + 'g19Pt8UbGKOyNuHvCRlwe8HngcY9cB.png', + u'TRT Turk': logobase + 'BDkqm302OLdoXCwpKFVUe8S43OlWj9.png', + u'TSN 1': logobase + 'GJsJeixsOKvFaXz8XKqvX4Uh6sUicu.png', + u'TV 1000': logobase + 'WJMEvVafVakrm7BUMy1lzku7VQCx25.png', u'TV 1000 Action East': logobase + 'GblbxkDGXZyW5oWt9W8wuERQAiZ7ZT.png', + u'TV 1000 World Kino': logobase + 'A5HJc3MwXrOfesrvG8iCiWGnFYqFOA.png', u'TV 1000 Русское кино': logobase + 'ch5DX6f8hxDnmyzrjotUoKHNGzcw9P.png', - u'TV 1000': logobase + 'WJMEvVafVakrm7BUMy1lzku7VQCx25.png', - u'TV Bakhoristan': logobase + 'LoXaN929SQC5r5aQ3JETDXwG6VlPMk.png', - u'TV Plus BG': logobase + 'cxxTbCRSZsh4l1CNpZ4mYychepxUGw.png', - u'TV Safina': logobase + 'mJUmNhJbQqcr2NPppAryEJqDPBJGV0.png', - u'TV Sale': logobase + 'hs0YdiUTlpRtb3wTiP4cXboX0H9oTN.png', - u'TV Smichov': logobase + 'hqgkCNoqMXiAgNU6uedqUNIR7Z0ox5.png', + u'TV EVROPA Bulgaria': logobase + '78SVTKvw7RzoIMaDyfjdH6tqRtGwF3.png', + u'TV Mall': logobase + 'hao8n2RWJzxwNaiVHUawwkNVG6X4Zp.png', u'TV XXI (TV21)': logobase + 'TKchoTWZFRMmGDBok08zoEFJ8mJJCe.png', + u'TV-4 (Украина)': logobase + 'aAcZbGFfyqMbiTybIo3eDV2gty3zFy.png', + u'TV-5': logobase + 'iPHeoajUJttv5qZl2yfcOYB1qFx6nv.png', + u'TV1000 Action HD': logobase + 'OdG0mEhFWsZehYaUmQRX2IdtPlGr7y.png', u'TV1000 Comedy HD': logobase + 'ygGiR2hkQLySH6khdo8GV9CyMJ8dXi.png', u'TV1000 Megahit HD': logobase + 'lVPY7WCjn1WM6NL6tfLFy8iGA4yk3Z.png', u'TV1000 Premium HD': logobase + 'raoDrpin8VKmi522LZWzSF0fLRO04m.png', - u'TV2 Sport HD': logobase + 'iL3TM972YPxOxajyfbuNcKGPFrVvTg.png', - u'TV4 Sport HD': logobase + 'm8tNJfJGN7cYZtUWBggz3PMVB28clK.png', - u'TV5 Monde Europe': logobase + 'ko7rbRBnyK1iINkLOA2adRvgVOEgUK.png', - u'TV6 LT': logobase + 'SKskx67yBUvbTdMIIZjH1Z4EcB8nYX.png', - u'TVT 1': logobase + 'CKKdhDfmno9O52tMfWptiAQT0IBWV8.png', - u'Teledeporte': logobase + '57r0Kq1rFB6vcMeldfWDvp438Jz5qT.png', - u'Teletravel HD': logobase + '4ZlASq3oDpOjXfhwluOzY74sy9elaE.png', - u'TiJi': logobase + 'mD3GW0E7rdPwc4stjk7xrLI2gZn4Hq.png', - u'Tonis': logobase + '38BRA5jO6LAsQ6rv1NC3FMJ6KALp8z.png', - u'Top Shop': logobase + 'uD257Lhw7Ko2YD1reC0nRqW7lpy93D.png', - u'Topsong TV': logobase + 'DsJRpcbI6rgjONbQftC5nHt1XMAXYQ.png', - u'Torrent TV - Android': logobase + 'wf43FCQBGnvSrknDmSJXTOtbVWgOiP.png', - u'Travel + adventure HD': logobase + 'b1HifWKMyefmDDvaDAJTwNNTaD8LF4.png', - u'Travel + adventure': logobase + 'b1HifWKMyefmDDvaDAJTwNNTaD8LF4.png', - u'Travel Channel HD': logobase + 'zfnAGLCvIu1fx9hfrITAZMoo9HYww4.png', - u'UBR': logobase + 'F6EzmjkOBVB0gmn1kQX6itv5VvFml5.png', - u'Ukraine Today': logobase + '3AVq6O577A7uw9uZ7fxIvpvE3CxdtW.png', - u'VH1 Classic': logobase + 'FhxUFQ2Bsfom4vb8Ce41gFObAbh1Vh.png', + u'TV3 Catalunya': logobase + 'JdogLgSiKoXxC7ZlxVLogeOLMQUQyN.png', + u'TV5 Monde': logobase + 'ko7rbRBnyK1iINkLOA2adRvgVOEgUK.png', + u'UA:Крим': logobase + '1oa1CrfXZ94gpdjszpFhOmEtO8mV6K.png', + u'UA:Перший': logobase + '7jcbPt9G0pkO42ROJ6p0CbfyugTBXQ.png', + u'USArmenia TV HD': logobase + 'llpqN7S6EpCPdoYIHjle6gc4cXlk17.png', u'VH1': logobase + '58.png', - u'VIVA DE': logobase + 'HagNMshKtJ7zKnk9fdmBLhITjoWdrJ.png', - u'Venus': logobase + 'R2ug0cuB3SmBBA6LK1uoNbEV66u39v.png', + u'VH1 Classic': logobase + 'FhxUFQ2Bsfom4vb8Ce41gFObAbh1Vh.png', u'Viasat Explore': logobase + 'uCqpsdKP0ialUUYxUk2fXshYdYfxzW.png', u'Viasat Fotboll': logobase + '0JLqj3qwFoT1Y61scCyUdWioV5U6hx.png', + u'Viasat Golf': logobase + 'IGpQl5iTxaDEPyKffdDEpU0EU0SPiO.png', u'Viasat History': logobase + 'MWGbB8wJp5Gm4vbPHl0ktohDDjMKdr.png', - u'Viasat Hockey': logobase + 'CuAbCRGdf3Z1FGFiwErTbHZ3lAMJzr.png', - u'Viasat Motor': logobase + 'RuYtGxEpqJ5DG7WxGCMWNDXosRdh59.png', u'Viasat Nature East': logobase + 'yimDcPvajJcUKQm9bY15cDdp3rJFcp.png', + u'Viasat Nature HD (Европа)': logobase + '6iBmDGCV7UArjU0ZkKnOZDcyB1FbYe.png', u'Viasat Nature-History HD': logobase + 'pSP6zxmuO4PU6xa6KRlZ9L8vvVM2Dy.png', + u'Viasat Sport': logobase + 'prAZKkny3W1HGM03lP0EhzcMmTPZdi.png', u'Viasat Sport Baltic': logobase + 'ZIITckvF1w5u1MlubmhoG45HxPgcZZ.png', u'Viasat Sport HD': logobase + 'prAZKkny3W1HGM03lP0EhzcMmTPZdi.png', - u'Viasat Sport Sverige': logobase + 'prAZKkny3W1HGM03lP0EhzcMmTPZdi.png', - u'Viasat Sport': logobase + 'prAZKkny3W1HGM03lP0EhzcMmTPZdi.png', - u'Viasat TV3 Sport 1': logobase + 'LUsZ9yjy6izQJHd2z2Hf7uBZ4UyUcM.png', - u'Vip TV HD': logobase + 'VXNvw8nbJhjRncTmxkuglf8htUxN2N.png', - u'WedTV (Свадебный)': logobase + 'u93ysJkZEp1NzeG7jTbVgB7nKhDTqH.png', + u'Vintage TV UK': logobase + 'mhFKSRmQIsgPnIUCWFzWbMAXK5dn3i.png', + u'Virgin Radio TV': logobase + '6DgkEMl3HtkpKQbzVovPdSYhy9f3ne.png', + u'Visit-X HD': logobase + 'PKLqXFmYlYpo1RbkynS7SfL79gH9fg.png', + u'Viva Austria': logobase + 'sUPeS0888UCOztGNrzvtpXumWwg3ao.png', u'World Fashion': logobase + '2YI4sT9YkGezrw9vZPn0uIRhZ7E2BV.png', - u'WowGirls': logobase + 'phiImbBi8hRs3LqmOOpLVsPqQkD2Hc.png', - u'XXL': logobase + '6nJtj85PlL0MxB8RDkM3toyGND3Anc.png', + u'World Fashion HD': logobase + '2YI4sT9YkGezrw9vZPn0uIRhZ7E2BV.png', + u'Xezer TV': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'XITE HD (NL)': logobase + 'N1zheznjGW9sV4i8GChlBD6sJ3009k.png', + u'XITE Music HD (GR)': logobase + 'FeWCTdXHZKnid3qXQNH7afhkBDdA0t.png', + u'XSport': logobase + 'SCwOf3movQkrJf7ez4tNLZYlWo1b7t.png', + u'XSport HD': logobase + 'CK1ynOTAF5wAvilMpyzryKKacuSdDl.png', u'ZDF': logobase + '5SH5FeZiITw27CPxscjksZp272u7He.png', u'Zee TV': logobase + '1HooaeEhMSvpKmWv6nneZxnTmG5r6Q.png', + u'zhuldyz tv': logobase + 'rleCG7XGat4Ga9MTsfa7JwKQUsJGcD.png', + u'ZIK (Украина)': logobase + 'dB1PmpYtgeI0C4u7Cv1QoXHlvc4JtT.png', u'Zoom': logobase + 'SyisYhg411o7z9kXci4vfpLq4KBZZ4.png', - u'bTV BG': logobase + 'xiNqovHjloSoVzrVieKo6saLQTUnJ7.png', - u'nSport + HD': logobase + 'JSpj8Lq758dRJzBaTEjM8nbSfnLf9M.png', - u'АРМ ТВ': logobase + 'OgrdBlfYISfcpr0XO0ImEyelCMjUVx.png', + u'Абаза ТВ': logobase + 'ljGzZNT1OOt7L0J9gNql2yl39PbooN.png', + u'Абхазское Телевидение': logobase + '3Mk8W64v1CqxF4S14K8pMNXSVFCthM.png', + u'Авто 24': logobase + 'GZwKgvkC0vfYquFAn1dKhppNxdDit9.png', u'Авто плюс': logobase + 'WkRxjy6fJEBJ5NZiaGn2j05eqfFfQq.png', + u'АЛА-ТОО 24': logobase + 'Qje1YfQEYAlYacMBfK8sGqCcJnLe7g.png', + u'Анекдот ТВ': logobase + 'foqd1f29CWMTVE8bNziAGglAEgtGPd.png', + u'Арктик ТВ': logobase + 'cwkVCO8OkPO5wNPC9bmVtpuF6iol2b.png', + u'Архыз24': logobase + 'tuMbRlnkeMiYQ6u9oeiAVfHF0F20RB.png', u'Беларусь 24': logobase + 'GxA1KJP5YwpWc38BoPEmLwQH6uDeEz.png', - u'Беларусь 5': logobase + 'aMU4HXJN11Bo9WissbPW4rhe06vAql.png', + u'Белгород 24': logobase + 'AePWQp63Dr37yC4Q7IGyYJl1T5X3a7.png', u'Белсат ТВ': logobase + '9VYuUQxx1ss7ieu2upENtlibyamBP0.png', u'Бигуди': logobase + 'JvcMdB5e6KVBpbXT12ulzmDqenheRx.png', u'Бобер': logobase + '2Edln8vEbg7UUSVUo7lIJPR780OWAR.png', - u'Боец': logobase + 'pmkJgRqsuZDzuN4c6v6jZaBVKCN3K3.png', - u'Бойцовский клуб': logobase + 'oo4RN3hUUjuVbtegW8Q5QE0bT6GwwD.png', - u'ВТВ': logobase + 'svsUD6TinXyv3B1q5sZf3fI9ebmpaF.png', + u'Бокс ТВ': logobase + 's3qlrVrIjISf7K0G4HHRYw5rmAej4b.png', + u'Бокс ТВ HD': logobase + 'mU9bGk24P0x0ggxiUvkj0LYOBY7QS1.png', + u'Брянская губерния': logobase + 'V5M08NU3jRbd2wKvtVLIRbg9J6ObAZ.png', + u'БСТ': logobase + '05dZ8fzOmf1lXYue2OvVsQ21eIeX69.png', + u'БСТ (резерв)': logobase + 'e1Na3L3JV5aPefPbsE9U9VVdWMvkIw.png', + u'БУМ ТВ HD': logobase + 'wHMGSLfPCHU2MX1y1ro6ZqQMxe0pcE.png', + u'В гостях у сказки': logobase + 'j5XrmEogwV5wTwBqFC9G1pD3xw0Yyk.png', + u'Винтаж ТВ': logobase + '6yngbEKfgy6XtMw28INQCoOdulf2tC.png', u'Вместе РФ': logobase + 'qa50GYekwBWym7KtoJdzrWHWqN8TeU.png', - u'Вопросы и ответы': logobase + 'xbV8M35FkvpieQ3TUEL8fhwU8MzjmQ.png', + u'Вместе РФ HD': logobase + 'JxgnwFeOwqX02WHKKdkWJDoAtpWTCT.png', + u'Возрождение ТВ': logobase + 'CKWaxJsLQYiJmnkLhtysIn8hb0NK8b.png', + u'Волга ТВ': logobase + '1CZtoSAY9ZjEavNeGFTrPvSvFxjAHH.png', + u'Волгоград 24': logobase + 'iCZ3kRKXWJITuH34sJ3HPRNedTtk4f.png', u'Время': logobase + 'F44yKDJQLsX0llpZ2wupg8V5vHx5fF.png', - u'Громадське ТБ HD': logobase + 'Ovkd9TiVv3nLcKPwQS2wkJ85KyYCMQ.png', - u'Детский мир': logobase + '00Vf3rPABNnbNQ6Rv0dnfcg3JsJelA.png', + u'ВТВ': logobase + 'zgmRL374BuIBNBhkkoKUiEDeNMVvPv.png', + u'ВТВ-Плюс': logobase + 'tPmijfGtbGhjmM4y3IT0PzuWbs6dz3.png', + u'Галичина': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Глас': logobase + '6m1xUI7vFyadFMFxXeIZjlATt7rlWw.png', + u'Горизонт': logobase + 'f6wqkzO4WZW7D5Z9xIB4VkMRGmgXUU.png', + u'Горящий камин HD': logobase + 'ujkL8d4MnAfrWSOBme8UxGFq6AJodT.png', + u'Громадське HD': logobase + 'tHAyXEoFX2NzCPgAAsbymwTsL6eEEj.png', + u'Губерния (Самара)': logobase + 'm6uotRQ8g6fjMLfVchecqsj0cbCITM.png', + u'Дача': logobase + 'mX1irJVUyLtrXr9vfDdhknRqiqV9wA.png', + u'Детектив ТВ': logobase + 'iXWgrqBGDDAT4qNehnEFDBBUu8qeez.png', u'Детский': logobase + 'jk8kody2p38CKdj5KGXWMwRLjgFIlG.png', - u'Джус ТВ': logobase + 'qVNFoyUAOJSDvN9tHhf9j2AP7x4VkV.png', - u'Дождь HD': logobase + '381.png', + u'Детский мир': logobase + '00Vf3rPABNnbNQ6Rv0dnfcg3JsJelA.png', + u'Дисконт ТВ': logobase + 'jZ5txPxhQBePijTpiiWtH68Pp4My8G.png', u'Дождь': logobase + '381.png', - u'Дом кино International': logobase + '69MqZE2YHJNewQkqRbJea33WuRkKgo.png', + u'Доктор': logobase + 'FznqTpJ71LS3RLESzMOHhNDxJMwBE8.png', u'Дом кино': logobase + 'jlC78Fy13KWjQUN6l3FtbsRLZDvc0x.png', - u'Домашний (+4)': logobase + 'LRMaRyPCroUq4dVcRhwKJKVuhvdvUZ.png', + u'Дом кино Премиум HD': logobase + 'Xg5l3gO5SnER0pwHhgsbHVMqSb42ty.png', + u'Домашние животные': logobase + 'HiWAmn5RvUKNJnSW2Jhxjs6maoNFV7.png', u'Домашний': logobase + 'qmqrH2E2EX11qitbIvq0CYsxQjsHGm.png', - u'Драйв ТВ': logobase + 'pmmgMKcRbxeYkjVUVr4IAWM0UuZHO4.png', + u'Домашний магазин': logobase + 'yhzajV7vdLogQzMKat0rLJWaDkPpTP.png', + u'Дон 24': logobase + 'qB34tCfiy8RoCwBprbt3yBlb9QTb7i.png', + u'Донбас': logobase + 'vsj1IA3Z8QVL3AzzuN4EshgT4LUmRr.png', + u'Драйв ТВ': logobase + 'uzYQb9aFsagXVHjM9hX3oeAbrY3k05.png', u'Еврокино': logobase + '34mszCG0j0Vf6kFcMrLPnFEA8UPdu6.png', + u'Евроновости': logobase + '6HRWXfDaZ5zdwbwOsq383OrtV4MprK.png', + u'ЕГЭ ТВ': logobase + 'dwe86aR0KHW2VzEyj863SViH9wtUGL.png', u'Еда HD': logobase + 'ojUD1jhpv7HBOLmubpEBOsANkpYNtk.png', - u'Еда ТВ': logobase + 'TWdAdMXfMSylb2mQ4efFnOAYosymNC.png', u'Еспресо ТВ': logobase + 'lOwm890F5URuR5Ej7IacerzECPIDt4.png', u'Живая Планета': logobase + 'xgKSMwqBdEyXnbVgb8LtNXSMiaPcOx.png', + u'Живая природа HD': logobase + 'L9qmq6UIprbg4VBQA46tqRjQ22thsp.png', u'Живи': logobase + 'cOluSjslxxs3JZtSVO8c15xh7h8SDU.png', u'Загородная жизнь': logobase + 'cGGo8HRkVhy66UXKXZ4tH5HyUaaxJA.png', - u'Звезда (+4)': logobase + '01VqCLfVy5OsMBN1qXjxOTp5NKT4QS.png', + u'Загородный': logobase + 'RX345W0BBqJbR3XdROHMi4dnbwqwlt.png', u'Звезда': logobase + '0HLRrFHt2QIkbJpLc1fy0RVe7hqCEC.png', - u'Здоровое ТВ': logobase + '2LgJcyMnjJpMAhUqX3rdQ4ChOmbuTo.png', + u'Звезда (+2)': logobase + 'wa6skHc8W2MRz9Mm3qoKS4LpctSvYs.png', + u'Звезда (+4)': logobase + 'OuOzOS8YQYpfvSiALYw8RE79MLLW8a.png', + u'Звезда (16:9) !': logobase + 'gPzH1fd9puL4joFkEcLAo3C4rc9XR7.png', u'Зоо ТВ': logobase + 'RtAhntWPlKQs6CIYAb72piNF9EsN3E.png', u'Зоопарк': logobase + '1Ugpb5T1THFcFpn19Mnua21KxHkjct.png', - u'Иллюзион+': logobase + 'XOO3bLrAAvCj45nIsxsGCppY14bY1n.png', - u'Индия': logobase + 'XVWyHt5bFFcZNzmysBSjuVdGBGl45D.png', + u'Известия HD': logobase + 'oDcPw8gfynentKu5CDXKlA0lo86tdK.png', + u'Иллюзион+': logobase + '8LToTNvWRBHvb5IKoteKm8EwAGw8mv.png', + u'Индиго': logobase + 'dFIp5shmC5DbfWIDVaFh7coAofmLON.png', + u'Индийское кино': logobase + 'y8wvb5nc77vn5c4x8kinv85c7mv6n875c84.png', u'Интер': logobase + '3SP67FapzyZqMVZTPiJIcN09KRkTeu.png', - u'Интер+': logobase + 'QEdaDBbqr13CCfwKQAP77UZYPQIPn0.png', - u'Искушение': logobase + 'p3WsIen84SZK76zTMWnslNgUjsqsMZ.png', + u'Интер +': logobase + 'gwNJRUrxiQUP4quofAUZwjfn470AkS.png', u'История': logobase + 'PNRaeOUFzOPFtrclFBBRTckj6Lvo0u.png', u'К1': logobase + 'mk2mYb28HFIxkFIiMNQWmKUdn1Y8hD.png', u'К2': logobase + 'IjG76jf8k8HTNLooNpUiEXtkPfA2rG.png', - u'КХЛ HD': logobase + 'kRN7BwVtcdaXrU4Mdg24qhFAxjx9oZ.png', - u'КХЛ ТВ': logobase + '216.png', + u'Калейдоскоп ТВ': logobase + 'YJMqmzlZ87QeXYm9XpjH0XzpjljNcU.png', + u'Камеры Санкт-Петербурга': logobase + 'jHbY0LrB7JKBBWzWC1ctPD6uDqQNnx.png', u'Карусель': logobase + 'S233D4b6eq7KOXfdyi4dY2GokKeltg.png', + u'Карусель (+3)': logobase + 'cYXQq7FnkEBAyYxfoPPsL2y3fwMDKU.png', + u'КВН ТВ': logobase + 'uHs398usENAAKycx5NFCHiC0jDtzrc.png', u'Киевская Русь': logobase + 'C1AZimW2NnNA17H1uJLxxePUMTPQZ7.png', u'Кино ТВ': logobase + 'KkITMDICqC1erWdSqyOqoccqde2wHC.png', - u'Кино премиум HD': logobase + 'p580CRZ8bBS6dw3plMWhhxXSzQ59uS.png', - u'Кинопоказ': logobase + 'v0JEbxExcFI8dVEzCkpZUoktgiS9t7.png', - u'Кинорейс 1': logobase + 'q3N266MTLCzzNVXy3q330VIVgTp93L.png', - u'Кинорейс 2': logobase + 'RO9ac4e18hSAsPhquZ9JzyTHo5oqMK.png', - u'Комедия ТВ': logobase + 'L2MEpT2YePoDvmKRjYy6yyt5ssH1m4.png', + u'Кино ТВ HD': logobase + 'NGQLEmlltpslVNKkD0htUF7CogGXqX.png', + u'Кинокомедия': logobase + 'y087hci8ityixcnyxinxoafhiu.png', + u'Киномикс': logobase + 'y6b876ih8g7R876hfug3897wrhj.png', + u'Кинопоказ': logobase + 'L2qsABeMEDKOSG4Tvh7XpfvX2BfksG.png', + u'Кинопоказ 1 HD': logobase + 'pNA1vR3sPoovYNKzO4TU6NQcSXNqjk.png', + u'Кинопоказ 2 HD': logobase + 'Yf2XKrtorOF2wSZ23Q9NHhL2MfOcqi.png', + u'КиноПремиум HD': logobase + 'p580CRZ8bBS6dw3plMWhhxXSzQ59uS.png', + u'Кинопремьера HD': logobase + 'y4Ihg8o7ikgJHF768iJHFH76iu.png', + u'Киносвидание': logobase + 'y1876iuyf7645urjg56utfy.png', + u'Киносемья': logobase + 'y2oerfuhdj2108nbs4ev875esk47dn.png', + u'Киносерия': logobase + '4TMYdVpZYXafyIumuB5d7PrjFnslyT.png', + u'Кинохит': logobase + 'y3igbjZ&LGyqDWGIASUY78AWID.png', + u'Конный мир': logobase + 'd8gVne1Em14rIfM6pSsiuufXZeYQqi.png', + u'Конный мир HD': logobase + 'q87VlmfaBeBhq8SOhAu0SVPEGjiqWM.png', + u'Кот ТВ': logobase + 'Rtdtc632nbBQ2TsymgXhkOt7nhytjW.png', u'Красная линия': logobase + 'I43S6jd5noclar0LlPJnyY8adonmUV.png', + u'Крик-ТВ': logobase + 'bYwacGhYkGwDoMsjGpDbltdCwMJSHJ.png', + u'Крым 24': logobase + 'x4s9QQXR8KerO13nlkuEujzcv5ww5F.png', + u'КТК': logobase + 'BhI4KURCZ3fAgCQcwQWn7vRCJQblH3.png', u'Кто есть кто': logobase + 'MwNkO3fXd6KefRdiGlOdOQ5q0Zu7kS.png', + u'КТРК Музыка': logobase + 'jqiucXUHkE8UkC4QqeQMt8jcldpJkh.png', + u'КТРК Спорт': logobase + 'x81GWJrYa6mbtQj36q0pg9QnhCKxR1.png', u'Кубань 24 Орбита': logobase + 'FauvJxsKmI5a1fR62uSH9hJfHs5TCr.png', - u'Кубань 24': logobase + 'CAAqiN96tQzFDdtz3vjrrgeIjAKqNq.png', - u'Культура Украина': logobase + 'pyKdve4YhoChQFGSha8J0FBWBf302a.png', + u'Кузбасс 24': logobase + 'E6ftIMytwxMxG1pbehfWfXdqn0iofP.png', + u'Культура (Украина)': logobase + 'pyKdve4YhoChQFGSha8J0FBWBf302a.png', + u'Курай ТВ': logobase + 'A0onEnFhSzqGxckeyyXsf0ZQ4R3Vc6.png', u'Кухня ТВ': logobase + 'G0WbVMphlP9oJ6KvHRfx0xDfhrF9Re.png', + u'КХЛ HD': logobase + 'kRN7BwVtcdaXrU4Mdg24qhFAxjx9oZ.png', + u'КХЛ ТВ': logobase + '216.png', + u'ЛДПР ТВ': logobase + '1keO0CoTmw0B3VZRe4TZnzfGdKDqSj.png', + u'Любимое ТВ': logobase + 'qVREQyj0rN87jDZ1ylYW0sDvzfNp8p.png', u'Ля-минор': logobase + '8FJA3xMMHcrZuGifHViyVQLjVIem5u.png', u'М1': logobase + 'ezvu2ugYMGnZ968LlnjPw7VjqWIPeM.png', u'М2': logobase + 'U4s78hznNz7mFYZQICkxN7J0HTtlCP.png', + u'Майдан': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Макс 24 (Сочи)': logobase + 'EkfRx44dYCkUuU7g0bM5jJz3bAASBG.png', + u'Малыш': logobase + 'lawBkwFuj6qiu3jZQGLnHJUc9wGPwM.png', u'Малятко ТВ': logobase + 'kjYF9vS2IDTMehpzC7WWfjnZ4NVpuk.png', u'Мама': logobase + 'nw9fROQIjjKSDp8Wjkjl1Wt0n0xHxd.png', - u'Марс ТВ': logobase + 'KnDO2ZAW1Xlahhp1ysdlDUPCQI3Jix.png', - u'Матч ТВ HD': logobase + 'MXyy9Uud7oDuH8JqVisjsD0csgAHnQ.png', u'Матч ТВ': logobase + 'hQDOuQjUVczvUU2ocLE0tkC1siCqpo.png', + u'Матч ТВ HD': logobase + 'MXyy9Uud7oDuH8JqVisjsD0csgAHnQ.png', + u'Матч! Арена': logobase + 'DDjh1dM2D09Wcl3L6YmEd1si3P17n0.png', + u'Матч! Арена HD': logobase + 'DDjh1dM2D09Wcl3L6YmEd1si3P17n0.png', + u'Матч! Боец': logobase + 'xj1tPp6g9LmQH5KAN0gFpWYoa9RdbL.png', + u'Матч! Игра': logobase + 'urIB7TbFWo36EmXW9eG3w4qre7MdX9.png', + u'Матч! Игра HD': logobase + 'urIB7TbFWo36EmXW9eG3w4qre7MdX9.png', + u'Матч! Наш Спорт': logobase + 'l0x8GopxrHL6jLiDGVHi8tIEc7RBt2.png', + u'Матч! Планета': logobase + 'mWQ92sSmszzmjme9GTueLPCxagJGtl.png', + u'Матч! Футбол 1': logobase + '9PM8M6cN21wQ3M5isVZgjNepzUI4Ry.png', + u'Матч! Футбол 1 HD': logobase + '9PM8M6cN21wQ3M5isVZgjNepzUI4Ry.png', + u'Матч! Футбол 1 HD Резерв 1': logobase + 'HWZYoNrs7kQimfhyzT8V2d0WvBRbEr.png', + u'Матч! Футбол 1 HD Резерв 2': logobase + 'jJsAF2rxef4RpccQrmfPkIbJc9kS7Q.png', + u'Матч! Футбол 1 HD Резерв 3': logobase + 'Tpv0BbEi315Omo6MKh3ZiX5zTzc8OT.png', + u'Матч! Футбол 1 Резерв 1': logobase + 'CWit8nYXCcCsmB6KbIedlKwFWWTyZT.png', + u'Матч! Футбол 1 Резерв 2': logobase + 'n6kF2auPAk8bMH2zray20zP12QKL1Z.png', + u'Матч! Футбол 1 Резерв 3': logobase + 'ipK5bpcm9mR9COdolCiHV6csIndprl.png', + u'Матч! Футбол 2': logobase + '8MA3WloO6RsWX8N7Ck5ugek2Kirf4B.png', + u'Матч! Футбол 2 HD': logobase + '8MA3WloO6RsWX8N7Ck5ugek2Kirf4B.png', + u'Матч! Футбол 3': logobase + 'OLHdmyfUev4mMX0OGniJrlUwHnMKOg.png', + u'Матч! Футбол 3 HD': logobase + 'OLHdmyfUev4mMX0OGniJrlUwHnMKOg.png', u'Мега': logobase + 'IXY7dRFoq0qCqn4UbY47iP36vVZ6ck.png', + u'Миллет': logobase + 'zDsxAdZp80tZ43Rd2IWAgFOFFYDkwh.png', + u'Министерство идей': logobase + 'k9RMWD6a1nooGeVYJd5ULWhAx7nsTP.png', + u'Мир': logobase + 'Oq6h2IicTagHENQu1mFkjLk5rChMnr.png', u'Мир (+3)': logobase + 'QxOYkz6f80IdhmC4RSHI1cMd32CqYZ.png', u'Мир 24': logobase + 'auv6717gJOWi0A2VoeDQaCsx9G1NOj.png', u'Мир HD': logobase + 'Oq6h2IicTagHENQu1mFkjLk5rChMnr.png', - u'Мир': logobase + 'Oq6h2IicTagHENQu1mFkjLk5rChMnr.png', - u'Многосерийное ТВ': logobase + '4TMYdVpZYXafyIumuB5d7PrjFnslyT.png', + u'Мир Белогорья': logobase + 'JRU6TyiBAX5Fk84DebhKeSxg7ZkrEf.png', + u'Мир сериала': logobase + 'XWDsU7aoUyPPoKfcX7WMYQheuzHJL0.png', + u'Мир увлечений': logobase + 'KtELENmDesBwzAAryLgJPqwlr9m17z.png', + u'Мой мир': logobase + 'cHearBQvhJZUmqc30x3W2yJZHC68MX.png', + u'Морской': logobase + 'uzlb3awoyNqvIcf6i35hTVqf7gvuqz.png', u'Москва 24': logobase + 'dZcmoqRoZLhCBh8BE4RnbQivuDY6hH.png', u'Москва Доверие': logobase + '9oPazhJQrGZcSN64ZOS3WjLwGmQIZy.png', u'Моя планета': logobase + 'Qa41eifERrD77xQsmpRGbeTq95Ldlv.png', - u'Мужское кино': logobase + 'PUDb8m2JFLndsPvb56tdH0V4RW0kZc.png', + u'Моя Удмуртия': logobase + 'l3xs5mws4kXBWbsjeVzS7tp9zttrpp.png', + u'Мужское кино': logobase + 'y997iu65e65h4w5d3s4dy.png', u'Мужской': logobase + '6YbhuWNqPKQWWsUGbBnSbAbm7IGssX.png', u'Муз ТВ': logobase + 'gttVvZmkAklbl2i0Mqy1MCzSCn7WiY.png', + u'МузСоюз': logobase + 'E4a3fEpdSy2c8AnYEdR8ZIFgFe2LAP.png', u'Музыка Первого': logobase + 'fD2Hnsq5BPMGvobLDMPZP049yNhBYt.png', u'Мульт': logobase + 'ZVzHvGF8mZ6RTsSh6aWsPbF1FBLjyp.png', u'Мультимания': logobase + '132.png', - u'НЛО ТВ': logobase + '2VGhYruaQo19G1NLGoOiTrwmPxef7d.png', - u'НСТ': logobase + 'fKYzdlWRz68qd9mRZnWuxMY73EyaSz.png', - u'НТВ (+4)': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', - u'НТВ (+7)': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', - u'НТВ HD': logobase + 'zdJ3ye6d3UWl5a56zm6LjqYH6ziSOs.png', - u'НТВ': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', - u'НТВ+ 3D': logobase + 'qvuG0JySlHPlEH9A7G4xMNjBqOB35h.png', - u'НТВ+ Баскетбол': logobase + 'bIWuyv7DJ65D5hIANkeo9SyIHGUXtn.png', - u'НТВ+ Кино Союз': logobase + 'F3RiiQtowA2YoHw73iEzcMLZwfDiSx.png', - u'НТВ+ Кино плюс': logobase + 'cnz8ZMypP2HV6phwv3rkSVQ7CgJExi.png', - u'НТВ+ Киноклуб': logobase + 'nRnksgRuhojvbFDqh5KZ30XJQ4iyFO.png', - u'НТВ+ Наше кино': logobase + 'UXJcZjVdZVIzciVHgGT3e6XxdRaBsD.png', - u'НТВ+ Премьера': logobase + 'lDiI54Y3LjIAOg5VV0adicP3OJrdgo.png', - u'НТВ+ Спорт плюс': logobase + '222.png', - u'НТВ+ Спорт': logobase + '2WCUNhvAYk7RJUCcbt4N8xvOxWGlbx.png', - u'НТВ+ Теннис': logobase + 'SdtlGA6I7WvjOpHsbabE4C9DP7JvJ7.png', - u'НТВ+ Футбол 1 HD': logobase + '5gVddUBrGBIvdTx0cpRgCMJwVgphJz.png', - u'НТВ+ Футбол 1': logobase + 'EQQJV8zgnv5MCfa5VBcvOm1GsLWovM.png', - u'НТВ+ Футбол 2 HD': logobase + '8X1dxETwOup3Qton0J35BoW5glu5UG.png', - u'НТВ+ Футбол 2': logobase + 'hD6OLNWbxyDtqE5VlVxCaoNeEoYpFb.png', - u'НТВ+ Футбол 3 HD': logobase + 'eC4IeAxFTXXMsVfQaQHPtAN7LvosGd.png', - u'НТВ+ Футбол 3': logobase + '4B2emgwWQ7kgFJwdoh0zNxDguh4Fh3.png', - u'НТН (Украина)': logobase + 'LpQE1Odb1EoH5dJ90gWjItVyEYBXsw.png', - u'НТРК "ИРТА"': logobase + 'd8zPTPLcK87xhBnGVFhgkuwFBY2TnK.png', + u'Надежда': logobase + 'fvCkzRJPsTx4HONWPv3vPMjQNloPBT.png', u'Нано ТВ': logobase + 'QuURIfJUmXegxsHMYqMivVwxizbfKd.png', + u'Настоящее время HD': logobase + 'Wh8dHDWVAik29wT1fdzdB9QClZ2PWv.png', + u'Наука': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Наука (Украина)': logobase + '73jfedkdp1PVWxdlddOvbwe5HqVEW6.png', u'Наука 2.0': logobase + 'ypWbqYqKApM8cnDK1FibvQgpmgEay9.png', + u'Наш детектив HD': logobase + 'ORgdUno4IvNQYiShiEY4srF16JguLs.png', + u'Наш кинороман HD': logobase + 'NyV9o2C4xIMFxxYUQsS1qsdAtkqz0k.png', + u'Наша Сибирь': logobase + 'zYJLNsu4daR3jomBUtHPWgeSFQo7Bq.png', + u'Наша Сибирь HD': logobase + 'VbmHqKo1RHl1fkZym386Pc5wTkNM70.png', u'Наше любимое кино': logobase + 'LSR5M6VxB0YDwv6803zrGFkq7vGQ3J.png', - u'Ника ТВ': logobase + 'pb3d3rBN4qW7ggzsosbAZXflfIv0Ty.png', - u'Новороссия ТВ': logobase + 'zUchDq13UVJRmlwAl3feV8cgKHYSyE.png', + u'Наше новое кино': logobase + 'y5jtghJCHG65ukyfjv 45stjxc76.png', + u'Наше ТВ': logobase + 'gTfxpSWtOGWBq7UHAHcz3XF10bzg78.png', + u'Неизвестная планета HD': logobase + 'Gvam2aif9yVwK5YjswZ43GQOwc9ZBJ.png', + u'Нова ТВ Болгария': logobase + 'WwgAjjB4yzi7zTG2O5Q0wUUz83O5Eh.png', + u'Новороссия ТВ': logobase + 'YXClYch7fOn1YzpwKjWqjMyPQa2RsV.png', + u'Новосибирские Новости': logobase + 'wN2tOSdrmMjKWUll48Oygdk3ROh9Aj.png', u'Новый канал': logobase + 'k7YdHhVpFZPIkBMXS2P2O2TkZSPf0y.png', + u'Новый мир': logobase + 'K0Oy2d445QmB0921HH1aB5JWUGScto.png', + u'Новый Христианский': logobase + 'gf6hTOcGXasvr47vTFRYZGV11xkDr5.png', u'Ностальгия': logobase + 'tIfiXoDaXoZevuGu9pZJSvX8unv1xl.png', u'Ночной клуб': logobase + 'nXifSdkxHJVKI4SKtgtBQmCSHXtgOt.png', - u'ОТР': logobase + 'CqxKorK72v3ULbWkB3ZOhdte0duYZa.png', - u'Оплот 2': logobase + 'EqwpuUgrI6Wl6JVDK2fLtXkNaqOXeU.png', - u'Оплот ТВ': logobase + 'gvofGxTug45qSt1vsX0BPzQxGTrwTr.png', + u'НСТ': logobase + 'fKYzdlWRz68qd9mRZnWuxMY73EyaSz.png', + u'НТВ': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', + u'НТВ (+2)': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', + u'НТВ (+4)': logobase + 'B5GA1cfgmn8EsxrdwfNUIrEbdqarXf.png', + u'НТВ HD': logobase + 'zdJ3ye6d3UWl5a56zm6LjqYH6ziSOs.png', + u'НТВ Мир Балтия': logobase + 'ykpU49Gl1akVkgiVSsCm5B4TjXvQ64.png', + u'НТВ Право': logobase + '7yNDqWT8KiQ2aa9kGYXSsnVFCPj5xx.png', + u'НТВ Сериал': logobase + 'T7amR78JUYW3EgFZp14hQdh7pjzWLJ.png', + u'НТВ Стиль': logobase + 'DjeH6JTGT2hH0Y1Vfwd9Cceg780nNl.png', + u'НТВ+ Инфоканал': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'НТК': logobase + 'ix3lD9BcsoKou0MIhUCGgKF9MypV1g.png', + u'НТН (Украина)': logobase + 'LpQE1Odb1EoH5dJ90gWjItVyEYBXsw.png', + u'О!': logobase + 'zHhfMQhsdu59e6puujg0qHt1675jYO.png', + u'О2ТВ': logobase + '7KCLIwxu4Rfly2aMpR8zWBGvpv1Py8.png', + u'О2ТВ HD': logobase + 'KEq35DyCm6eZhUR49IglOsSLvBdmDy.png', + u'ОНТ (Беларусь)': logobase + 'a84If8XdqSFa6nHpegdujt52vAGNJW.png', + u'Оплот': logobase + 'o6U3Rw8Ts9R3ctV5HTjGYEXxb6AJeS.png', u'Оружие': logobase + 'CyDUCmYXK8WS2kXCX5kiAOFejnlwoP.png', u'Остросюжетное HD': logobase + 'mxF7CZsqsDRMMK4pN8ekdccEgvEsZC.png', + u'ОТБ (Харьков)': logobase + 'JiLpVicaO0fGahxYY5HqzEGe6rbUdN.png', + u'ОТВ (Приморье)': logobase + 'W3FNGLfo7R7JpYI98Am5sGamz0bfaN.png', + u'ОТВ HD (Челябинск)': logobase + 'PJ6O6K8WcAZihwqeWkAyHnBY0aazCq.png', + u'Открытый HD': logobase + 'CdIvMNsa7gAihJnpAKX8QnMdpvhAXc.png', + u'Открытый мир': logobase + 'vzFPGE5EMEh8ilV1WIGYcCKwYVVTSZ.png', + u'ОТР': logobase + 'CqxKorK72v3ULbWkB3ZOhdte0duYZa.png', + u'Охота и рыбалка': logobase + '5l2P20J6ebTh0ptOr27Hh704niP3nU.png', u'Охотник и рыболов': logobase + 'Ws2ddPI0b5Ie7PymoPUsboVlz9lYMS.png', - u'Парк развлечений': logobase + 'beyfqyeacrFG0PrOeKUQhzQ4bV6Q5d.png', - u'Первый автомобильный (Украина)': logobase + 'oZTXrmNOxeJIVSbnuxqbiuAL3voXYa.png', - u'Первый городской (Киров)': logobase + 'sxUNuJVQpUjRMmASa5TvwlGykSBAkY.png', - u'Первый городской Одесса': logobase + 'vBOI3YTA4FDLD0c7BHHjq476p9GMCZ.png', + u'Охотник и рыболов HD': logobase + 'O0wexOqiQXKMwr2coDMKWvJEb4zLQ1.png', + u'Первый Городской (Киров)': logobase + 'DoKQvt9mUUYEwhXbO4M6PgGOAhok1X.png', u'Первый деловой': logobase + 'a1Qf3MpxC9FPD68Tj8vtUTNK8P25xr.png', - u'Первый канал (+4)': logobase + 'nHJycH0CkOhPeZ9DmB47iSMWP5HyWz.png', - u'Первый канал (+6)': logobase + 'xEhi4YWxLlIcHq33Y44NrYvyHRArwa.png', - u'Первый канал (Европа)': logobase + 'WimZD6efLd6QotrPP9uiJeF7t50nFv.png', - u'Первый канал (СНГ)': logobase + 'WimZD6efLd6QotrPP9uiJeF7t50nFv.png', - u'Первый канал HD': logobase + 'VxAFWzh1y88c8Aqa17TsxD2IO5pqoi.png', u'Первый канал': logobase + 'WimZD6efLd6QotrPP9uiJeF7t50nFv.png', + u'Первый канал (+2)': logobase + 'GGe1ttMtczS0mgzMCegYji57J3PIaJ.png', + u'Первый канал (+2) (16:9)': logobase + 'Vzz9xtVJcWtpfAh4mmz8AreS3FTlae.png', + u'Первый канал (+4)': logobase + 'nUznF8VjPtO3KwbjlU0KaSWhutRRaL.png', + u'Первый канал (+6)': logobase + '0FNeH92R3NfkxmqF5kGJegh7wFS8lP.png', + u'Первый канал (4:3)': logobase + 'WimZD6efLd6QotrPP9uiJeF7t50nFv.png', + u'Первый канал (Евразия)': logobase + 'oPB8TcSFAuJtk4hBWgAqC9yNjMpfsi.png', + u'Первый канал HD': logobase + 'VxAFWzh1y88c8Aqa17TsxD2IO5pqoi.png', + u'Первый канал HD (+4)': logobase + 'VxAFWzh1y88c8Aqa17TsxD2IO5pqoi.png', + u'Первый крымский HD': logobase + '83MCfIk9L2xgpTAs9UlPS3Ufsg0EVG.png', + u'Первый Метео': logobase + 'vdA7FKd1SCYhX4ovvzjQEHFdW3uA5X.png', u'Первый музыкальный HD': logobase + 'YxKl6Jqi6fmlUJjYGPnBhWhntKzI65.png', u'Первый музыкальный UHD': logobase + 'YxKl6Jqi6fmlUJjYGPnBhWhntKzI65.png', - u'Первый музыкальный Россия HD': logobase + 'h7EDhdGypKmtfEP98O052SLlXUCcXt.png', - u'Первый музыкальный Россия': logobase + 'MkX2WG1zhZ2KcYFdL0xWH1T4xkO7UW.png', u'Первый музыкальный канал': logobase + 'gYpYhzD3akuKSFpRmkh2p36pXnqHoW.png', - u'Первый национальный (Украина)': logobase + '8yGRnEG4pNYMLFDVekA2yeOAX1lGZ2.png', + u'Первый музыкальный Россия': logobase + 'MkX2WG1zhZ2KcYFdL0xWH1T4xkO7UW.png', + u'Первый музыкальный Россия HD': logobase + 'h7EDhdGypKmtfEP98O052SLlXUCcXt.png', u'Первый образовательный': logobase + '1kXxtStMuodaPU09H3rla3ry3QA2Wr.png', - u'Перец (+4)': logobase + '28.png', - u'Перец': logobase + '28.png', + u'Первый Республиканский телеканал': logobase + 'VyJcFlIDs6xMLIHocDdpb9WeE0JDII.png', + u'Первый Ярославский': logobase + '6FZ4QqwM18DLS3WE6XKeF0rJzpSAIH.png', + u'Пёс и Ко': logobase + 'Lj9LwC8sHiSvUM6XDraDuKQPWf1BKK.png', u'Пиксель ТВ': logobase + 'BdCXB7wPZMNvlWzB5xEFzmsYUXcfXW.png', + u'Планета HD': logobase + '1QjirpCLi3q9qPu1CTvEvCB0BfINeo.png', u'ПлюсПлюс': logobase + '6gVIy7RMokFO61iVawgwbthe5mhgqm.png', - u'Право ТВ': logobase + 'jqV4vr8830fm6lYlX9F7w3tRvcrRra.png', + u'Поехали': logobase + '0tjsemk0EPOCa1EKOlwuow2Z9jxXyf.png', + u'Правда тут': logobase + 'ejWH5qVf8XAWkc3HLQYTmEpYchtaIS.png', + u'Про все': logobase + 'S2i01pmEOFJxwJtJCVF7PYc6Ij4jBX.png', + u'Про жизнь': logobase + 'Robm1kzCcV401H7oo3Fyh18OFOwXRW.png', + u'Продвижение HD (Омск)': logobase + 'qmaGd81WFsVDntpeM8KcNU1mATTV4N.png', u'Просвещение': logobase + 'Fpx3Vqqk2VNcXl4YjsfO53XscWadvF.png', + u'Прямий': logobase + '38BRA5jO6LAsQ6rv1NC3FMJ6KALp8z.png', + u'Прямий HD': logobase + '38BRA5jO6LAsQ6rv1NC3FMJ6KALp8z.png', u'Психология 21': logobase + 'AyLAdiqcKu5X8ykdLf2bO9HsxMlJdO.png', + u'Пятница': logobase + '0fafj6PSIWdqtBdgwYTl9M06SDU2wA.png', u'Пятница (+2)': logobase + '0fafj6PSIWdqtBdgwYTl9M06SDU2wA.png', - u'Пятница (+4)': logobase + 'fF9FYWNiHFfuR1ZrkaboYHwi1O37TJ.png', u'Пятница (+7)': logobase + '0fafj6PSIWdqtBdgwYTl9M06SDU2wA.png', - u'Пятница': logobase + '0fafj6PSIWdqtBdgwYTl9M06SDU2wA.png', - u'Пятый канал (+4)': logobase + 'tUE3C0hSxn7AxGHhST36CWi6HgJbIi.png', u'Пятый канал': logobase + 'nIUDYY41OO4Xo0ntGpGv2rfpOR5ngt.png', + u'Пятый канал (+2)': logobase + 'rsEtkBk2ta1Wj1Y8uvxvSm4Vmbycir.png', + u'Пятый канал (+4)': logobase + 'jLZJgPYRXOQWt6vmNpisLyb0HC6wnT.png', + u'Пятый канал HD': logobase + 'SYEopR9w4oAuBjJKULqiDvAxtB36W9.png', + u'Рада (Украина)': logobase + 'hBFJBYNiqZUom0ooVtNEJKliZwfioO.png', + u'Радость моя': logobase + 'VRylZFYgFq7AL0FWcbf5JVOX3desn3.png', + u'Раз ТВ': logobase + 'CWaPSXdAL4Ejo1wNOqSWNPNVwbbhC7.png', u'РБК': logobase + 'JUMDXZxxB3UiVpMpU8t0aCpbVzxTmP.png', - u'РЕН ТВ (+2)': logobase + 'xwSFxBlid4YhPjZl8ibcIeTlzP0VVS.png', - u'РЕН ТВ (+4) (резерв)': logobase + '2Z3hcLqC0pQC9gLkuZSl5WkHfS5HYb.png', - u'РЕН ТВ (+4)': logobase + 'BE7n1y2cjisjflpQuMdC9P3c79rWb7.png', + u'РГВК Дагестан': logobase + 'qn17bOV3KKdSlA4iY9wM3QQGjSYv9f.png', u'РЕН ТВ': logobase + 'LJvkfB2kYaDzij1Y13Fy6syUCkP5Y6.png', - u'РИА Новости': logobase + 'DixgG6tVZzcVHO2LPQEx3QrtfoVah3.png', - u'Рада Украина': logobase + 'hBFJBYNiqZUom0ooVtNEJKliZwfioO.png', - u'Радость моя': logobase + 'VRylZFYgFq7AL0FWcbf5JVOX3desn3.png', - u'Ретро ТВ': logobase + 'axrNIB7372SHIRwqT0jBbfyvjSoZ7I.png', - u'Россия 1 (+4)': logobase + 'DL17FIS3R8m6eWTwFvdDYualmxvkGV.png', + u'РЕН ТВ (+2)': logobase + 'LJvkfB2kYaDzij1Y13Fy6syUCkP5Y6.png', + u'РЕН ТВ (+7)': logobase + 'Vs2UxW1Qw5caI5D1xU3J39tVm39SlJ.png', + u'РЕН ТВ (резерв)': logobase + 'KMIosCGWDkcYCFeiSrqOBFxXIDxSHu.png', + u'РЕН-ТВ ИСТОКИ - ОРЕЛ': logobase + 'G9eQwShtkUavBqrQnprfYLZEbTVCxH.png', + u'Ретро ТВ !': logobase + 'IQNV43xGiDarnJoh6G4klfCD3I4kls.png', + u'Родное кино': logobase + 'y7saqicb538iqo64ho46hh46.png', u'Россия 1': logobase + 'UUrfoqi6NQcc9gRLnCc8ODZJ2T3ShE.png', + u'Россия 1 (+2)': logobase + 'TmknFdEdaX9GxcJhqd856oKgv0o1lp.png', + u'Россия 1 (+4)': logobase + 'buzygzarNA0VXEEKSE2oMALmjSxwao.png', + u'Россия 1 (Мурманск)': logobase + 'iyNAFKplpz55eyI2QzZpOpoI4T6mSL.png', + u'Россия 1 (Чита)': logobase + '508Yuw2bkpv4J4x9LYXOPayVeBbbrq.png', u'Россия 24': logobase + 'LWfGV6eICPYL7psaBfw2dOgGrOtHFS.png', + u'Россия 24 (Мурманск)': logobase + 'CjtIvIg7sTZSlUKXuEVBxchSB8voQp.png', u'Россия HD': logobase + 'ghvqmVpPWqn9x6POAm9UJBvXFzTrqN.png', - u'Россия К (+4)': logobase + 'lzLdqpUZ8iHL9JEV7vQGG1gSlyswfB.png', u'Россия К': logobase + 'W9pWrec1BOJTmj8okrFeyM44wcpyd4.png', - u'Россия РТР (Украина)': logobase + '5o9OWeEw90hM5ouECuTLwj5QP8MwU3.png', + u'Россия РТР': logobase + '5o9OWeEw90hM5ouECuTLwj5QP8MwU3.png', + u'Рудана': logobase + 'sHD2d6haQuqacBMkqiHcrCYJwMed8J.png', + u'Русская комедия': logobase + '4Q3vGcJh5o0cgJB18scb90ogvL6OYV.png', u'Русская ночь': logobase + '9Sh9bJuj6js5AJsypAd6UvwnsIB25R.png', u'Русский бестселлер': logobase + 'b5JXaosgmcanh9EVJg52yBefvdLQF7.png', u'Русский детектив': logobase + '7I7VjbsFMIkZdoSbHFXiKEVZKNUbOM.png', u'Русский иллюзион': logobase + 'E9Imfr8aHN5midPVpNhJ3fo49FHbQE.png', u'Русский роман': logobase + '2smriIFxtj7Ojh4jyZq0K1XrT98XjS.png', + u'Русский роман HD': logobase + '2smriIFxtj7Ojh4jyZq0K1XrT98XjS.png', u'Русский экстрим': logobase + 'upndVpIdjY3vb5vrituof5UcKISNcQ.png', + u'Русский экстрим HD': logobase + 'upndVpIdjY3vb5vrituof5UcKISNcQ.png', u'Рыжий': logobase + 'wfBSy60qHaPSKPpTfrNv9Q167iHIPu.png', - u'СТБ': logobase + 'saZlIDrdaXWoiQa8sfZp2bEAeH0kXk.png', - u'СТРК HD': logobase + 'xOmVS1kQFIHeAwqtJbBfrbE75Quj2a.png', - u'СТС (+4)': logobase + 'Tl0KbPAKgErgJHEw9yo3VMoc35EWZt.png', - u'СТС Love': logobase + 'iciJHbEmJ1hHXAMhzC9cRWhmh9gH0L.png', - u'СТС': logobase + 'is620Pu6DreVLLnpHkpcXXZC9PI2Hi.png', + u'Рыжий (сурд.)': logobase + 'GVloiNkHEd8z66ZjQicsIOkjeInnru.png', u'Санкт-Петербург': logobase + 'sb81YtPOvlHidztMnC5tZPSKkb1uMI.png', + u'Саратов 24': logobase + '5MeGeK5GeEkVWdYKsfFVpcvUcbs39S.png', u'Сарафан ТВ': logobase + 'LsYzwEOUspoxkY2hrTSy9zKqvpWlY8.png', + u'Светлое ТВ': logobase + 'TI5r40h6Un0bHj8MIVkx6qMJRJTHqa.png', + u'Своё ТВ (Ставрополь)': logobase + 'sZC9lsS9y83orqXpovNOhcP7RdTmzQ.png', + u'Сказки Зайки': logobase + 'ate5hd5vMAzbydl6dVpG6CMyxnqKnE.png', + u'Слуцк ТВ': logobase + 'wty5VfAnYYhOUcThWeSgeaH62b3m5b.png', + u'Смайлик ТВ HD': logobase + 'l4eM2nWbsJxQNKgUP7D4XZ96ieTkb6.png', u'Совершенно секретно': logobase + 'BZJQEpa6Y4KL9tQjPHAIxbodw0KAyN.png', u'Сонце': logobase + 'TJXJVeoBFRMFrUgzpPW4dunJL6XSzn.png', u'Союз': logobase + 'YpsuBorUwulPHW3nI8O6nKETnEVB83.png', u'Спас ТВ': logobase + 'pAFeyS1iCV4BybnpnnwjoKm0y0zvaA.png', - u'Спорт 1 (Украина)': logobase + 'XqwvMS8Hn0mOpbh79esrIqELTsvo5b.png', - u'Спорт 1 HD': logobase + 'cqsjEb2YlMBsTNJPvKmkxmAWLkfNmp.png', - u'Спорт 1': logobase + 'UNqCK5IiCxetsHsN7eMApPOzhaM9v0.png', - u'Спорт 2 (Украина)': logobase + 'q0PokCXx6jtCHEPMvE42I0pD3ZNY0o.png', - u'Спорт': logobase + 'InpgSRB4SoFzJSAOkLpGCMayuYTtVm.png', + u'Спорт 2 (Украина)': logobase + 'caui2OjRMPFWAFtfDnfCG4P1qt7uQj.png', + u'СТБ': logobase + 'saZlIDrdaXWoiQa8sfZp2bEAeH0kXk.png', + u'СТВ (Беларусь)': logobase + 'XX9KxgBXMkvJkP1PeUshGI1pe43Huc.png', u'Страна': logobase + '5G27bahViND43dD1VlkaKlQRsYOqwL.png', + u'Страна FM ТВ': logobase + 'ysrRW9deFkccNGlhtT0Sww5Yt8IpY1.png', + u'СТРК HD': logobase + 'xOmVS1kQFIHeAwqtJbBfrbE75Quj2a.png', + u'СТРК HD Сочи': logobase + '2PphESGueDUS1T6wSOr12iTgTbIVJm.png', + u'СТС': logobase + 'is620Pu6DreVLLnpHkpcXXZC9PI2Hi.png', + u'СТС (+4)': logobase + '9kPWMgG96ZZcBeleMogGOHKZcsOARf.png', + u'СТС (+7)': logobase + 'qxxLwx53wKNngJTCtT126zgZT5Wi1x.png', + u'СТС (резерв)': logobase + 'is620Pu6DreVLLnpHkpcXXZC9PI2Hi.png', + u'СТС Love': logobase + 'iciJHbEmJ1hHXAMhzC9cRWhmh9gH0L.png', + u'СТС+ТВ21 (Мурманск)': logobase + 'qXdXqfxDK23uC5srFGAfOlzmaUDYe1.png', + u'Тагил ТВ': logobase + 'ubegTi3hkdx7xtZCdOz8K0gBQLlght.png', u'ТБН': logobase + 'r9O7HmwQbFR4oKMH9yKAogE8xBzwz4.png', - u'ТВ 3 (+2)': logobase + '427.png', u'ТВ 3': logobase + '427.png', - u'ТВ2-ТВ': logobase + 'fFl6h6QUwFKoI3N4lqEyhNLdACGkNq.png', - u'ТВЦ (+4)': logobase + 'dR7hMBOIq0MDGMkydFuksHGLNIWz7U.png', + u'ТВ 3 (+2)': logobase + '427.png', + u'ТВ 3 (+3)': logobase + '427.png', + u'ТВ 3 (+7)': logobase + 'HfEo6PctsuTnlPQIa0xVKgEDqFwI48.png', + u'ТВ FM': logobase + 'A3YxOjZfAw2KJ3naZrCznXfdlotAlw.png', + u'ТВ Центр (+7)': logobase + 'diq0UxVTgqVSQn8KpcYCqe1Tk9q9Wh.png', + u'ТВ Центр (Москва)': logobase + 'F4YXd72KBNgv5iZVXrdPA28uGwISso.png', + u'ТВ Центр Международный': logobase + 'QEpQTskZ9hcfI0rgD8osHVYSv58pde.png', + u'ТВ21+': logobase + 'jir42UhUeeHKMrpUMCfnGdECmP4E5f.png', + u'Твой Дом': logobase + 'TJCGg7LuuUfVsyv5IFjMPLAGZmlNNL.png', + u'Твой Пушкинский': logobase + 'bGue6qSxFjWrP6DjWxeHj8fBBhB161.png', u'ТВЦ': logobase + 'QEpQTskZ9hcfI0rgD8osHVYSv58pde.png', u'ТДК': logobase + 'eSrHE6Gws4U6JxhFXA3mQ4iDVc0SwS.png', + u'Театр': logobase + '8qawrcOtzHZTAa3kI0rcDfVFEZoI5u.png', + u'Теледом HD': logobase + 'XviuCfRo0T4WFTOhFaC978AwZ1a3Ge.png', + u'Телекафе': logobase + 'fYRFV5oY197jXcyModfWVs0AlrCOIs.png', + u'Теленовелла': logobase + 'VmrH6tFL11avii735fJeVryZtEaCQ3.png', + u'Телепутешествия': logobase + 'fz4bqwLySJAQkUN7l2EPKNqyvilfRD.png', + u'Терра': logobase + 'GfIwWExYktvecczAx1jL64Rk8xdyea.png', u'ТЕТ': logobase + 'jp0YxRwXOyMWgVfDAyQaXNwle90sV3.png', + u'Техно 24': logobase + 'JbUGHLuuZa3WQbjtbzUo0cDZkGnLRK.png', + u'Тиса-1': logobase + 'pPRRy0SjPl3JKXdcZrQrdXQz3NTKOH.png', + u'Тлум HD': logobase + 'FhxsethirEr5lhrE54VGA4acHu7MJn.png', u'ТНВ-Планета': logobase + 'vBMv8AtIpDhBGQLjPoxkko3baWMFac.png', u'ТНВ-Татарстан': logobase + 'kMdYm3qFLgK52EV0ymvRBB43peSrj9.png', - u'ТНТ (+4)': logobase + 'eU71rcMDW3T6Ra5K7Ahh16wGf1gPvr.png', - u'ТНТ (+7)': logobase + 'Vtt1KKIpLY4LTQGnV03sdBYyX3hyWR.png', - u'ТНТ Comedy': logobase + 'IihdBuOBtjIeeUli9g5jR0196S9Ryk.png', u'ТНТ': logobase + 'Vtt1KKIpLY4LTQGnV03sdBYyX3hyWR.png', + u'ТНТ (+2)': logobase + 'Vtt1KKIpLY4LTQGnV03sdBYyX3hyWR.png', + u'ТНТ (+4)': logobase + 'Vtt1KKIpLY4LTQGnV03sdBYyX3hyWR.png', + u'ТНТ (+7)': logobase + 'lBHVGkAE8EVjDXLQGl52H6HTLavzBR.png', + u'ТНТ (резерв)': logobase + 'yEDrU5cgZbdUgfq8kzb40581xLcXNy.png', + u'ТНТ HD': logobase + 'iD2Gi2nqvSSZnrtPeaqAp6M3NpD50v.png', + u'ТНТ International (Беларусь)': logobase + 'd4loXdWqOPiwF7thzyBXX8JSspfVjU.png', + u'ТНТ-Music': logobase + '6Go23tY9hpakfrvTEUH7Z7o5Y9hpOG.png', + u'ТНТ4': logobase + 'yTclqOAW0EWwhw9vt0spVSUcS70ZR0.png', + u'ТНТ4 (+2)': logobase + 'yTclqOAW0EWwhw9vt0spVSUcS70ZR0.png', + u'Томское время': logobase + '1Z7yLtfyuITQhXchO59gwyQ3mES7qS.png', + u'Тонус ТВ': logobase + 'bE8WfReOerYTIbqPOo6VD2ajrFdOBT.png', + u'Точка ТВ': logobase + 'JWwPbPnkWooIpKd5WYsdpfO3Mh14oA.png', + u'Третий Цифровой': logobase + 'MXcua7OlJ9CplpD15hD84Xn2QjoCdt.png', + u'Три Ангела': logobase + 'EphQD4X09CGs9ukmPVM5FpmQBSyvch.png', + u'ТРК 555': logobase + 'z3GvW4WdjwSmsQrfhAIZewEsdH0rsi.png', u'ТРК Киев': logobase + 'qW0p5z3De7COmSxTmvJ4ZA2wOuSJjg.png', u'ТРК Украина': logobase + '0co3dwhFDhoCVeTbfMV8ASYFYxSrWM.png', + u'ТРК Черновцы': logobase + 'ENrdPt7hypBuec07uRWhxecOTgspkN.png', u'ТРО Союза': logobase + 'xAXy9iMyJ4wa2wmugJvbZuDIzc9pVz.png', - u'Теледом HD': logobase + 'XviuCfRo0T4WFTOhFaC978AwZ1a3Ge.png', - u'Телекафе': logobase + 'fYRFV5oY197jXcyModfWVs0AlrCOIs.png', - u'Телепутешествия': logobase + 'fz4bqwLySJAQkUN7l2EPKNqyvilfRD.png', - u'Техно 24': logobase + 'JbUGHLuuZa3WQbjtbzUo0cDZkGnLRK.png', - u'Тонус ТВ': logobase + 'bE8WfReOerYTIbqPOo6VD2ajrFdOBT.png', - u'Трофей': logobase + 'pOZb3d5BA6OkYL7qpTS4AUiihdLgZ5.png', - u'Улыбка ребенка': logobase + 'P8aPFN50uJWJHkrqFGb7wgzfaTHUOO.png', + u'Трофей': logobase + 'tQTWwjNBC8aLLWiWZfCj43BhWNH51I.png', + u'ТТС': logobase + 'crsSIipA6N288sjn4EvUyOyTd0ed9A.png', + u'Улыбка ребенка HD': logobase + 'P8aPFN50uJWJHkrqFGb7wgzfaTHUOO.png', u'Унiан': logobase + 'fhpFrTDoI9xx7UlK65KAjAbdTGehLL.png', - u'Усадьба': logobase + '5yIxLQzQyZnH5EJcwpSGb28QuRTSFH.png', + u'УрФО 24 HD': logobase + 'dO2b0PDb8ubcYjL4ZxLyZTQ7xjPn0q.png', + u'Усадьба ТВ': logobase + '3PdlNOdFo973qiGewntvIRanZF6MgP.png', u'Успех': logobase + 'RLcfsouYRxTNrQT97AOPIYfSneJyB6.png', + u'Фауна (Украина)': logobase + 'tuGtX9QIXM8h6O6RQoL3S0Gm7a23Zg.png', u'Феникс+ Кино': logobase + 'idiNkkBsxLwxWCF2VZrc9LQEevKh0d.png', + u'Футбол': logobase + '472.png', u'Футбол 1 (Украина)': logobase + 'AMKtYwcgSAX5mTcPdhQDe4he18Jz7S.png', - u'Футбол 1 HD (Украина)': logobase + 'hZaWPKLVxTqUWZk0LTmLi1K1WUzX85.png', u'Футбол 2 (Украина)': logobase + 'PUXTI9mKcs49JnEENkh95KoKqt9VNg.png', - u'Футбол 2 HD (Украина)': logobase + 'TTvGrBoRM07MHY4q6bSwfzVuDKEGTi.png', - u'Футбол': logobase + '472.png', + u'Хабар': logobase + 'fb3YNahVC0npAJuKKEEU0x7xkGiuRT.png', + u'Хабар 24': logobase + '5ucUtr617J0k3od3cVRn1mMy0Ezi1x.png', + u'Хорошее кино': logobase + '9kWkXFAAlhXRtSpF3VJgVD0eMsqRZQ.png', + u'Царьград ТВ': logobase + '9DJiua5LxhwBe2Is3l4LDJIH8zf5EE.png', + u'Царьград ТВ HD': logobase + 'WTBbf0ZqL5Ju4AzOLum6NPC5ZGE8Tm.png', + u'Центральный канал': logobase + 'vzfLS14qVT0rSphoNeEuO2WDvXFoub.png', u'ЧГТРК Грозный': logobase + 'LqDNdQj6nf4MraZztT6ZnACn7yOJpV.png', + u'Че': logobase + 'Hv36ZG48lg1mm2wdxAo3ju1EFS41Ga.png', + u'Че (+2)': logobase + 'HYWZATW7uW3ajRR7ULEZxcVteqHIqt.png', + u'Че (+7)': logobase + 'fV6EjhiQaVyMMlk1era5UxYZOKX7Rf.png', + u'Черновицкий Проминь HD': logobase + 'Us5Sr5jEx6SA6eBdZA9xchHzj1hJ4t.png', + u'Черноморская телекомпания': logobase + 'LJVoPVcJAE0s4DNmXCga0UPZkRkLTq.png', + u'ЧП-Инфо': logobase + 'Xy7mLl3exaBKuLlDGdrRls6hyR7mSw.png', u'Шансон ТВ': logobase + 'VY0TyCCkKOj5b8BhBJjT020sQoxL9F.png', u'Эгоист ТВ': logobase + 'moG8uExVh4nw3MN7dmGFdysJHBWLk6.png', - u'Ю (+2)': logobase + 'YvnG7hXCwMmHnakp2KkCbqeCigHcuK.png', - u'Ю (+7)': logobase + 'lS7OcLo9fsdDFdDFDvdlM3OE3Uu8Tj.png', + u'Эко-ТВ': logobase + 'EmsE2NuqzHi5NXh6OIMMXQpfmD0VIl.png', + u'Эфир 24 (Татарстан 24)': logobase + '8H6PLKiehQgY46uEPLIzebfSTSnwIx.png', u'Ю': logobase + 'YvnG7hXCwMmHnakp2KkCbqeCigHcuK.png', + u'Ю (+2)': logobase + 'YvnG7hXCwMmHnakp2KkCbqeCigHcuK.png', + u'Ювелирочка': logobase + 'pvKHFUCv25R51hJoUpAJfvjGnWyqoZ.png', u'Юмор ТВ': logobase + '6VFA1SVxeFHUsGaKPbNxWZREDkGeZw.png', + u'Юнион': logobase + '0TgIAZbV3nAOO4OxrGGN94atKhnBS2.png', u'Ямал Регион': logobase + 'xapccCaMjlT6JEAkZmk27wzCXlEU2m.png' } \ No newline at end of file diff --git a/plugins/helloworld_plugin_.py b/plugins/helloworld_plugin_.py index 7d20aa5..00cc3d2 100644 --- a/plugins/helloworld_plugin_.py +++ b/plugins/helloworld_plugin_.py @@ -13,8 +13,12 @@ class Helloworld(AceProxyPlugin): def __init__(self, AceConfig, AceStuff): pass - def handle(self, connection): + def handle(self, connection, headers_only=False): connection.send_response(200) connection.end_headers() + + if headers_only: + return + connection.wfile.write( '

Hello world!

') diff --git a/plugins/modules/PlaylistGenerator.py b/plugins/modules/PlaylistGenerator.py old mode 100644 new mode 100755 index 8be7dd5..6bba708 --- a/plugins/modules/PlaylistGenerator.py +++ b/plugins/modules/PlaylistGenerator.py @@ -5,17 +5,22 @@ ''' import re import urllib2 +from plugins.config.playlist import PlaylistConfig as config class PlaylistGenerator(object): - m3uheader = \ - '#EXTM3U url-tvg="http://www.teleguide.info/download/new3/jtv.zip"\n' - m3uemptyheader = '#EXTM3U\n' - m3uchanneltemplate = \ - '#EXTINF:-1 group-title="%s" tvg-name="%s" tvg-id="%s" tvg-logo="%s",%s\n%s\n' - - def __init__(self): + def __init__(self, + m3uemptyheader=config.m3uemptyheader, + m3uheader=config.m3uheader, + m3uchanneltemplate=config.m3uchanneltemplate, + changeItem=config.changeItem, + comparator=config.compareItems if config.sort else None): self.itemlist = list() + self.m3uemptyheader = m3uemptyheader + self.m3uheader = m3uheader + self.m3uchanneltemplate = m3uchanneltemplate + self.changeItem = changeItem + self.comparator = comparator def addItem(self, itemdict): ''' @@ -30,50 +35,76 @@ def addItem(self, itemdict): ''' self.itemlist.append(itemdict) - @staticmethod - def _generatem3uline(item): + def _generatem3uline(self, item): ''' Generates EXTINF line with url ''' - return PlaylistGenerator.m3uchanneltemplate % ( - item.get('group', ''), item.get('tvg', ''), item.get('tvgid', ''), - item.get('logo', ''), item.get('name'), item.get('url')) + return self.m3uchanneltemplate % item + + def _changeItems(self): + for item in self.itemlist: + self.changeItem(item) + if not item.has_key('tvg'): + item['tvg'] = item.get('name').replace(' ', '_') + if not item.has_key('tvgid'): + item['tvgid'] = '' + if not item.has_key('group'): + item['group'] = '' + if not item.has_key('logo'): + item['logo'] = '' - def exportm3u(self, hostport, add_ts=False, empty_header=False, archive=False, header=None): + def exportm3u(self, hostport, path='', add_ts=False, empty_header=False, archive=False, process_url=True, header=None, fmt=None): ''' Exports m3u playlist ''' + if add_ts: + # Adding ts:// after http:// for some players + hostport = 'ts://' + hostport + if header is None: if not empty_header: - itemlist = PlaylistGenerator.m3uheader + itemlist = self.m3uheader else: - itemlist = PlaylistGenerator.m3uemptyheader - if add_ts: - # Adding ts:// after http:// for some players - hostport = 'ts://' + hostport + itemlist = self.m3uemptyheader else: itemlist = header + + self._changeItems() + if self.comparator: + items = sorted(self.itemlist, cmp=self.comparator) + else: + items=self.itemlist - for item in self.itemlist: - item['tvg'] = item.get('tvg', '') if item.get('tvg') else \ - item.get('name').replace(' ', '_') - # For .acelive and .torrent - item['url'] = re.sub('^(http.+)$', lambda match: 'http://' + hostport + '/torrent/' + \ - urllib2.quote(match.group(0), '') + '/stream.mp4', item['url'], - flags=re.MULTILINE) - # For PIDs - item['url'] = re.sub('^(acestream://)?(?P[0-9a-f]{40})$', 'http://' + hostport + '/pid/\\g/stream.mp4', - item['url'], flags=re.MULTILINE) + for i in items: + item = i.copy() + item['name'] = item['name'].replace('"', "'").replace(',', '.') + url = item['url']; - # For channel id's - if archive: - item['url'] = re.sub('^([0-9]+)$', lambda match: 'http://' + hostport + '/archive/play?id=' + match.group(0), - item['url'], flags=re.MULTILINE) - else: - item['url'] = re.sub('^([0-9]+)$', lambda match: 'http://' + hostport + '/channels/play?id=' + match.group(0), - item['url'], flags=re.MULTILINE) + if process_url: + # For .acelive and .torrent + item['url'] = re.sub('^(http.+)$', lambda match: 'http://' + hostport + path + '/torrent/' + \ + urllib2.quote(match.group(0), '') + '/stream.mp4', url, + flags=re.MULTILINE) + if url == item['url']: # For PIDs + item['url'] = re.sub('^(acestream://)?(?P[0-9a-f]{40})$', 'http://' + hostport + path + '/pid/\\g/stream.mp4', + url, flags=re.MULTILINE) + if archive and url == item['url']: # For archive channel id's + item['url'] = re.sub('^([0-9]+)$', lambda match: 'http://' + hostport + path + '/archive/play?id=' + match.group(0), + url, flags=re.MULTILINE) + if not archive and url == item['url']: # For channel id's + item['url'] = re.sub('^([0-9]+)$', lambda match: 'http://' + hostport + path + '/channels/play?id=' + match.group(0), + url, flags=re.MULTILINE) + if url == item['url']: # For channel names + item['url'] = re.sub('^([^/]+)$', lambda match: 'http://' + hostport + path + '/' + match.group(0), + url, flags=re.MULTILINE) + + if fmt: + if '?' in item['url']: + item['url'] = item['url'] + '&fmt=' + fmt + else: + item['url'] = item['url'] + '/?fmt=' + fmt - itemlist += PlaylistGenerator._generatem3uline(item) + itemlist += self._generatem3uline(item) return itemlist diff --git a/plugins/modules/PluginInterface.py b/plugins/modules/PluginInterface.py index 378e488..ad65a4c 100644 --- a/plugins/modules/PluginInterface.py +++ b/plugins/modules/PluginInterface.py @@ -15,5 +15,5 @@ class AceProxyPlugin(object): def __init__(self, AceConfig, AceStuff): pass - def handle(self, connection): + def handle(self, connection, headers_only=False): raise NotImplementedError diff --git a/plugins/p2pproxy_plugin.py b/plugins/p2pproxy_plugin.py old mode 100644 new mode 100755 index bb4e509..b93e726 --- a/plugins/p2pproxy_plugin.py +++ b/plugins/p2pproxy_plugin.py @@ -22,7 +22,8 @@ import urllib2 import urlparse from torrenttv_api import TorrentTvApi -from datetime import date +from datetime import date, timedelta +import time from modules.PluginInterface import AceProxyPlugin from modules.PlaylistGenerator import PlaylistGenerator @@ -30,15 +31,17 @@ class P2pproxy(AceProxyPlugin): - handlers = ('channels', 'archive', 'xbmc.pvr') - + TTV = 'http://1ttv.org/' + TTVU = TTV + 'uploads/' + handlers = ('channels', 'channels.m3u', 'archive', 'xbmc.pvr', 'logos') logger = logging.getLogger('plugin_p2pproxy') def __init__(self, AceConfig, AceStuff): super(P2pproxy, self).__init__(AceConfig, AceStuff) self.params = None - - def handle(self, connection): + self.api = TorrentTvApi(config.email, config.password, config.sessiontimeout, config.zoneid) + + def handle(self, connection, headers_only=False): P2pproxy.logger.debug('Handling request') hostport = connection.headers['Host'] @@ -46,7 +49,7 @@ def handle(self, connection): query = urlparse.urlparse(connection.path).query self.params = urlparse.parse_qs(query) - if connection.reqtype == 'channels': # /channels/ branch + if connection.reqtype == 'channels' or connection.reqtype == 'channels.m3u': # /channels/ branch if len(connection.splittedpath) == 3 and connection.splittedpath[2].split('?')[ 0] == 'play': # /channels/play?id=[id] channel_id = self.get_param('id') @@ -59,22 +62,36 @@ def handle(self, connection): connection.send_header('Access-Control-Allow-Origin', '*') connection.send_header('Connection', 'close') connection.send_header('Content-Type', 'text/plain;charset=utf-8') - connection.send_header('Server', 'P2pProxy/1.0.3.1 AceProxy') + connection.send_header('Server', 'P2pProxy/1.0.4.4 AceProxy') connection.wfile.write('\r\n') return else: connection.dieWithError() # Bad request return + if headers_only: + connection.send_response(200) + connection.send_header("Content-Type", "video/mpeg") + connection.end_headers() + return + stream_url = None + stream_type, stream, translations_list = self.api.stream_source(channel_id) + name=logo='' - session = TorrentTvApi.auth(config.email, config.password) - stream_type, stream = TorrentTvApi.stream_source(session, channel_id) + for channel in translations_list: + if channel.getAttribute('id') == channel_id: + name = channel.getAttribute('name') + logo = channel.getAttribute('logo') + if config.fullpathlogo: + logo = P2pproxy.TTVU + logo + break if stream_type == 'torrent': - stream_url = re.sub('^(http.+)$', - lambda match: '/torrent/' + urllib2.quote(match.group(0), '') + '/stream.mp4', - stream) + stream_url = re.sub('^(http.+)$', + lambda match: '/torrent/' + urllib2.quote(match.group(0), '') + '/stream.mp4', + stream) + elif stream_type == 'contentid': stream_url = re.sub('^([0-9a-f]{40})', lambda match: '/pid/' + urllib2.quote(match.group(0), '') + '/stream.mp4', @@ -82,26 +99,35 @@ def handle(self, connection): connection.path = stream_url connection.splittedpath = stream_url.split('/') connection.reqtype = connection.splittedpath[1].lower() - connection.handleRequest(False) - elif self.get_param('type') == 'm3u': # /channels/?filter=[filter]&group=[group]&type=m3u - connection.send_response(200) - connection.send_header('Content-Type', 'application/x-mpegurl') - connection.end_headers() + connection.handleRequest(headers_only, name, logo, fmt=self.get_param('fmt')) + elif connection.reqtype == 'channels.m3u' or self.get_param('type') == 'm3u': # /channels/?filter=[filter]&group=[group]&type=m3u + if headers_only: + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.end_headers() + return - param_group = self.get_param('group') + param_group = self.params.get('group') param_filter = self.get_param('filter') if not param_filter: param_filter = 'all' # default filter + if param_group: + if 'all' in param_group: + param_group = None + else: + tmp = [] + for g in param_group: + tmp += g.split(',') + param_group = tmp - session = TorrentTvApi.auth(config.email, config.password) - translations_list = TorrentTvApi.translations(session, param_filter) + translations_list = self.api.translations(param_filter) playlistgen = PlaylistGenerator() P2pproxy.logger.debug('Generating requested m3u playlist') for channel in translations_list: group_id = channel.getAttribute('group') - if param_group and param_group != 'all' and param_group != group_id: # filter channels by group + if param_group and not group_id in param_group: # filter channels by group continue name = channel.getAttribute('name') @@ -109,63 +135,138 @@ def handle(self, connection): cid = channel.getAttribute('id') logo = channel.getAttribute('logo') if config.fullpathlogo: - logo = 'http://torrent-tv.ru/uploads/' + logo + logo = P2pproxy.TTVU + logo fields = {'name': name, 'id': cid, 'url': cid, 'group': group, 'logo': logo} - fields['tvgid'] = config.tvgid %fields + fields['tvgid'] = config.tvgid % fields playlistgen.addItem(fields) P2pproxy.logger.debug('Exporting') - header = '#EXTM3U url-tvg="%s" tvg-shift=%d\n' %(config.tvgurl, config.tvgshift) - exported = playlistgen.exportm3u(hostport=hostport, header=header) + header = '#EXTM3U url-tvg="%s" tvg-shift=%d\n' % (config.tvgurl, config.tvgshift) + exported = playlistgen.exportm3u(hostport=hostport, header=header, fmt=self.get_param('fmt')) exported = exported.encode('utf-8') + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.send_header('Content-Length', str(len(exported))) + connection.end_headers() connection.wfile.write(exported) else: # /channels/?filter=[filter] + if headers_only: + connection.send_response(200) + connection.send_header('Access-Control-Allow-Origin', '*') + connection.send_header('Connection', 'close') + connection.send_header('Content-Type', 'text/xml;charset=utf-8') + connection.end_headers() + return + param_filter = self.get_param('filter') if not param_filter: param_filter = 'all' # default filter - session = TorrentTvApi.auth(config.email, config.password) - translations_list = TorrentTvApi.translations(session, param_filter, True) + translations_list = self.api.translations(param_filter, True) P2pproxy.logger.debug('Exporting') - connection.send_response(200) connection.send_header('Access-Control-Allow-Origin', '*') connection.send_header('Connection', 'close') - connection.send_header('Content-Length', str(len(translations_list))) connection.send_header('Content-Type', 'text/xml;charset=utf-8') + connection.send_header('Content-Length', str(len(translations_list))) connection.end_headers() connection.wfile.write(translations_list) elif connection.reqtype == 'xbmc.pvr': # same as /channels request if len(connection.splittedpath) == 3 and connection.splittedpath[2] == 'playlist': - session = TorrentTvApi.auth(config.email, config.password) - translations_list = TorrentTvApi.translations(session, 'all', True) - - P2pproxy.logger.debug('Exporting') - connection.send_response(200) connection.send_header('Access-Control-Allow-Origin', '*') connection.send_header('Connection', 'close') - connection.send_header('Content-Length', str(len(translations_list))) connection.send_header('Content-Type', 'text/xml;charset=utf-8') + + if headers_only: + connection.end_headers() + return + + translations_list = self.api.translations('all', True) + connection.send_header('Content-Length', str(len(translations_list))) connection.end_headers() + P2pproxy.logger.debug('Exporting') connection.wfile.write(translations_list) elif connection.reqtype == 'archive': # /archive/ branch - if len(connection.splittedpath) == 3 and connection.splittedpath[2] == 'channels': # /archive/channels + if len(connection.splittedpath) >= 3 and (connection.splittedpath[2] == 'dates' or connection.splittedpath[2] == 'dates.m3u'): # /archive/dates.m3u + d = date.today() + delta = timedelta(days=1) + playlistgen = PlaylistGenerator() + hostport = connection.headers['Host'] + days = int(self.get_param('days')) if self.params.has_key('days') else 7 + suffix = '&suffix=' + self.get_param('suffix') if self.params.has_key('suffix') else '' + for i in range(days): + dfmt = d.strftime('%d-%m-%Y') + url = 'http://%s/archive/playlist/?date=%s%s' % (hostport, dfmt, suffix) + playlistgen.addItem({'group': '', 'tvg': '', 'name': dfmt, 'url': url}) + d = d - delta + exported = playlistgen.exportm3u(hostport, empty_header=True, process_url=False, fmt=self.get_param('fmt')).encode('utf-8') + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.send_header('Content-Length', str(len(exported))) + connection.end_headers() + connection.wfile.write(exported) + return + elif len(connection.splittedpath) >= 3 and (connection.splittedpath[2] == 'playlist' or connection.splittedpath[2] == 'playlist.m3u'): # /archive/playlist.m3u + dates = list() - session = TorrentTvApi.auth(config.email, config.password) - archive_channels = TorrentTvApi.archive_channels(session, True) + if self.params.has_key('date'): + for d in self.params['date']: + dates.append(self.parse_date(d).strftime('%d-%m-%Y').replace('-0', '-')) + else: + d = date.today() + delta = timedelta(days=1) + days = int(self.get_param('days')) if self.params.has_key('days') else 7 + for i in range(days): + dates.append(d.strftime('%d-%m-%Y').replace('-0', '-')) + d = d - delta - P2pproxy.logger.debug('Exporting') + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + if headers_only: + connection.end_headers() + return + + channels_list = self.api.archive_channels() + hostport = connection.headers['Host'] + playlistgen = PlaylistGenerator() + suffix = '&suffix=' + self.get_param('suffix') if self.params.has_key('suffix') else '' + + for channel in channels_list: + epg_id = channel.getAttribute('epg_id') + name = channel.getAttribute('name') + logo = channel.getAttribute('logo') + if logo != '' and config.fullpathlogo: + logo = P2pproxy.TTVU + logo + for d in dates: + n = name + ' (' + d + ')' if len(dates) > 1 else name + url = 'http://%s/archive/?type=m3u&date=%s&channel_id=%s%s' % (hostport, d, epg_id, suffix) + playlistgen.addItem({'group': name, 'tvg': '', 'name': n, 'url': url, 'logo': logo}) + + exported = playlistgen.exportm3u(hostport, empty_header=True, process_url=False, fmt=self.get_param('fmt')).encode('utf-8') + connection.send_header('Content-Length', str(len(exported))) + connection.end_headers() + connection.wfile.write(exported) + return + elif len(connection.splittedpath) == 3 and connection.splittedpath[2] == 'channels': # /archive/channels connection.send_response(200) connection.send_header('Access-Control-Allow-Origin', '*') connection.send_header('Connection', 'close') - connection.send_header('Content-Length', str(len(archive_channels))) connection.send_header('Content-Type', 'text/xml;charset=utf-8') - connection.end_headers() - connection.wfile.write(archive_channels) + + if headers_only: + connection.end_headers() + else: + archive_channels = self.api.archive_channels(True) + P2pproxy.logger.debug('Exporting') + connection.send_header('Content-Length', str(len(archive_channels))) + connection.end_headers() + connection.wfile.write(archive_channels) + + return if len(connection.splittedpath) == 3 and connection.splittedpath[2].split('?')[ 0] == 'play': # /archive/play?id=[record_id] record_id = self.get_param('id') @@ -173,15 +274,20 @@ def handle(self, connection): connection.dieWithError() # Bad request return + if headers_only: + connection.send_response(200) + connection.send_header("Content-Type", "video/mpeg") + connection.end_headers() + return + stream_url = None - session = TorrentTvApi.auth(config.email, config.password) - stream_type, stream = TorrentTvApi.archive_stream_source(session, record_id) + stream_type, stream = self.api.archive_stream_source(record_id) if stream_type == 'torrent': - stream_url = re.sub('^(http.+)$', - lambda match: '/torrent/' + urllib2.quote(match.group(0), '') + '/stream.mp4', - stream) + stream_url = re.sub('^(http.+)$', + lambda match: '/torrent/' + urllib2.quote(match.group(0), '') + '/stream.mp4', + stream) elif stream_type == 'contentid': stream_url = re.sub('^([0-9a-f]{40})', lambda match: '/pid/' + urllib2.quote(match.group(0), '') + '/stream.mp4', @@ -189,56 +295,72 @@ def handle(self, connection): connection.path = stream_url connection.splittedpath = stream_url.split('/') connection.reqtype = connection.splittedpath[1].lower() - connection.handleRequest(False) + connection.handleRequest(headers_only, fmt=self.get_param('fmt')) elif self.get_param('type') == 'm3u': # /archive/?type=m3u&date=[param_date]&channel_id=[param_channel] - connection.send_response(200) - connection.send_header('Content-Type', 'application/x-mpegurl') - connection.end_headers() + d = self.get_date_param() - param_date = self.get_param('date') - if not param_date: - d = date.today() # consider default date as today if not given - else: - try: - param_date = param_date.split('-') - d = date(param_date[2], param_date[1], param_date[0]) - except IndexError: - P2pproxy.logger.error('date param is not correct!') - connection.dieWithError() - return - param_channel = self.get_param('channel_id') - if param_channel == '' or not param_channel: - P2pproxy.logger.error('Got /archive/ request but no channel_id specified!') - connection.dieWithError() + if headers_only: + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.end_headers() return - session = TorrentTvApi.auth(config.email, config.password) - records_list = TorrentTvApi.records(session, param_channel, d.strftime('%d-%m-%Y')) - channels_list = TorrentTvApi.archive_channels(session) - playlistgen = PlaylistGenerator() - P2pproxy.logger.debug('Generating archive m3u playlist') - for record in records_list: - record_id = record.getAttribute('record_id') - name = record.getAttribute('name') - channel_id = record.getAttribute('channel_id') - channel_name = '' - logo = '' - for channel in channels_list: - if channel.getAttribute('channel_id') == channel_id: - channel_name = channel.getAttribute('name') - logo = channel.getAttribute('logo') + param_channel = self.get_param('channel_id') + d = d.strftime('%d-%m-%Y').replace('-0', '-') - if channel_name != '': - name = '(' + channel_name + ') ' + name - if logo != '' and config.fullpathlogo: - logo = 'http://torrent-tv.ru/uploads/' + logo + if param_channel == '' or not param_channel: + channels_list = self.api.archive_channels() - playlistgen.addItem({'name': name, 'url': record_id, 'logo': logo}) + for channel in channels_list: + channel_id = channel.getAttribute('epg_id') + try: + records_list = self.api.records(channel_id, d) + channel_name = channel.getAttribute('name') + logo = channel.getAttribute('logo') + if logo != '' and config.fullpathlogo: + logo = P2pproxy.TTVU + logo + + for record in records_list: + name = record.getAttribute('name') + record_id = record.getAttribute('record_id') + playlistgen.addItem({'group': channel_name, 'tvg': '', + 'name': name, 'url': record_id, 'logo': logo}) + except: + P2pproxy.logger.debug('Failed to load archive for ' + channel_id) + + else: + records_list = self.api.records(param_channel, d) + channels_list = self.api.archive_channels() + P2pproxy.logger.debug('Generating archive m3u playlist') + + for record in records_list: + record_id = record.getAttribute('record_id') + channel_id = record.getAttribute('epg_id') + name = record.getAttribute('name') + d = time.localtime(float(record.getAttribute('time'))) + n = '%.2d:%.2d %s' % (d.tm_hour, d.tm_min, name) + logo = '' + for channel in channels_list: + if channel.getAttribute('epg_id') == channel_id: + channel_name = channel.getAttribute('name') + logo = channel.getAttribute('logo') + + if channel_name != '': + name = '(' + channel_name + ') ' + name + if logo != '' and config.fullpathlogo: + logo = P2pproxy.TTVU + logo + + playlistgen.addItem({'group': channel_name, 'name': n, 'url': record_id, 'logo': logo, 'tvg': ''}) P2pproxy.logger.debug('Exporting') - exported = playlistgen.exportm3u(hostport, empty_header=True, archive=True) + exported = playlistgen.exportm3u(hostport, empty_header=True, archive=True, fmt=self.get_param('fmt')) exported = exported.encode('utf-8') + + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.send_header('Content-Length', str(len(exported))) + connection.end_headers() connection.wfile.write(exported) else: # /archive/?date=[param_date]&channel_id=[param_channel] param_date = self.get_param('date') @@ -247,7 +369,7 @@ def handle(self, connection): else: try: param_date = param_date.split('-') - d = date(param_date[2], param_date[1], param_date[0]) + d = date(int(param_date[2]), int(param_date[1]), int(param_date[0])) except IndexError: P2pproxy.logger.error('date param is not correct!') connection.dieWithError() @@ -258,21 +380,58 @@ def handle(self, connection): connection.dieWithError() return - session = TorrentTvApi.auth(config.email, config.password) - records_list = TorrentTvApi.records(session, param_channel, d.strftime('%d-%m-%Y'), True) - - P2pproxy.logger.debug('Exporting') connection.send_response(200) connection.send_header('Access-Control-Allow-Origin', '*') connection.send_header('Connection', 'close') - connection.send_header('Content-Length', str(len(records_list))) connection.send_header('Content-Type', 'text/xml;charset=utf-8') - connection.end_headers() - connection.wfile.write(records_list) + + if headers_only: + connection.end_headers() + else: + records_list = self.api.records(param_channel, d.strftime('%d-%m-%Y'), True) + P2pproxy.logger.debug('Exporting') + connection.send_header('Content-Length', str(len(records_list))) + connection.end_headers() + connection.wfile.write(records_list) + elif connection.reqtype == 'logos': # Used to generate logomap for the torrenttv plugin + translations_list = self.api.translations('all') + last = translations_list[-1] + connection.send_response(200) + connection.send_header('Content-Type', 'text/plain;charset=utf-8') + connection.end_headers() + connection.wfile.write("logobase = '" + P2pproxy.TTVU + "'\n") + connection.wfile.write("logomap = {\n") + + for channel in translations_list: + name = channel.getAttribute('name').encode('utf-8') + logo = channel.getAttribute('logo').encode('utf-8') + connection.wfile.write(" u'%s': logobase + '%s'" % (name, logo)) + if not channel == last: + connection.wfile.write(",\n") + else: + connection.wfile.write("\n") + + connection.wfile.write("}\n") def get_param(self, key): if key in self.params: return self.params[key][0] else: - return None \ No newline at end of file + return None + + def get_date_param(self): + d = self.get_param('date') + + if not d: + return date.today() + else: + return self.parse_date(d) + + def parse_date(self, d): + try: + param_date = d.split('-') + return date(int(param_date[2]), int(param_date[1]), int(param_date[0])) + except IndexError as e: + P2pproxy.logger.error('date param is not correct!') + raise e diff --git a/plugins/stat_plugin.py b/plugins/stat_plugin.py old mode 100644 new mode 100755 index f99b53e..76e59ea --- a/plugins/stat_plugin.py +++ b/plugins/stat_plugin.py @@ -1,27 +1,96 @@ -''' -Simple statistics plugin +''' +Simple statistics plugin + +To use it, go to http://127.0.0.1:8000/stat +''' +from modules.PluginInterface import AceProxyPlugin +from subprocess import Popen, PIPE +import re +import time +import logging +import urllib2 +import plugins.modules.ipaddr as ipaddr +import locale +import json + +localnetranges = ( + '192.168.0.0/16', + '10.0.0.0/8', + '172.16.0.0/12', + '224.0.0.0/4', + '240.0.0.0/5', + '127.0.0.0/8', + ) + +class Stat(AceProxyPlugin): + handlers = ('stat', 'favicon.icon') + logger = logging.getLogger('STAT') + + def __init__(self, AceConfig, AceStuff): + self.config = AceConfig + self.stuff = AceStuff + + def geo_ip_lookup(self, ip_address): + lookup_url = 'http://freegeoip.net/json/' + ip_address + Stat.logger.debug('Trying to obtain geoip info : ' + lookup_url) + + req = urllib2.Request(lookup_url, headers={'User-Agent' : "Magic Browser"}) + response = json.loads(urllib2.urlopen(req, timeout=10).read()) + + return {'country_code' : '' if not response['country_code'] else response['country_code'] , + 'country' : '' if not response['country_name'] else response['country_name'] , + 'city' : '' if not response['city'] else response['city']} -To use it, go to http://127.0.0.1:8000/stat -''' -from modules.PluginInterface import AceProxyPlugin + def mac_lookup(self,ip_address): + Popen(["ping", "-c 1", ip_address], stdout = PIPE, shell=False) + pid = Popen(["arp", "-n", ip_address], stdout = PIPE, shell=False) + s = pid.communicate()[0] + mac_address = re.search(r"(([a-f\d]{1,2}\:){5}[a-f\d]{1,2})", s) + if mac_address != None: + mac_address = mac_address.groups()[0] + lookup_url = "http://api.macvendors.com/" + mac_address + Stat.logger.debug('Trying to obtain MAC address : ' + lookup_url) + req = urllib2.Request(lookup_url, headers={'User-Agent' : "Magic Browser"}) + response = urllib2.urlopen(req, timeout=10).read() + else: + Stat.logger.debug("Can't obtain MAC address for Local IP") + response = '' + return "Local IP address " if not response else response + + def handle(self, connection, headers_only=False): + current_time = time.time() + + if connection.reqtype == 'favicon.ico': + connection.send_response(404) + return + connection.wfile.write('') + connection.wfile.write('') + connection.wfile.write('AceProxy stat info') + connection.wfile.write('') + connection.wfile.write('') + connection.wfile.write('') + connection.wfile.write('

Connected clients: ' + str(self.stuff.clientcounter.total) + '

') + connection.wfile.write('
Concurrent connections limit: ' + str(self.config.maxconns) + '
') + connection.wfile.write('') + for i in self.stuff.clientcounter.clients: + for c in self.stuff.clientcounter.clients[i]: + connection.wfile.write('') + clientinrange = any(map(lambda i: ipaddr.IPAddress(c.handler.clientip) in ipaddr.IPNetwork(i),localnetranges)) - def handle(self, connection): - connection.send_response(200) - connection.send_header('Content-type', 'text/html') - connection.end_headers() - connection.wfile.write( - '

Connected clients: ' + str(self.stuff.clientcounter.total) + '

') - connection.wfile.write( - '
Concurrent connections limit: ' + str(self.config.maxconns) + '
') - for i in self.stuff.clientcounter.clients: - connection.wfile.write(str(i) + ' : ' + str(self.stuff.clientcounter.clients[i][0]) + ' ' + - str(self.stuff.clientcounter.clients[i][1]) + '
') - connection.wfile.write('') + if clientinrange: + connection.wfile.write('') + else: + geo_ip_info = self.geo_ip_lookup(c.handler.clientip) + connection.wfile.write('') + connection.wfile.write('') + connection.wfile.write('') + connection.wfile.write('
Channel nameClient IPClient/LocationStart timeDuration
') + if c.channelIcon: + connection.wfile.write(' ') + if c.channelName: + connection.wfile.write(c.channelName.encode('UTF8')) + else: + connection.wfile.write(i) -class Stat(AceProxyPlugin): - handlers = ('stat', ) - - def __init__(self, AceConfig, AceStuff): - self.config = AceConfig - self.stuff = AceStuff + connection.wfile.write('' + c.handler.clientip + '' + self.mac_lookup(c.handler.clientip).encode('UTF8').strip() + '' + geo_ip_info.get('country').encode('UTF8') + ', ' + geo_ip_info.get('city').encode('UTF8') + '  ' + time.strftime('%c', time.localtime(c.connectionTime)) + '' + time.strftime("%H:%M:%S", time.gmtime(current_time-c.connectionTime)) + '
') diff --git a/plugins/torrentfilms_plugin.py b/plugins/torrentfilms_plugin.py new file mode 100644 index 0000000..70e3eb6 --- /dev/null +++ b/plugins/torrentfilms_plugin.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +''' +Torrent Films Playlist Plugin +http://ip:port/films +(C) Dorik1972 +''' +import os +import logging +import urllib2 +import base64 +import json +from modules.PluginInterface import AceProxyPlugin +import config.torrentfilms as config +from aceconfig import AceConfig + +class Torrentfilms(AceProxyPlugin): + + handlers = ('torrentfilms', 'films',) + + def __init__(self, AceConfig, AceStuff): + self.logger = logging.getLogger('plugin_TorrentFilms') + self.filelist = None + pass + + def createFilelist(self): + try: + self.logger.debug("Trying to load torrent files from "+config.directory) + if os.path.exists(config.directory): + self.filelist = filter(lambda x: x.endswith(('.torrent','.torrent.added')), os.listdir(config.directory)) + except: + self.logger.error("Can't load torrent files from "+config.directory) + return False + return True + + def getCid(self, filename): + cid = '' + try: + self.logger.debug('Get file name : '+filename) + with open(filename, "rb") as torrent_file: + f = base64.b64encode(torrent_file.read()) + req = urllib2.Request('http://api.torrentstream.net/upload/raw', f) + req.add_header('User-Agent', 'Python-urllib/2.7') + req.add_header('Content-Type', 'application/octet-stream') + cid = json.loads(urllib2.urlopen(req, timeout=10).read())['content_id'] + self.logger.debug("CID: " + cid) + except: + pass + + if cid == '': + logging.debug("Failed to get ContentID from WEB API") + + return None if not cid or cid == '' else cid + + + def handle(self, connection, headers_only=False): + + if not self.filelist: + if not self.createFilelist(): + connection.dieWithError() + return + + hostport = connection.headers['Host'] + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.end_headers() + + if headers_only: + return; + + connection.wfile.write('#EXTM3U deinterlace=1 m3uautoload=1 cache=1000\n') + + for i in range(len(self.filelist)): + self.filenames = config.directory+'/'+self.filelist[i] + content_id = self.getCid(self.filenames) + + if content_id!='': + req = urllib2.Request('http://'+AceConfig.acehost+':6878/server/api?method=get_media_files&content_id='+content_id) + try: + result = json.loads(urllib2.urlopen(req, timeout=10).read())['result'] + for key in result: + connection.wfile.write('#EXTINF:-1 group-title="TorrentFilms",'+ result[key].encode('UTF-8').translate(None, b"%~}{][^$@*,!?&`|><") +'\n') + connection.wfile.write('http://'+hostport.partition(':')[0]+':6878/ace/getstream?id='+content_id+'&_idx='+key+'\n') + except: + self.logger.debug("Can't load info form "+self.filelist[i]+" file !!") + pass + + self.filelist = None + self.logger.debug('Playlist created!') diff --git a/plugins/torrenttelik_plugin.py b/plugins/torrenttelik_plugin.py index 163040d..8f893cb 100644 --- a/plugins/torrenttelik_plugin.py +++ b/plugins/torrenttelik_plugin.py @@ -6,10 +6,12 @@ http://ip:port/torrent-telik/?type=mob_ttv = torrent-tv mobile playlist http://ip:port/torrent-telik/?type=allfon = allfon playlist ''' + import json import logging -import urllib2 import urlparse +import requests +import time from modules.PluginInterface import AceProxyPlugin from modules.PlaylistGenerator import PlaylistGenerator import config.torrenttelik as config @@ -19,21 +21,32 @@ class Torrenttelik(AceProxyPlugin): handlers = ('torrent-telik', ) - logger = logging.getLogger('plugin_torrenttelik') + logger = logging.getLogger('Plugin_Torrenttelik') playlist = None + playlisttime = None def downloadPlaylist(self, url): try: - req = urllib2.Request(url, headers={'User-Agent' : "Magic Browser"}) - Torrenttelik.playlist = urllib2.urlopen(req, timeout=10).read() - Torrenttelik.playlist = Torrenttelik.playlist.split('\xef\xbb\xbf')[1] # garbage at the beginning + Torrenttelik.logger.debug('Trying to download Torrent-telik playlist') + self.headers = {'User-Agent' : "Magic Browser", + 'Accept-Encoding': 'gzip'} + if config.useproxy: + r = requests.get(url, headers=self.headers, proxies=config.proxies, timeout=30) + else: + r = requests.get(url, headers=self.headers, timeout=10) + Torrenttelik.playlist = r.content + Torrenttelik.playlisttime = int(time.time()) + Torrenttelik.logger.debug('Torrent-telik playlist ' + r.url + ' downloaded !') + + Torrenttelik.playlist = Torrenttelik.playlist.split('\xef\xbb\xbf')[1] + Torrenttelik.playlist = Torrenttelik.playlist.replace(',\r\n]}', '\r\n]}') except: - Torrenttelik.logger.error("Can't download playlist!") + Torrenttelik.logger.error("Can't download Torrent-telik playlist!") return False return True - def handle(self, connection): + def handle(self, connection, headers_only=False): hostport = connection.headers['Host'] @@ -41,6 +54,9 @@ def handle(self, connection): connection.send_header('Content-Type', 'application/x-mpegurl') connection.end_headers() + if headers_only: + return + query = urlparse.urlparse(connection.path).query self.params = urlparse.parse_qs(query) @@ -53,9 +69,11 @@ def handle(self, connection): elif list_type.startswith('allfon'): url = config.url_allfon - if not self.downloadPlaylist(url): - connection.dieWithError() - return + # 30 minutes cache + if not Torrenttelik.playlist or (int(time.time()) - Torrenttelik.playlisttime > 30 * 60): + if not self.downloadPlaylist(url): + connection.dieWithError() + return # Un-JSON channel list try: @@ -80,9 +98,12 @@ def handle(self, connection): playlistgen = PlaylistGenerator() for channel in channels: + channel['group'] = channel.get('cat', '') playlistgen.addItem(channel) - exported = playlistgen.exportm3u(hostport, add_ts=add_ts) + Torrenttelik.logger.debug('Torrent-telik playlist created') + header = '#EXTM3U url-tvg="%s" tvg-shift=%d deinterlace=1 m3uautoload=1 cache=1000\n' %(config.tvgurl, config.tvgshift) + exported = playlistgen.exportm3u(hostport, header=header, add_ts=add_ts, fmt=self.getparam('fmt')) exported = exported.encode('utf-8') connection.wfile.write(exported) diff --git a/plugins/torrenttv_api.py b/plugins/torrenttv_api.py old mode 100644 new mode 100755 index ce5a86d..3fcb9e0 --- a/plugins/torrenttv_api.py +++ b/plugins/torrenttv_api.py @@ -7,8 +7,12 @@ import urllib2 import socket +import random import xml.dom.minidom as dom - +import json +import logging +import time +import threading class TorrentTvApiException(Exception): """ @@ -33,8 +37,20 @@ class TorrentTvApi(object): 12: 'Религиозные' } - @staticmethod - def auth(email, password, raw=False): + API_URL = 'http://1ttvapi.top/v3/' + + def __init__(self, email, password, maxIdle, zoneid='1'): + self.email = email + self.password = password + self.maxIdle = maxIdle + self.zoneid = zoneid + self.session = None + self.allTranslations = None + self.lastActive = 0.0 + self.lock = threading.RLock() + self.log = logging.getLogger("TTV API") + + def auth(self): """ User authentication Returns user session that can be used for API requests @@ -44,17 +60,27 @@ def auth(email, password, raw=False): :param raw: if True returns unprocessed data :return: unique session string """ + with self.lock: + if self.session and (time.time() - self.lastActive) < self.maxIdle: + self.lastActive = time.time() + self.log.debug("Reusing previous session: " + self.session) + return self.session - xmlresult = TorrentTvApi._result( - 'v2_auth.php?username=' + email + '&password=' + password + '&application=tsproxy') - if raw: - return xmlresult - res = TorrentTvApi._check(xmlresult) - session = res.getElementsByTagName('session')[0].firstChild.data - return session + self.log.debug("Creating new session") + self.session = None + req = TorrentTvApi.API_URL + 'auth.php?typeresult=json&username=' + self.email + '&password=' + self.password + '&application=tsproxy&guid=' + str(random.randint(100000000,199999999)) + result = self._jsoncheck(json.loads(urllib2.urlopen(req, timeout=10).read())) + self.session = result['session'] + self.lastActive = time.time() + self.log.debug("New session created: " + self.session) + + req = TorrentTvApi.API_URL + 'set_zone.php?session=' + self.session + '&zone=' + self.zoneid + result = self._jsoncheck(json.loads(urllib2.urlopen(req, timeout=10).read())) + self.log.debug("HTTP streaming ZoneID set to : " + self.zoneid) + + return self.session - @staticmethod - def translations(session, translation_type, raw=False): + def translations(self, translation_type, raw=False): """ Gets list of translations Translations are basically TV channels @@ -64,16 +90,22 @@ def translations(session, translation_type, raw=False): :param raw: if True returns unprocessed data :return: translations list """ - xmlresult = TorrentTvApi._result( - 'v2_alltranslation.php?session=' + session + '&type=' + translation_type) + + query = '&type=' + translation_type if raw: - return xmlresult - res = TorrentTvApi._check(xmlresult) - translationslist = res.getElementsByTagName('channel') - return translationslist + try: + res = self._xmlresult('translation_list.php', query) + self._checkxml(res) + return res + except TorrentTvApiException: + res = self._xmlresult('translation_list.php', query) + self._checkxml(res) + return res + else: + res = self._checkedxmlresult('translation_list.php', query) + return res.getElementsByTagName('channel') - @staticmethod - def records(session, channel_id, date, raw=False): + def records(self, channel_id, date, raw=False): """ Gets list of available record for given channel and date @@ -83,16 +115,22 @@ def records(session, channel_id, date, raw=False): :param raw: if True returns unprocessed data :return: records list """ - xmlresult = TorrentTvApi._result( - 'v2_arc_getrecords.php?session=' + session + '&channel_id=' + channel_id + '&date=' + date) + date = date.replace('-0', '-') + if raw: - return xmlresult - res = TorrentTvApi._check(xmlresult) - recordslist = res.getElementsByTagName('channel') - return recordslist + try: + res = self._xmlresult('arc_records.php', '&epg_id=' + channel_id + '&date=' + date) + self._checkxml(res) + return res + except TorrentTvApiException: + res = self._xmlresult('arc_records.php', '&epg_id=' + channel_id + '&date=' + date) + self._checkxml(res) + return res + else: + res = self._checkedxmlresult('arc_records.php', '&epg_id=' + channel_id + '&date=' + date) + return res.getElementsByTagName('channel') - @staticmethod - def archive_channels(session, raw=False): + def archive_channels(self, raw=False): """ Gets the channels list for archive @@ -100,32 +138,38 @@ def archive_channels(session, raw=False): :param raw: if True returns unprocessed data :return: archive channels list """ - xmlresult = TorrentTvApi._result( - 'v2_arc_getchannels.php?session=' + session) + if raw: - return xmlresult - res = TorrentTvApi._check(xmlresult) - archive_channelslist = res.getElementsByTagName('channel') - return archive_channelslist + try: + res = self._xmlresult('arc_list.php', '') + self._checkxml(res) + return res + except TorrentTvApiException: + res = self._xmlresult('arc_list.php', '') + self._checkxml(res) + return res + else: + res = self._checkedxmlresult('arc_list.php', '') + return res.getElementsByTagName('channel') - @staticmethod - def stream_source(session, channel_id): + def stream_source(self, channel_id): """ Gets the source for Ace Stream by channel id :param session: valid user session required :param channel_id: id of channel in translations list (see translations() method) - :return: type of stream and source + :return: type of stream and source and translation list """ - xmlresult = TorrentTvApi._result( - 'v2_get_stream.php?session=' + session + '&channel_id=' + channel_id) - res = TorrentTvApi._check(xmlresult) - stream_type = res.getElementsByTagName('type')[0].firstChild.data - source = res.getElementsByTagName('source')[0].firstChild.data - return stream_type.encode('utf-8'), source.encode('utf-8') - @staticmethod - def archive_stream_source(session, record_id): + res = self._checkedjsonresult('translation_stream.php', '&channel_id=' + channel_id) + stream_type = res['type'] + source = res['source'] + allTranslations = self.allTranslations + if not allTranslations: + self.allTranslations = allTranslations = self.translations('all') + return stream_type.encode('utf-8'), source.encode('utf-8'), allTranslations + + def archive_stream_source(self, record_id): """ Gets stream source for archive record @@ -133,15 +177,28 @@ def archive_stream_source(session, record_id): :param record_id: id of record in records list (see records() method) :return: type of stream and source """ - xmlresult = TorrentTvApi._result( - 'v2_arc_getstream.php?session=' + session + '&record_id=' + record_id) - res = TorrentTvApi._check(xmlresult) - stream_type = res.getElementsByTagName('type')[0].firstChild.data - source = res.getElementsByTagName('source')[0].firstChild.data + + res = self._checkedjsonresult('arc_stream.php', '&record_id=' + record_id) + stream_type = res['type'] + source = res['source'] return stream_type.encode('utf-8'), source.encode('utf-8') - @staticmethod - def _check(xmlresult): + def _jsoncheck(self, jsonresult): + """ + Validates received API answer + Raises an exception if error detected + + :param jsonresult: API answer to check + :return: minidom-parsed xmlresult + :raise: TorrentTvApiException + """ + success = jsonresult['success'] + if success == '0' or not success: + error = jsonresult['error'] + raise TorrentTvApiException('API returned error: ' + error) + return jsonresult + + def _checkxml(self, xmlresult): """ Validates received API answer Raises an exception if error detected @@ -157,8 +214,21 @@ def _check(xmlresult): raise TorrentTvApiException('API returned error: ' + error) return res - @staticmethod - def _result(request): + def _checkedjsonresult(self, request, params): + try: + return self._jsoncheck(self._jsonresult(request, params)) + except TorrentTvApiException: + self._resetSession() + return self._jsoncheck(self._jsonresult(request, params)) + + def _checkedxmlresult(self, request, params): + try: + return self._checkxml(self._xmlresult(request, params)) + except TorrentTvApiException: + self._resetSession() + return self._checkxml(self._xmlresult(request, params)) + + def _jsonresult(self, request, params): """ Sends request to API and returns the result in form of string @@ -167,7 +237,31 @@ def _result(request): :raise: TorrentTvApiException """ try: - result = urllib2.urlopen('http://api.torrent-tv.ru/' + request + '&typeresult=xml', timeout=10).read() + req = TorrentTvApi.API_URL + request + '?session=' + self.auth() + '&typeresult=json' + params + self.log.debug(req) + result = urllib2.urlopen(req, timeout=10).read() + return json.loads(result) + except (urllib2.URLError, socket.timeout) as e: + raise TorrentTvApiException('Error happened while trying to access API: ' + repr(e)) + + def _xmlresult(self, request, params): + """ + Sends request to API and returns the result in form of string + + :param request: API command string + :return: result of request to API + :raise: TorrentTvApiException + """ + try: + req = TorrentTvApi.API_URL + request + '?session=' + self.auth() + '&typeresult=xml' + params + self.log.debug(req) + result = urllib2.urlopen(req, timeout=10).read() return result except (urllib2.URLError, socket.timeout) as e: - raise TorrentTvApiException('Error happened while trying to access API: ' + repr(e)) \ No newline at end of file + raise TorrentTvApiException('Error happened while trying to access API: ' + repr(e)) + + def _resetSession(self): + with self.lock: + self.session = None + self.allTranslations = None + self.auth() diff --git a/plugins/torrenttv_plugin.py b/plugins/torrenttv_plugin.py index d8294db..13f8a31 100644 --- a/plugins/torrenttv_plugin.py +++ b/plugins/torrenttv_plugin.py @@ -4,79 +4,162 @@ ''' import re import logging -import urllib2 import time import gevent +import threading +import urlparse +import md5 +import traceback +import urllib2 +import requests from modules.PluginInterface import AceProxyPlugin from modules.PlaylistGenerator import PlaylistGenerator import config.torrenttv as config - +import config.p2pproxy as p2pconfig +from torrenttv_api import TorrentTvApi class Torrenttv(AceProxyPlugin): # ttvplaylist handler is obsolete - handlers = ('torrenttv', 'ttvplaylist') - - logger = logging.getLogger('plugin_torrenttv') - playlist = None - playlisttime = None + handlers = ('torrenttv', 'ttvplaylist',) def __init__(self, AceConfig, AceStuff): + self.logger = logging.getLogger('Plugin_TorrentTV') + self.lock = threading.Lock() + self.channels = None + self.playlist = None + self.playlisttime = None + + self.etag = None + self.logomap = config.logomap + self.updatelogos = p2pconfig.email != 're.place@me' and p2pconfig.password != 'ReplaceMe' + if config.updateevery: - self.downloadPlaylist() gevent.spawn(self.playlistTimedDownloader) def playlistTimedDownloader(self): while True: + with self.lock: + self.downloadPlaylist() gevent.sleep(config.updateevery * 60) - self.downloadPlaylist() def downloadPlaylist(self): try: - Torrenttv.logger.debug('Trying to download playlist') - req = urllib2.Request(config.url, headers={'User-Agent' : "Magic Browser"}) - Torrenttv.playlist = urllib2.urlopen( - req, timeout=10).read() - Torrenttv.playlisttime = int(time.time()) + self.logger.debug('Trying to download TTV playlist') + self.headers = {'User-Agent' : "Magic Browser", + 'Accept-Encoding': 'gzip'} + if config.useproxy: + r = requests.get(config.url, headers=self.headers, proxies=config.proxies, timeout=30) + else: + r = requests.get(config.url, headers=self.headers, timeout=10) + + origin = r.text.encode('UTF-8') + self.logger.debug('TTV playlist ' + r.url + ' downloaded !') + + matches = re.finditer(r',(?P\S.+) \((?P.+)\)[\r\n]+(?P[^\r\n]+)?', origin, re.MULTILINE) + self.playlisttime = int(time.time()) + self.playlist = PlaylistGenerator() + self.channels = dict() + m = md5.new() + + for match in matches: + itemdict = match.groupdict() + encname = itemdict.get('name') + name = encname.decode('UTF-8') + logo = config.logomap.get(name) + url = itemdict['url'] + if logo: + itemdict['logo'] = logo + + if (url.startswith('acestream://')) or (url.startswith('http://') and url.endswith('.acelive')): + self.channels[name] = url + itemdict['url'] = urllib2.quote(encname, '') + '.mp4' + self.playlist.addItem(itemdict) + m.update(encname) + + self.etag = '"' + m.hexdigest() + '"' except: - Torrenttv.logger.error("Can't download playlist!") + self.logger.error("Can't download TTV playlist!") + self.logger.error(traceback.format_exc()) return False + if self.updatelogos: + try: + api = TorrentTvApi(p2pconfig.email, p2pconfig.password, p2pconfig.sessiontimeout, p2pconfig.zoneid) + translations = api.translations('all') + logos = dict() + + for channel in translations: + name = channel.getAttribute('name').encode('utf-8') + logo = channel.getAttribute('logo').encode('utf-8') + logos[name] = config.logobase + logo + + self.logomap = logos + self.logger.debug("Logos updated") + self.updatelogos = False + except: + # p2pproxy plugin seems not configured + self.updatelogos = False + return True - def handle(self, connection): - # 30 minutes cache - if not Torrenttv.playlist or (int(time.time()) - Torrenttv.playlisttime > 30 * 60): - if not self.downloadPlaylist(): - connection.dieWithError() - return + def handle(self, connection, headers_only=False): + play = False - hostport = connection.headers['Host'] + with self.lock: - connection.send_response(200) - connection.send_header('Content-Type', 'application/x-mpegurl') - connection.end_headers() + # 30 minutes cache + if not self.playlist or (int(time.time()) - self.playlisttime > 30 * 60): + self.updatelogos = p2pconfig.email != 're.place@me' and p2pconfig.password != 'ReplaceMe' + if not self.downloadPlaylist(): + connection.dieWithError() + return - # Match playlist with regexp - matches = re.finditer(r',(?P\S.+) \((?P.+)\)\n(?P^.+$)', - Torrenttv.playlist, re.MULTILINE) - - add_ts = False - try: - if connection.splittedpath[2].lower() == 'ts': - add_ts = True - except: - pass - - - playlistgen = PlaylistGenerator() - for match in matches: - itemdict = match.groupdict() - name = itemdict.get('name').decode('UTF-8') - logo = config.logomap.get(name) - if logo is not None: - itemdict['logo'] = logo - playlistgen.addItem(itemdict) - - header = '#EXTM3U url-tvg="%s" tvg-shift=%d\n' %(config.tvgurl, config.tvgshift) - connection.wfile.write(playlistgen.exportm3u(hostport, add_ts=add_ts, header=header)) + url = urlparse.urlparse(connection.path) + path = url.path[0:-1] if url.path.endswith('/') else url.path + params = urlparse.parse_qs(url.query) + fmt = params['fmt'][0] if params.has_key('fmt') else None + + if path.startswith('/torrenttv/channel/'): + if not path.endswith('.mp4'): + connection.dieWithError(404, 'Invalid path: ' + path, logging.DEBUG) + return + + name = urllib2.unquote(path[19:-4]).decode('UTF8') + url = self.channels.get(name) + if not url: + connection.dieWithError(404, 'Unknown channel: ' + name, logging.DEBUG) + return + elif url.startswith('acestream://'): + connection.path = '/pid/' + url[12:] + '/stream.mp4' + connection.splittedpath = connection.path.split('/') + connection.reqtype = 'pid' + else: + connection.path = '/torrent/' + urllib2.quote(url, '') + '/stream.mp4' + connection.splittedpath = connection.path.split('/') + connection.reqtype = 'torrent' + play = True + elif self.etag == connection.headers.get('If-None-Match'): + self.logger.debug('ETag matches - returning 304') + connection.send_response(304) + connection.send_header('Connection', 'close') + connection.end_headers() + return + else: + hostport = connection.headers['Host'] + path = '' if len(self.channels) == 0 else '/torrenttv/channel' + add_ts = True if path.endswith('/ts') else False + header = '#EXTM3U url-tvg="%s" tvg-shift=%d deinterlace=1 m3uautoload=1 cache=1000\n' % (config.tvgurl, config.tvgshift) + exported = self.playlist.exportm3u(hostport, path, add_ts=add_ts, header=header, fmt=fmt) + + connection.send_response(200) + connection.send_header('Content-Type', 'application/x-mpegurl') + connection.send_header('ETag', self.etag) + connection.send_header('Content-Length', str(len(exported))) + connection.send_header('Connection', 'close') + connection.end_headers() + + if play: + connection.handleRequest(headers_only, name, config.logomap.get(name), fmt=fmt) + elif not headers_only: + connection.wfile.write(exported) diff --git a/vlcclient/vlcclient.py b/vlcclient/vlcclient.py index bf2be49..851a7b0 100644 --- a/vlcclient/vlcclient.py +++ b/vlcclient/vlcclient.py @@ -4,7 +4,7 @@ import gevent import gevent.event -import gevent.coros +import gevent.lock import telnetlib import logging from vlcmessages import * @@ -26,7 +26,7 @@ class VlcClient(object): def __init__( self, host='127.0.0.1', port=4212, password='admin', connect_timeout=5, - result_timeout=5, out_port=8081): + result_timeout=10, out_port=8081): # Receive buffer self._recvbuffer = None # Output port @@ -40,7 +40,7 @@ def __init__( # Authentication done event self._auth = gevent.event.AsyncResult() # Request lock - self._resultlock = gevent.coros.RLock() + self._resultlock = gevent.lock.RLock() # Request result self._result = gevent.event.AsyncResult() # VLC version string @@ -104,7 +104,7 @@ def _write(self, message): try: # Write message - self._socket.write(message + "\r\n") + self._socket.write(message.encode("UTF8") + "\r\n") except EOFError as e: raise VlcException("Vlc Write error! ERROR: " + repr(e))