Guest User

Untitled

a guest
Nov 8th, 2018
16,259
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 73.45 KB | None | 0 0
  1. #!/usr/bin/env python2
  2. # encoding: utf-8
  3.  
  4. """
  5. e2m3u2bouquet.e2m3u2bouquet -- Enigma2 IPTV m3u to bouquet parser
  6.  
  7. @author: Dave Sully, Doug Mackay
  8. @copyright: 2017 All rights reserved.
  9. @license: GNU GENERAL PUBLIC LICENSE version 3
  10. @deffield updated: Updated
  11. """
  12.  
  13. import sys
  14. import os
  15. import errno
  16. import re
  17. import unicodedata
  18. import datetime
  19. import urllib
  20. import urlparse
  21. import imghdr
  22. import tempfile
  23. import glob
  24. import ssl
  25. import hashlib
  26. import base64
  27. import socket
  28. from PIL import Image
  29. from collections import OrderedDict
  30. from collections import deque
  31. try:
  32. import xml.etree.cElementTree as ET
  33. except ImportError:
  34. import xml.etree.ElementTree as ET
  35. try:
  36. from enigma import eDVBDB
  37. except ImportError:
  38. eDVBDB = None
  39. from argparse import ArgumentParser
  40. from argparse import RawDescriptionHelpFormatter
  41.  
  42. __all__ = []
  43. __version__ = '0.7.7'
  44. __date__ = '2017-06-04'
  45. __updated__ = '2018-10-26'
  46.  
  47. DEBUG = 0
  48. TESTRUN = 0
  49.  
  50. ENIGMAPATH = '/etc/enigma2/'
  51. EPGIMPORTPATH = '/etc/epgimport/'
  52. CFGPATH = os.path.join(ENIGMAPATH, 'e2m3u2bouquet/')
  53. PICONSPATH = '/usr/share/enigma2/picon/'
  54. PROVIDERS = {}
  55. PROVIDERSURL = 'https://raw.githubusercontent.com/su1s/e2m3u2bouquet/master/providers.enc'
  56. IMPORTED = False
  57.  
  58. class CLIError(Exception):
  59. """Generic exception to raise and log different fatal errors."""
  60. def __init__(self, msg):
  61. super(CLIError).__init__(type(self))
  62. self.msg = "E: %s" % msg
  63.  
  64. def __str__(self):
  65. return self.msg
  66.  
  67. def __unicode__(self):
  68. return self.msg
  69.  
  70.  
  71. class AppUrlOpener(urllib.FancyURLopener):
  72. """Set user agent for downloads"""
  73. version = 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
  74.  
  75.  
  76. class IPTVSetup:
  77. def display_welcome(self):
  78. # welcome message
  79. print('\n********************************')
  80. print('Starting Engima2 IPTV bouquets v{}'.format(__version__))
  81. print(str(datetime.datetime.now()))
  82. print("********************************\n")
  83.  
  84. def uninstaller(self):
  85. """Clean up routine to remove any previously made changes"""
  86. print('----Running uninstall----')
  87. try:
  88. # Bouquets
  89. print('Removing old IPTV bouquets...')
  90. for fname in os.listdir(ENIGMAPATH):
  91. if 'userbouquet.suls_iptv_' in fname:
  92. os.remove(os.path.join(ENIGMAPATH, fname))
  93. elif 'bouquets.tv.bak' in fname:
  94. os.remove(os.path.join(ENIGMAPATH, fname))
  95. # Custom Channels and sources
  96. print('Removing IPTV custom channels...')
  97. if os.path.isdir(EPGIMPORTPATH):
  98. for fname in os.listdir(EPGIMPORTPATH):
  99. if 'suls_iptv_' in fname:
  100. os.remove(os.path.join(EPGIMPORTPATH, fname))
  101. # bouquets.tv
  102. print('Removing IPTV bouquets from bouquets.tv...')
  103. os.rename(os.path.join(ENIGMAPATH, 'bouquets.tv'), os.path.join(ENIGMAPATH, 'bouquets.tv.bak'))
  104. tvfile = open(os.path.join(ENIGMAPATH, 'bouquets.tv'), 'w+')
  105. bakfile = open(os.path.join(ENIGMAPATH, 'bouquets.tv.bak'))
  106. for line in bakfile:
  107. if '.suls_iptv_' not in line:
  108. tvfile.write(line)
  109. bakfile.close()
  110. tvfile.close()
  111. except Exception, e:
  112. print('Unable to uninstall')
  113. raise
  114. print('----Uninstall complete----')
  115.  
  116. def download_m3u(self, url):
  117. """Download m3u file from url"""
  118. path = tempfile.gettempdir()
  119. filename = os.path.join(path, 'e2m3u2bouquet.m3u')
  120. print("\n----Downloading m3u file----")
  121. if DEBUG:
  122. print("m3uurl = {}".format(url))
  123. try:
  124. urllib.urlretrieve(url, filename)
  125. except Exception, e:
  126. print('Unable to download m3u file from url')
  127. raise
  128. return filename
  129.  
  130. def download_providers(self, url):
  131. """Download providers file from url"""
  132. path = tempfile.gettempdir()
  133. filename = os.path.join(path, 'providers.txt')
  134. print("\n----Downloading providers file----")
  135. if DEBUG:
  136. print("providers url = {}".format(url))
  137. try:
  138. # context = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
  139. context = ssl._create_unverified_context()
  140. urllib.urlretrieve(url, filename, context=context)
  141. return filename
  142. except Exception:
  143. pass # fallback to no ssl context
  144. try:
  145. urllib.urlretrieve(url, filename)
  146. return filename
  147. except Exception, e:
  148. print('Unable to download Github providers')
  149. raise
  150.  
  151. def download_bouquet(self, url):
  152. """Download panel bouquet file from url"""
  153. path = tempfile.gettempdir()
  154. filename = os.path.join(path, 'userbouquet.panel.tv')
  155. print("\n----Downloading providers bouquet file----")
  156. if DEBUG:
  157. print("bouqueturl = {}".format(url))
  158. try:
  159. urllib.urlretrieve(url, filename)
  160. except Exception, e:
  161. print('Unable to download providers panel bouquet file')
  162. raise
  163. return filename
  164.  
  165. def parse_panel_bouquet(self, panel_bouquet_file):
  166. """Check providers bouquet for custom service references
  167. """
  168. panel_bouquet = {}
  169.  
  170. if os.path.isfile(panel_bouquet_file):
  171. with open(panel_bouquet_file, "r") as f:
  172. for line in f:
  173. if '#SERVICE' in line:
  174. # get service ref values we need (dict value) and stream file (dict key)
  175. service = line.strip().split(':')
  176. if len(service) == 11:
  177. pos = service[10].rfind('/')
  178. if pos != -1 and (pos + 1 != len(service[10])):
  179. key = service[10][pos + 1:]
  180. value = ':'.join((service[3], service[4], service[5], service[6]))
  181. if value != '0:0:0:0':
  182. # only add to dict if a custom service id is present
  183. panel_bouquet[key] = value
  184. if not DEBUG:
  185. # remove panel bouquet file
  186. os.remove(panel_bouquet_file)
  187. return panel_bouquet
  188.  
  189. def parse_m3u(self, filename, all_iptv_stream_types, tv_stream_type, vod_stream_type, panel_bouquet, xcludesref, providername):
  190. """core parsing routine"""
  191. # Extract and generate the following items from the m3u
  192. # group-title
  193. # tvg-name
  194. # tvg-id
  195. # tvg-logo
  196. # stream-name
  197. # stream-url
  198.  
  199. print("\n----Parsing m3u file----")
  200. try:
  201. if not os.path.getsize(filename):
  202. raise Exception("M3U file is empty. Check username & password")
  203. except Exception, e:
  204. raise
  205.  
  206. category_order = []
  207. category_options = {}
  208. channeldict = {}
  209.  
  210. dictchannels = OrderedDict()
  211. with open(filename, "r") as f:
  212. for line in f:
  213. if 'EXTM3U' in line: # First line we are not interested
  214. continue
  215. elif 'EXTINF:' in line: # Info line - work out group and output the line
  216. channeldict = {'tvg-id': '', 'tvg-name': '', 'tvg-logo': '', 'group-title': '', 'stream-name': '',
  217. 'stream-url': '',
  218. 'enabled': True,
  219. 'nameOverride': '',
  220. 'serviceRef': '',
  221. 'serviceRefOverride': False
  222. }
  223. if line.find('tvg-') == -1 :
  224. raise Exception("No extended playlist info found. Check m3u url should be 'type=m3u_plus'")
  225. channel = line.split('"')
  226. # strip unwanted info at start of line
  227. pos = channel[0].find(' ')
  228. channel[0] = channel[0][pos:]
  229.  
  230. # loop through params and build dict
  231. for i in xrange(0, len(channel) - 2, 2):
  232. channeldict[channel[i].lower().strip(' =')] = channel[i + 1].decode('utf-8')
  233.  
  234. # Get the stream name from end of line (after comma)
  235. stream_name_pos = line.rfind(',')
  236. if stream_name_pos != -1:
  237. channeldict['stream-name'] = line[stream_name_pos + 1:].strip().decode('utf-8')
  238.  
  239. # Set default name for any blank groups
  240. if channeldict['group-title'] == '':
  241. channeldict['group-title'] = u'None'
  242. elif 'http:' in line or 'https:' in line or 'rtmp:' in line:
  243. if 'tvg-id' not in channeldict:
  244. # if this is the true the playlist had a http line but not EXTINF
  245. raise Exception("No extended playlist info found. Check m3u url should be 'type=m3u_plus'")
  246. channeldict['stream-url'] = line.strip()
  247. self.set_streamtypes_vodcats(channeldict, all_iptv_stream_types, tv_stream_type, vod_stream_type)
  248.  
  249. if channeldict['group-title'] not in dictchannels:
  250. dictchannels[channeldict['group-title']] = [channeldict]
  251. else:
  252. dictchannels[channeldict['group-title']].append(channeldict)
  253.  
  254. category_order = dictchannels.keys()
  255.  
  256. # sort categories by custom order (if exists)
  257. sorted_categories, category_options = self.parse_map_bouquet_xml(dictchannels, providername)
  258. sorted_categories.extend(category_order)
  259. # remove duplicates, keep order
  260. category_order = OrderedDict((x, True) for x in sorted_categories).keys()
  261.  
  262. # Check for and parse override map
  263. self.parse_map_channels_xml(dictchannels, xcludesref, providername)
  264.  
  265. # Add Service references
  266. # VOD won't have epg so use same service id for all VOD
  267. vod_service_id = 65535
  268. serviceid_start = 34000
  269. category_offset = 150
  270. catstartnum = serviceid_start
  271.  
  272. for cat in category_order:
  273. num = catstartnum
  274. if cat in dictchannels:
  275. if not cat.startswith("VOD"):
  276. if cat in category_options:
  277. # check if we have cat idStart from override file
  278. if category_options[cat]["idStart"] > 0:
  279. num = category_options[cat]["idStart"]
  280. else:
  281. category_options[cat]["idStart"] = num
  282. else:
  283. category_options[cat] = {"idStart": num}
  284.  
  285. for x in dictchannels[cat]:
  286. cat_id = self.get_category_id(cat, providername)
  287. service_ref = "{:x}:{}:{}:0".format(num, cat_id[:4], cat_id[4:])
  288. if panel_bouquet:
  289. # check if we have the panels custom service ref
  290. pos = x['stream-url'].rfind('/')
  291. if pos != -1 and (pos + 1 != len(x['stream-url'])):
  292. m3u_stream_file = x['stream-url'][pos + 1:]
  293. if m3u_stream_file in panel_bouquet:
  294. # have a match use the panels custom service ref
  295. service_ref = panel_bouquet[m3u_stream_file]
  296. if not x['serviceRefOverride']:
  297. # if service ref is not overridden in xml update
  298. x['serviceRef'] = "{}:0:1:{}:0:0:0".format(x['stream-type'], service_ref)
  299. num += 1
  300. else:
  301. for x in dictchannels[cat]:
  302. x['serviceRef'] = "{}:0:1:{:x}:0:0:0:0:0:0".format(x['stream-type'], vod_service_id)
  303. while catstartnum < num:
  304. catstartnum += category_offset
  305.  
  306. # move all VOD categories to VOD placeholder position
  307. if ("VOD" in category_order):
  308. vodindex = category_order.index("VOD")
  309. vodcategories = list((cat for cat in category_order if cat.startswith('VOD -')))
  310. if len(vodcategories):
  311. # remove the multi vod categories from their current location
  312. category_order = [x for x in category_order if x not in vodcategories]
  313. # insert the multi vod categories at the placeholder pos
  314. category_order[vodindex:vodindex] = vodcategories
  315. category_order.remove("VOD")
  316.  
  317. # Have a look at what we have
  318. if DEBUG and TESTRUN:
  319. datafile = open(os.path.join(CFGPATH, 'channels.debug'), "w+")
  320. for cat in category_order:
  321. if cat in dictchannels:
  322. for line in dictchannels[cat]:
  323. linevals = ""
  324. for key, value in line.items():
  325. if type(value) is bool:
  326. linevals += str(value) + ":"
  327. else:
  328. linevals += (value).encode("utf-8") + ":"
  329. datafile.write("{}\n".format(linevals))
  330. datafile.close()
  331. print("Completed parsing data...")
  332.  
  333. if not DEBUG:
  334. # remove m3u file
  335. if os.path.isfile(filename):
  336. os.remove(filename)
  337.  
  338. return category_order, category_options, dictchannels
  339.  
  340. def set_streamtypes_vodcats(self, channeldict, all_iptv_stream_types, tv_stream_type, vod_stream_type):
  341. """Set the stream types and VOD categories
  342. """
  343. parsed_stream_url = urlparse.urlparse(channeldict['stream-url'])
  344. root, ext = os.path.splitext(parsed_stream_url.path)
  345.  
  346. # check for vod streams ending .*.m3u8 e.g. 2345.mp4.m3u8
  347. is_m3u8_vod = re.search('\..+\.m3u8$', parsed_stream_url.path)
  348.  
  349. if (parsed_stream_url.path.endswith('.ts') or parsed_stream_url.path.endswith('.m3u8')) \
  350. or not ext \
  351. and not is_m3u8_vod \
  352. and not channeldict['group-title'].startswith('VOD'):
  353. channeldict['stream-type'] = '4097' if all_iptv_stream_types else '1'
  354. if tv_stream_type:
  355. # Set custom TV stream type if supplied - this overrides all_iptv_stream_types
  356. channeldict['stream-type'] = str(tv_stream_type)
  357. else:
  358. channeldict['group-title'] = u"VOD - {}".format(channeldict['group-title'])
  359. channeldict['stream-type'] = '4097' if not vod_stream_type else str(vod_stream_type)
  360.  
  361. def parse_map_bouquet_xml(self, dictchannels, providername):
  362. """Check for a mapping override file and parses it if found
  363. """
  364. category_order = []
  365. category_options = {}
  366. mapping_file = self.get_mapping_file(providername)
  367. if mapping_file:
  368. print("\n----Parsing custom bouquet order----")
  369.  
  370. tree = ET.ElementTree(file=mapping_file)
  371. for node in tree.findall(".//category"):
  372. dictoption = {}
  373.  
  374. category = node.attrib.get('name')
  375. if not type(category) is unicode:
  376. category = category.decode("utf-8")
  377. cat_title_override = node.attrib.get('nameOverride', '')
  378. if not type(cat_title_override) is unicode:
  379. cat_title_override = cat_title_override.decode("utf-8")
  380. dictoption['nameOverride'] = cat_title_override
  381. dictoption['idStart'] = int(node.attrib.get('idStart', '0')) \
  382. if node.attrib.get('idStart', '0').isdigit() else 0
  383. if node.attrib.get('enabled') == 'false':
  384. dictoption["enabled"] = False
  385. # Remove category/bouquet
  386. if category != "VOD":
  387. if category in dictchannels:
  388. dictchannels.pop(category, None)
  389. else:
  390. keys_to_remove = []
  391. for k in dictchannels.iterkeys():
  392. if k.startswith("VOD"):
  393. keys_to_remove.append(k)
  394. if keys_to_remove:
  395. for k in keys_to_remove:
  396. dictchannels.pop(k, None)
  397. else:
  398. dictoption["enabled"] = True
  399. category_order.append(category)
  400.  
  401. category_options[category] = dictoption
  402.  
  403. print("custom bouquet order parsed...")
  404. return category_order, category_options
  405.  
  406. def parse_map_xmltvsources_xml(self, providername):
  407. """Check for a mapping override file and parses it if found
  408. """
  409. list_xmltv_sources = {}
  410. mapping_file = self.get_mapping_file(providername)
  411. if mapping_file:
  412. tree = ET.ElementTree(file=mapping_file)
  413. for group in tree.findall('.//xmltvextrasources/group'):
  414. group_name = group.attrib.get('id')
  415. urllist = []
  416. for url in group:
  417. urllist.append(url.text)
  418. list_xmltv_sources[group_name] = urllist
  419. return list_xmltv_sources
  420.  
  421. def parse_map_channels_xml(self, dictchannels, xcludesref, providername):
  422. """Check for a mapping override file and applies it if found
  423. """
  424. mappingfile = self.get_mapping_file(providername)
  425. if mappingfile:
  426. print("\n----Parsing custom channel order, please be patient----")
  427.  
  428. tree = ET.ElementTree(file=mappingfile)
  429. for cat in dictchannels:
  430. if not cat.startswith("VOD"):
  431. # We don't override any individual VOD streams
  432. print("sorting {}".format(cat.encode("utf-8")))
  433.  
  434. sortedchannels = []
  435. listchannels = []
  436. for x in dictchannels[cat]:
  437. listchannels.append(x['stream-name'])
  438. for node in tree.findall(u'.//channel[@category="{}"]'.format(cat)):
  439. sortedchannels.append(node.attrib.get('name'))
  440.  
  441. sortedchannels.extend(listchannels)
  442. # remove duplicates, keep order
  443. listchannels = OrderedDict((x, True) for x in sortedchannels).keys()
  444.  
  445. # sort the channels by new order
  446. channel_order_dict = {channel: index for index, channel in enumerate(listchannels)}
  447. dictchannels[cat].sort(key=lambda x: channel_order_dict[x['stream-name']])
  448.  
  449. for x in dictchannels[cat]:
  450. node = tree.find(u'.//channel[@name="{}"]'.format(x['stream-name']))
  451. if node is not None:
  452. if node.attrib.get('enabled') == 'false':
  453. x['enabled'] = False
  454. x['nameOverride'] = node.attrib.get('nameOverride', '')
  455. # default to current values if attribute doesn't exist
  456. x['tvg-id'] = node.attrib.get('tvg-id', x['tvg-id'])
  457. if node.attrib.get('serviceRef', None) and not xcludesref:
  458. x['serviceRef'] = node.attrib.get('serviceRef', x['serviceRef'])
  459. x['serviceRefOverride'] = True
  460. # streamUrl no longer output to xml file but we still check and process it
  461. x['stream-url'] = node.attrib.get('streamUrl', x['stream-url'])
  462. clear_stream_url = node.attrib.get('clearStreamUrl') == 'true'
  463. if clear_stream_url:
  464. x['stream-url'] = ''
  465.  
  466. print('custom channel order parsed...')
  467.  
  468. def save_map_xml(self, categoryorder, category_options, dictchannels, list_xmltv_sources, providername):
  469. """Create mapping file"""
  470. mappingfile = os.path.join(CFGPATH, self.get_safe_filename(providername) + '-sort-current.xml')
  471. indent = " "
  472. vod_category_output = False
  473.  
  474. if dictchannels:
  475. with open(mappingfile, "wb") as f:
  476. f.write('<!--\r\n')
  477. f.write('{} E2m3u2bouquet Custom mapping file\r\n'.format(indent))
  478. f.write('{} Rearrange bouquets or channels in the order you wish\r\n'.format(indent))
  479. f.write('{} Disable bouquets or channels by setting enabled to "false"\r\n'.format(indent))
  480. f.write('{} Map DVB EPG to IPTV by changing channel serviceRef attribute to match DVB service reference\r\n'.format(indent))
  481. f.write('{} Map XML EPG to different feed by changing channel tvg-id attribute\r\n'.format(indent))
  482. f.write('{} Rename this file as {}-sort-override.xml for changes to apply\r\n'.format(indent, self.get_safe_filename(providername)))
  483. f.write('-->\r\n')
  484.  
  485. f.write('<mapping>\r\n')
  486.  
  487. f.write('{}<xmltvextrasources>\r\n'.format(indent))
  488. if not list_xmltv_sources:
  489. # output example config
  490. f.write('{}<!-- Example Config\r\n'.format((2 * indent)))
  491. # UK - Freeview (xz)
  492. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - Freeview (xz)'))
  493. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_Basic.xz'))
  494. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.ipservers.eu/epg_data/rytecUK_Basic.xz'))
  495. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_Basic.xz'))
  496. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_Basic.xz'))
  497. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.vuplus-community.net/rytec/rytecUK_Basic.xz'))
  498. f.write('{}</group>\r\n'.format(2 * indent))
  499. # UK - FTA (xz)
  500. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - FTA (xz)'))
  501. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_FTA.xz'))
  502. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.ipservers.eu/epg_data/rytecUK_FTA.xz'))
  503. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_FTA.xz'))
  504. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_FTA.xz'))
  505. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.vuplus-community.net/rytec/rytecUK_FTA.xz'))
  506. f.write('{}</group>\r\n'.format(2 * indent))
  507. # UK - International (xz)
  508. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - International (xz)'))
  509. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_int.xz'))
  510. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  511. 'http://rytecepg.ipservers.eu/epg_data/rytecUK_int.xz'))
  512. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_int.xz'))
  513. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  514. 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_int.xz'))
  515. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  516. 'http://www.vuplus-community.net/rytec/rytecUK_int.xz'))
  517. f.write('{}</group>\r\n'.format(2 * indent))
  518. # UK - Sky Live (xz)
  519. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - Sky Live (xz)'))
  520. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_SkyLive.xz'))
  521. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  522. 'http://rytecepg.ipservers.eu/epg_data/rytecUK_SkyLive.xz'))
  523. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_SkyLive.xz'))
  524. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  525. 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_SkyLive.xz'))
  526. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  527. 'http://www.vuplus-community.net/rytec/rytecUK_SkyLive.xz'))
  528. f.write('{}</group>\r\n'.format(2 * indent))
  529. # UK - Sky Dead (xz)
  530. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - Sky Dead (xz)'))
  531. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_SkyDead.xz'))
  532. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  533. 'http://rytecepg.ipservers.eu/epg_data/rytecUK_SkyDead.xz'))
  534. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_SkyDead.xz'))
  535. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  536. 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_SkyDead.xz'))
  537. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  538. 'http://www.vuplus-community.net/rytec/rytecUK_SkyDead.xz'))
  539. f.write('{}</group>\r\n'.format(2 * indent))
  540. # UK - Sports/Movies (xz)
  541. f.write('{}<group id="{}">\r\n'.format(2 * indent, 'UK - Sports/Movies (xz)'))
  542. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://www.xmltvepg.nl/rytecUK_SportMovies.xz'))
  543. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  544. 'http://rytecepg.ipservers.eu/epg_data/rytecUK_SportMovies.xz'))
  545. f.write('{}<url>{}</url>\r\n'.format(3 * indent, 'http://rytecepg.wanwizard.eu/rytecUK_SportMovies.xz'))
  546. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  547. 'http://91.121.106.172/~rytecepg/epg_data/rytecUK_SportMovies.xz'))
  548. f.write('{}<url>{}</url>\r\n'.format(3 * indent,
  549. 'http://www.vuplus-community.net/rytec/rytecUK_SportMovies.xz'))
  550. f.write('{}</group>\r\n'.format(2 * indent))
  551. f.write('{}-->\r\n'.format(2 * indent))
  552.  
  553. else:
  554. for group in list_xmltv_sources:
  555. f.write('{}<group id="{}">\r\n'.format(2 * indent, self.xml_escape(group)))
  556. for source in list_xmltv_sources[group]:
  557. f.write('{}<url>{}</url>\r\n'.format(3 * indent, self.xml_escape(source)))
  558. f.write('{}</group>\r\n'.format(2 * indent))
  559. f.write('{}</xmltvextrasources>\r\n'.format(indent))
  560.  
  561. f.write('{}<categories>\r\n'.format(indent))
  562. for cat in categoryorder:
  563. if cat in dictchannels:
  564. if not cat.startswith('VOD -'):
  565. cat_title_override = ''
  566. idStart = ''
  567. if cat in category_options:
  568. cat_title_override = category_options[cat].get('nameOverride', '')
  569. idStart = category_options[cat].get('idStart', '')
  570. f.write('{}<category name="{}" nameOverride="{}" idStart="{}" enabled="true" />\r\n'
  571. .format(2 * indent,
  572. self.xml_escape(cat).encode('utf-8'),
  573. self.xml_escape(cat_title_override).encode('utf-8'),
  574. idStart
  575. ))
  576. elif not vod_category_output:
  577. # Replace multivod categories with single VOD placeholder
  578. cat_title_override = ''
  579. if 'VOD' in category_options:
  580. cat_title_override = category_options['VOD'].get('nameOverride', '')
  581. f.write('{}<category name="{}" nameOverride="{}" enabled="true" />\r\n'
  582. .format(2 * indent,
  583. 'VOD',
  584. self.xml_escape(cat_title_override).encode('utf-8'),
  585. ))
  586. vod_category_output = True
  587. for cat in category_options:
  588. if 'enabled' in category_options[cat] and category_options[cat]['enabled'] is False:
  589. f.write('{}<category name="{}" nameOverride="{}" enabled="false" />\r\n'
  590. .format(2 * indent,
  591. self.xml_escape(cat).encode("utf-8"),
  592. self.xml_escape(cat_title_override).encode("utf-8")
  593. ))
  594.  
  595. f.write('{}</categories>\r\n'.format(indent))
  596.  
  597. f.write('{}<channels>\r\n'.format(indent))
  598. for cat in categoryorder:
  599. if cat in dictchannels:
  600. # Don't output any of the VOD channels
  601. if not cat.startswith('VOD'):
  602. f.write('{}<!-- {} -->\r\n'.format(2 * indent, self.xml_safe_comment(self.xml_escape(cat.encode('utf-8')))))
  603. for x in dictchannels[cat]:
  604. f.write('{}<channel name="{}" nameOverride="{}" tvg-id="{}" enabled="{}" category="{}" serviceRef="{}" clearStreamUrl="{}" />\r\n'
  605. .format(2 * indent,
  606. self.xml_escape(x['stream-name'].encode('utf-8')),
  607. self.xml_escape(x.get('nameOverride', '').encode('utf-8')),
  608. self.xml_escape(x['tvg-id'].encode('utf-8')),
  609. str(x['enabled']).lower(),
  610. self.xml_escape(cat.encode('utf-8')),
  611. self.xml_escape(x['serviceRef']),
  612. 'false' if x['stream-url'] else 'true'
  613. ))
  614.  
  615. f.write('{}</channels>\r\n'.format(indent))
  616. f.write('</mapping>')
  617.  
  618. def download_picons(self, dictchannels, iconpath):
  619. print('\n----Downloading Picon files, please be patient----')
  620. print('If no Picons exist this will take a few minutes')
  621. try:
  622. os.makedirs(iconpath)
  623. except OSError, e: # race condition guard
  624. if e.errno != errno.EEXIST:
  625. raise
  626.  
  627. for cat in dictchannels:
  628. if not cat.startswith('VOD'):
  629. # Download Picon if not VOD
  630. for x in dictchannels[cat]:
  631. self.download_picon_file(x['tvg-logo'], self.get_service_title(x), iconpath)
  632. print('\nPicons download completed...')
  633. print('Box will need restarted for Picons to show...')
  634.  
  635. def download_picon_file(self, logourl, title, iconpath):
  636. if logourl:
  637. if not logourl.startswith('http'):
  638. logourl = 'http://{}'.format(logourl)
  639. piconname = self.get_picon_name(title)
  640. piconfilepath = os.path.join(iconpath, piconname)
  641. existingpicon = filter(os.path.isfile, glob.glob(piconfilepath + '*'))
  642.  
  643. if not existingpicon:
  644. if DEBUG:
  645. print("Picon file doesn't exist downloading")
  646. print('PiconURL: {}'.format(logourl))
  647. else:
  648. # Output some kind of progress indicator
  649. if not IMPORTED:
  650. # don't output when called from the plugin
  651. sys.stdout.write('.')
  652. sys.stdout.flush()
  653. try:
  654. response = urllib.urlopen(logourl)
  655. info = response.info()
  656. response.close()
  657. if info.maintype == 'image':
  658. urllib.urlretrieve(logourl, piconfilepath)
  659. else:
  660. if DEBUG:
  661. print('Download Picon - not an image skipping')
  662. return
  663. except Exception, e:
  664. if DEBUG:
  665. print('Download picon urlopen error', e)
  666. return
  667. self.picon_post_processing(piconfilepath)
  668.  
  669. def picon_post_processing(self, piconfilepath):
  670. """Check type of image received and convert to png
  671. if necessary
  672. """
  673. ext = ""
  674. # get image type
  675. try:
  676. ext = imghdr.what(piconfilepath)
  677. except Exception, e:
  678. if DEBUG:
  679. print('Picon post processing - not an image or no file', e, piconfilepath)
  680. return
  681. # if image but not png convert to png
  682. if (ext is not None) and (ext is not 'png'):
  683. if DEBUG:
  684. print('Converting Picon to png')
  685. try:
  686. Image.open(piconfilepath).save("{}.{}".format(piconfilepath, 'png'))
  687. except Exception, e:
  688. if DEBUG:
  689. print('Picon post processing - unable to convert image', e)
  690. return
  691. try:
  692. # remove non png file
  693. os.remove(piconfilepath)
  694. except Exception, e:
  695. if DEBUG:
  696. print('Picon post processing - unable to remove non png file', e)
  697. return
  698. else:
  699. # rename to correct extension
  700. try:
  701. os.rename(piconfilepath, "{}.{}".format(piconfilepath, ext))
  702. except Exception, e:
  703. if DEBUG:
  704. print('Picon post processing - unable to rename file ', e)
  705.  
  706. def get_picon_name(self, serviceName):
  707. """Convert the service name to a Picon Service Name
  708. """
  709. name = serviceName
  710. if type(name) is unicode:
  711. name = name.encode('utf-8')
  712. name = unicodedata.normalize('NFKD', unicode(name, 'utf_8')).encode('ASCII', 'ignore')
  713. exclude_chars = ['/', '\\', '\'', '"', '`', '?', ' ', '(', ')', ':', '<', '>', '|', '.', '\n', '!']
  714. name = re.sub('[%s]' % ''.join(exclude_chars), '', name)
  715. name = name.replace('&', 'and')
  716. name = name.replace('+', 'plus')
  717. name = name.replace('*', 'star')
  718. name = name.lower()
  719. return name
  720.  
  721. def get_safe_filename(self, filename):
  722. """Convert filename to safe filename
  723. """
  724. name = filename.replace(" ", "_").replace("/", "_")
  725. if type(name) is unicode:
  726. name = name.encode('utf-8')
  727. name = unicodedata.normalize('NFKD', unicode(name, 'utf_8')).encode('ASCII', 'ignore')
  728. exclude_chars = ['/', '\\', '\'', '"', '`',
  729. '?', ' ', '(', ')', ':', '<', '>',
  730. '|', '.', '\n', '!', '&', '+', '*']
  731. name = re.sub('[%s]' % ''.join(exclude_chars), '', name)
  732. name = name.lower()
  733. return name
  734.  
  735. def get_current_bouquet_indexes(self, providername):
  736. """Get all the bouquet indexes except this provider
  737. """
  738. current_bouquets_indexes =[]
  739.  
  740. with open(os.path.join(ENIGMAPATH, 'bouquets.tv'), 'r') as f:
  741. for line in f:
  742. if line.startswith('#NAME'):
  743. continue
  744. else:
  745. if not '.suls_iptv_{}'.format(self.get_safe_filename(providername)) in line:
  746. current_bouquets_indexes.append(line)
  747. return current_bouquets_indexes
  748.  
  749. def create_bouquets(self, category_order, category_options, dictchannels, multivod, allbouquet, bouquettop, providername):
  750. """Create the Enigma2 bouquets
  751. """
  752. print("\n----Creating bouquets----")
  753. # clean old bouquets before writing new
  754. if dictchannels:
  755. for fname in os.listdir(ENIGMAPATH):
  756. if 'userbouquet.suls_iptv_{}'.format(self.get_safe_filename(providername)) in fname:
  757. os.remove(os.path.join(ENIGMAPATH, fname))
  758. iptv_bouquet_list = []
  759.  
  760. if allbouquet:
  761. iptv_bouquet_list = self.create_all_channels_bouquet(category_order, category_options, dictchannels, providername)
  762.  
  763. vod_categories = list(cat for cat in category_order if cat.startswith('VOD -'))
  764. vod_category_output = False
  765. vod_bouquet_entry_output = False
  766. channel_number_start_offset_output = False
  767.  
  768. for cat in category_order:
  769. if cat in dictchannels:
  770. cat_title = self.get_category_title(cat, category_options)
  771. # create file
  772. cat_filename = self.get_safe_filename(cat_title)
  773. provider_filename = self.get_safe_filename(providername)
  774.  
  775. if cat in vod_categories and not multivod:
  776. cat_filename = "VOD"
  777.  
  778. bouquet_filepath = os.path.join(ENIGMAPATH, 'userbouquet.suls_iptv_{}_{}.tv'
  779. .format(provider_filename, cat_filename))
  780. if DEBUG:
  781. print("Creating: {}".format(bouquet_filepath))
  782.  
  783. if cat not in vod_categories or multivod:
  784. with open(bouquet_filepath, "w+") as f:
  785. bouquet_name = '{} - {}'.format(providername, cat_title.encode('utf-8')).decode("utf-8")
  786. if not cat.startswith('VOD -'):
  787. if cat in category_options and category_options[cat].get('nameOverride', False):
  788. bouquet_name = category_options[cat]['nameOverride'].decode('utf-8')
  789. else:
  790. if 'VOD' in category_options and category_options['VOD'].get('nameOverride', False):
  791. bouquet_name = '{} - {}'\
  792. .format(category_options['VOD']['nameOverride'].decode('utf-8'),
  793. cat_title.replace('VOD - ', '').decode("utf-8"))
  794. channel_num = 0
  795. f.write("#NAME {}\n".format(bouquet_name.encode("utf-8")))
  796. if not channel_number_start_offset_output and not allbouquet:
  797. # write place holder services (for channel numbering)
  798. for i in xrange(100):
  799. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  800. channel_number_start_offset_output = True
  801. channel_num += 1
  802.  
  803. for x in dictchannels[cat]:
  804. if x['enabled']:
  805. self.save_bouquet_entry(f, x)
  806. channel_num += 1
  807.  
  808. while (channel_num % 100) is not 0:
  809. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  810. channel_num += 1
  811. elif not vod_category_output and not multivod:
  812. # not multivod - output all the vod services in one file
  813. with open(bouquet_filepath, "w+") as f:
  814. bouquet_name = '{} - VOD'.format(providername).decode("utf-8")
  815. if 'VOD' in category_options and category_options['VOD'].get('nameOverride', False):
  816. bouquet_name = category_options['VOD']['nameOverride'].decode('utf-8')
  817.  
  818. channel_num = 0
  819. f.write("#NAME {}\n".format(bouquet_name.encode("utf-8")))
  820. if not channel_number_start_offset_output and not allbouquet:
  821. # write place holder services (for channel numbering)
  822. for i in xrange(100):
  823. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  824. channel_number_start_offset_output = True
  825. channel_num += 1
  826.  
  827. for vodcat in vod_categories:
  828. if vodcat in dictchannels:
  829. # Insert group description placeholder in bouquet
  830. f.write("#SERVICE 1:64:0:0:0:0:0:0:0:0:\n")
  831. f.write("#DESCRIPTION {}\n". format(vodcat))
  832. for x in dictchannels[vodcat]:
  833. self.save_bouquet_entry(f, x)
  834. channel_num += 1
  835.  
  836. while (channel_num % 100) is not 0:
  837. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  838. channel_num += 1
  839. vod_category_output = True
  840.  
  841. # Add to bouquet index list
  842. if cat not in vod_categories or (cat in vod_categories and not vod_bouquet_entry_output):
  843. iptv_bouquet_list.append(self.get_bouquet_index_name(cat_filename, provider_filename))
  844. if cat in vod_categories and not multivod:
  845. vod_bouquet_entry_output = True
  846.  
  847. # write the bouquets.tv indexes
  848. self.save_bouquet_index_entries(iptv_bouquet_list, bouquettop, providername)
  849.  
  850. print("bouquets created ...")
  851.  
  852. def create_all_channels_bouquet(self, category_order, category_options, dictchannels, providername):
  853. """Create the Enigma2 all channels bouquet
  854. """
  855. print("\n----Creating all channels bouquet----")
  856.  
  857. bouquet_indexes = []
  858.  
  859. vod_categories = list(cat for cat in category_order if cat.startswith('VOD -'))
  860. bouquet_name = '{} All Channels'.format(providername)
  861. cat_filename = self.get_safe_filename(bouquet_name)
  862. provider_filename = self.get_safe_filename(providername)
  863.  
  864. # create file
  865. bouquet_filepath = os.path.join(ENIGMAPATH, 'userbouquet.suls_iptv_{}_{}.tv'
  866. .format(provider_filename, cat_filename))
  867. if DEBUG:
  868. print("Creating: {}".format(bouquet_filepath))
  869.  
  870. with open(bouquet_filepath, 'w+') as f:
  871. f.write('#NAME {} - {}\n'.format(providername.encode('utf-8'), bouquet_name.encode('utf-8')))
  872.  
  873. # write place holder channels (for channel numbering)
  874. for i in xrange(100):
  875. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  876. channel_num = 1
  877.  
  878. for cat in category_order:
  879. if cat in dictchannels:
  880. if cat not in vod_categories:
  881. cat_title = self.get_category_title(cat, category_options)
  882. # Insert group description placeholder in bouquet
  883. f.write("#SERVICE 1:64:0:0:0:0:0:0:0:0:\n")
  884. f.write("#DESCRIPTION {}\n".format(cat_title.encode('utf-8')))
  885. for x in dictchannels[cat]:
  886. if x['enabled']:
  887. self.save_bouquet_entry(f, x)
  888. channel_num += 1
  889.  
  890. while (channel_num % 100) is not 0:
  891. f.write('#SERVICE 1:832:d:0:0:0:0:0:0:0:\n')
  892. channel_num += 1
  893.  
  894. # Add to bouquet index list
  895. bouquet_indexes.append(self.get_bouquet_index_name(cat_filename, provider_filename))
  896. print("all channels bouquet created ...")
  897. return bouquet_indexes
  898.  
  899. def save_bouquet_entry(self, f, channel):
  900. """Add service to bouquet file
  901. """
  902. f.write("#SERVICE {}:{}:{}\n"
  903. .format(channel['serviceRef'], urllib.quote(channel['stream-url']),
  904. self.get_service_title(channel).encode("utf-8")))
  905. f.write("#DESCRIPTION {}\n".format(self.get_service_title(channel).encode("utf-8")))
  906.  
  907. def get_bouquet_index_name(self, cat_filename, provider_filename):
  908. return ('#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.suls_iptv_{}_{}.tv" ORDER BY bouquet\n'
  909. .format(provider_filename, cat_filename))
  910.  
  911. def save_bouquet_index_entries(self, iptv_bouquets, bouquettop, providername):
  912. """Add to the main bouquets.tv file
  913. """
  914. # get current bouquets indexes
  915. current_bouquet_indexes = self.get_current_bouquet_indexes(providername)
  916.  
  917. if iptv_bouquets:
  918. with open(os.path.join(ENIGMAPATH, 'bouquets.tv'), 'w') as f:
  919. f.write('#NAME Bouquets (TV)\n')
  920. if bouquettop:
  921. for bouquet in iptv_bouquets:
  922. f.write(bouquet)
  923. for bouquet in current_bouquet_indexes:
  924. f.write(bouquet)
  925. else:
  926. for bouquet in current_bouquet_indexes:
  927. f.write(bouquet)
  928. for bouquet in iptv_bouquets:
  929. f.write(bouquet)
  930.  
  931. def reload_bouquets(self):
  932. if not TESTRUN:
  933. print("\n----Reloading bouquets----")
  934. if eDVBDB:
  935. eDVBDB.getInstance().reloadBouquets()
  936. print("bouquets reloaded...")
  937. else:
  938. os.system("wget -qO - http://127.0.0.1/web/servicelistreload?mode=2 > /dev/null 2>&1 &")
  939. print("bouquets reloaded...")
  940.  
  941. def create_epgimporter_config(self, categoryorder, category_options, dictchannels, list_xmltv_sources, epgurl, provider):
  942. indent = " "
  943. if DEBUG:
  944. print('creating EPGImporter config')
  945. # create channels file
  946. try:
  947. os.makedirs(EPGIMPORTPATH)
  948. except OSError, e: # race condition guard
  949. if e.errno != errno.EEXIST:
  950. raise
  951. channels_filename = os.path.join(EPGIMPORTPATH, 'suls_iptv_{}_channels.xml'.format(self.get_safe_filename(provider)))
  952.  
  953. if dictchannels:
  954. with open(channels_filename, "w+") as f:
  955. f.write('<channels>\n')
  956. for cat in categoryorder:
  957. if cat in dictchannels:
  958. if not cat.startswith('VOD'):
  959. cat_title = self.get_category_title(cat, category_options)
  960.  
  961. f.write('{}<!-- {} -->\n'.format(indent, self.xml_safe_comment(self.xml_escape(cat_title.encode('utf-8')))))
  962. for x in dictchannels[cat]:
  963. tvg_id = x['tvg-id'] if x['tvg-id'] else self.get_service_title(x)
  964. if x['enabled']:
  965. # force the epg channels to stream type '1'
  966. epg_service_ref = x['serviceRef']
  967. pos = epg_service_ref.find(':')
  968. if pos != -1:
  969. epg_service_ref = '1{}'.format(epg_service_ref[pos:])
  970. f.write('{}<channel id="{}">{}:http%3a//example.m3u8</channel> <!-- {} -->\n'
  971. .format(indent, self.xml_escape(tvg_id.encode('utf-8')), epg_service_ref,
  972. self.xml_safe_comment(self.xml_escape(self.get_service_title(x).encode('utf-8')))))
  973. f.write('</channels>\n')
  974.  
  975. # create epg-importer sources file for providers feed
  976. self.create_epgimport_source([epgurl], provider)
  977.  
  978. # create epg-importer sources file for additional feeds
  979. for group in list_xmltv_sources:
  980. self.create_epgimport_source(list_xmltv_sources[group], provider, group)
  981.  
  982. def create_epgimport_source(self, sources, provider, group=None):
  983. """Create epg-importer source file
  984. """
  985. indent = " "
  986. source_name = '{} - {}'.format(provider, group) if group else provider
  987.  
  988. channels_filename = os.path.join(EPGIMPORTPATH, 'suls_iptv_{}_channels.xml'.format(self.get_safe_filename(provider)))
  989.  
  990. # write providers epg feed
  991. source_filename = os.path.join(EPGIMPORTPATH, 'suls_iptv_{}.sources.xml'
  992. .format(self.get_safe_filename(source_name)))
  993.  
  994. with open(os.path.join(EPGIMPORTPATH, source_filename), "w+") as f:
  995. f.write('<sources>\n')
  996. f.write('{}<sourcecat sourcecatname="IPTV Bouquet Maker - E2m3u2bouquet">\n'.format(indent))
  997. f.write('{}<source type="gen_xmltv" nocheck="1" channels="{}">\n'
  998. .format(2 * indent, channels_filename))
  999. f.write('{}<description>{}</description>\n'.format(3 * indent, self.xml_escape(source_name)))
  1000. for source in sources:
  1001. f.write('{}<url><![CDATA[{}]]></url>\n'.format(3 * indent, source))
  1002. f.write('{}</source>\n'.format(2 * indent))
  1003. f.write('{}</sourcecat>\n'.format(indent))
  1004. f.write('</sources>\n')
  1005.  
  1006. def read_providers(self, providerfile):
  1007. # Check we have data
  1008. try:
  1009. if not os.path.getsize(providerfile):
  1010. raise Exception('Providers file is empty')
  1011. except Exception, e:
  1012. raise
  1013. with open(providerfile, "r") as f:
  1014. for line in f:
  1015. if line == '400: Invalid request\n':
  1016. print("Providers download is invalid please resolve or use URL based setup")
  1017. sys(exit(1))
  1018. line = base64.b64decode(line)
  1019. if line:
  1020. provider = {
  1021. 'name': line.split(',')[0],
  1022. 'm3u': line.split(',')[1],
  1023. 'epg': line.split(',')[2]
  1024. }
  1025. PROVIDERS[provider['name']] = provider
  1026.  
  1027. if not DEBUG:
  1028. # remove providers file
  1029. os.remove(providerfile)
  1030. return PROVIDERS
  1031.  
  1032. def process_provider(self, provider, username, password):
  1033. username = username if username else ''
  1034. password = password if password else ''
  1035. supported_providers = ''
  1036. for line in PROVIDERS:
  1037. supported_providers += " " + PROVIDERS[line]['name']
  1038. if PROVIDERS[line]['name'].upper() == provider.upper():
  1039. if DEBUG:
  1040. print("----Provider setup details----")
  1041. print("m3u = " + PROVIDERS[line]['m3u'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password)))
  1042. print("epg = " + PROVIDERS[line]['epg'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password)) + "\n")
  1043. return PROVIDERS[line]['m3u'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password)), \
  1044. PROVIDERS[line]['epg'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password)), \
  1045. supported_providers
  1046. # If we get here the supplied provider is invalid
  1047. return "NOTFOUND", "", supported_providers
  1048.  
  1049. def extract_user_details_from_url(self, url):
  1050. username = ''
  1051. password = ''
  1052. if url:
  1053. parsed = urlparse.urlparse(url)
  1054. username_param = urlparse.parse_qs(parsed.query).get('username')
  1055. if username_param:
  1056. username = username_param[0]
  1057. password_param = urlparse.parse_qs(parsed.query).get('password')
  1058. if password_param:
  1059. password = password_param[0]
  1060. return username, password
  1061.  
  1062. def get_mapping_file(self, providername):
  1063. mapping_file = None
  1064. search_path = [os.path.join(CFGPATH, self.get_safe_filename(providername) + '-sort-override.xml'),
  1065. os.path.join(os.getcwd(), self.get_safe_filename(providername) + '-sort-override.xml')]
  1066. for path in search_path:
  1067. if os.path.isfile(path):
  1068. mapping_file = path
  1069. break;
  1070. return mapping_file
  1071.  
  1072. def xml_escape(self, string):
  1073. return string.replace("&", "&amp;") \
  1074. .replace("\"", "&quot;") \
  1075. .replace("'", "&apos;") \
  1076. .replace("<", "&lt;") \
  1077. .replace(">", "&gt;")
  1078.  
  1079. def xml_unescape(self, string):
  1080. return string.replace('&quot;', '"') \
  1081. .replace() \
  1082. .replace("&apos;", "'") \
  1083. .replace("&lt;", "<") \
  1084. .replace("&gt;", ">") \
  1085. .replace("&amp;", "&")
  1086.  
  1087. def xml_safe_comment(self, string):
  1088. """Can't have -- in xml comments"""
  1089. return string.replace('--', '- - ')
  1090.  
  1091. def get_service_title(self, channel):
  1092. """Return the title override if set else the title
  1093. """
  1094. return channel['nameOverride'] if channel.get('nameOverride', False) else channel['stream-name']
  1095.  
  1096. def get_category_title(self, cat, category_options):
  1097. """Return the title override if set else the title
  1098. """
  1099. if cat in category_options:
  1100. return category_options[cat]['nameOverride'] if category_options[cat].get('nameOverride', False) else cat
  1101. return cat
  1102.  
  1103. def get_category_id(self, cat, providername):
  1104. """Generate 32 bit category id to help make service refs unique"""
  1105. return hashlib.md5(providername + cat.encode('utf-8')).hexdigest()[:8]
  1106.  
  1107.  
  1108. class config:
  1109.  
  1110. def makeconfig(self, configfile):
  1111. print('Default configuration file created in {}\n'.format(os.path.join(CFGPATH, 'config.xml')))
  1112.  
  1113. f = open(configfile, 'wb')
  1114. f.write("""<!--\r
  1115. E2m3u2bouquet supplier config file\r
  1116. Add as many suppliers as required and run the script with no parameters\r
  1117. this config file will be used and the relevant bouquets set up for all suppliers entered\r
  1118. 0 = No/false\r
  1119. 1 = Yes/true\r
  1120. For elements with <![CDATA[]] enter value between brackets e.g. <![CDATA[mypassword]]>\r
  1121. -->\r
  1122. <config>\r
  1123. <supplier>\r
  1124. <name>Supplier Name 1</name><!-- Supplier Name -->\r
  1125. <enabled>1</enabled><!-- Enable or disable the supplier (0 or 1) -->\r
  1126. <m3uurl><![CDATA[http://address.yourprovider.com:80/get.php?username=USERNAME&password=PASSWORD&type=m3u_plus&output=ts]]></m3uurl><!-- Extended M3U url -->\r
  1127. <epgurl><![CDATA[http://address.yourprovider.com:80/xmltv.php?username=USERNAME&password=PASSWORD]]></epgurl><!-- XMLTV EPG url -->\r
  1128. <username><![CDATA[]]></username><!-- (Optional) will replace USERNAME placeholder in urls -->\r
  1129. <password><![CDATA[]]></password><!-- (Optional) will replace PASSWORD placeholder in urls -->\r
  1130. <iptvtypes>0</iptvtypes><!-- Change all streams to IPTV type (0 or 1) -->\r
  1131. <streamtypetv></streamtypetv><!-- (Optional) Custom TV stream type (e.g. 1, 4097, 5001 or 5002) -->\r
  1132. <streamtypevod></streamtypevod><!-- (Optional) Custom VOD stream type (e.g. 4097, 5001 or 5002) -->\r
  1133. <multivod>0</multivod><!-- Split VOD into seperate categories (0 or 1) -->\r
  1134. <allbouquet>1</allbouquet><!-- Create all channels bouquet -->\r
  1135. <picons>0</picons><!-- Automatically download Picons (0 or 1) -->\r
  1136. <iconpath>/usr/share/enigma2/picon/</iconpath><!-- Location to store picons -->\r
  1137. <xcludesref>1</xcludesref><!-- Disable service ref overriding from override.xml file (0 or 1) -->\r
  1138. <bouqueturl><![CDATA[]]></bouqueturl><!-- (Optional) url to download providers bouquet - to map custom service references -->\r
  1139. <bouquetdownload>0</bouquetdownload><!-- Download providers bouquet (use default url) must have username and password set above - to map custom service references -->\r
  1140. <bouquettop>0</bouquettop><!-- Place IPTV bouquets at top (0 or 1)-->\r
  1141. </supplier>\r
  1142. <supplier>\r
  1143. <name>Supplier Name</name><!-- Supplier Name -->\r
  1144. <enabled>0</enabled><!-- Enable or disable the supplier (0 or 1) -->\r
  1145. <m3uurl><![CDATA[http://address.yourprovider.com:80/get.php?username=USERNAME&password=PASSWORD&type=m3u_plus&output=ts]]></m3uurl><!-- Extended M3U url -->\r
  1146. <epgurl><![CDATA[http://address.yourprovider.com:80/xmltv.php?username=USERNAME&password=PASSWORD]]></epgurl><!-- XMLTV EPG url -->\r
  1147. <username><![CDATA[]]></username><!-- (Optional) will replace USERNAME placeholder in urls -->\r
  1148. <password><![CDATA[]]></password><!-- (Optional) will replace PASSWORD placeholder in urls -->\r
  1149. <iptvtypes>0</iptvtypes><!-- Change all streams to IPTV type (0 or 1) -->\r
  1150. <streamtypetv></streamtypetv><!-- (Optional) Custom TV service type (e.g. 1, 4097, 5001 or 5002) -->\r
  1151. <streamtypevod></streamtypevod><!-- (Optional) Custom VOD service type (e.g. 4097, 5001 or 5002) -->\r
  1152. <multivod>0</multivod><!-- Split VOD into seperate categories (0 or 1) -->\r
  1153. <allbouquet>1</allbouquet><!-- Create all channels bouquet -->\r
  1154. <picons>0</picons><!-- Automatically download Picons (0 or 1) -->\r
  1155. <iconpath>/usr/share/enigma2/picon/</iconpath><!-- Location to store picons -->\r
  1156. <xcludesref>1</xcludesref><!-- Disable service ref overriding from override.xml file (0 or 1) -->\r
  1157. <bouqueturl><![CDATA[]]></bouqueturl><!-- (Optional) url to download providers bouquet - to map custom service references -->\r
  1158. <bouquetdownload>0</bouquetdownload><!-- Download providers bouquet (use default url) must have username and password set above - to map custom service references -->\r
  1159. <bouquettop>0</bouquettop><!-- Place IPTV bouquets at top (0 or 1)--> \r
  1160. </supplier>\r
  1161. </config>""")
  1162.  
  1163. def readconfig(self, configfile):
  1164. suppliers = OrderedDict()
  1165. tree = ET.ElementTree(file=configfile)
  1166. for node in tree.findall('.//supplier'):
  1167. supplier = {}
  1168. for child in node:
  1169. if (DEBUG == 1) or (TESTRUN == 1):
  1170. print('{} = {}'.format(child.tag, '' if child.text is None else child.text.strip()))
  1171. supplier[child.tag] = '' if child.text is None else child.text.strip()
  1172. if supplier.get('name'):
  1173. suppliers[supplier['name']] = supplier
  1174. return suppliers
  1175.  
  1176. def run_e2m3u2bouquet(self, provider):
  1177. # Build up our args
  1178. newargs = []
  1179. username = provider['username'] if 'username' in provider else ''
  1180. password = provider['password'] if 'password' in provider else ''
  1181.  
  1182. if provider.get('name'):
  1183. newargs.append('-n={}'.format(provider['name']))
  1184. if not username == '':
  1185. newargs.append('-u={}'.format(username))
  1186. if not password == '':
  1187. newargs.append('-p={}'.format(password))
  1188. newargs.append('-m={}'.format(provider['m3uurl'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password))))
  1189. if provider.get('epgurl'):
  1190. newargs.append('-e={}'.format(provider['epgurl'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password))))
  1191. if provider.get('iptvtypes') and provider['iptvtypes'] == '1':
  1192. newargs.append('-i')
  1193. if provider.get('streamtypetv'):
  1194. newargs.append('-sttv={}'.format(provider['streamtypetv']))
  1195. if provider.get('streamtypevod'):
  1196. newargs.append('-stvod={}'.format(provider['streamtypevod']))
  1197. if provider.get('multivod') and provider["multivod"] == "1":
  1198. newargs.append('-M')
  1199. if provider.get('allbouquet') and provider['allbouquet'] == "1":
  1200. newargs.append('-a')
  1201. if provider.get('picons') and provider["picons"] == "1":
  1202. newargs.append('-P')
  1203. if provider.get('iconpath'):
  1204. newargs.append('-q={}'.format(provider['iconpath']))
  1205. if provider.get('xcludesref') and provider['xcludesref'] == '1':
  1206. newargs.append('-xs')
  1207. if provider.get('bouquettop') and provider['bouquettop'] == '1':
  1208. newargs.append('-bt')
  1209. if provider.get('bouquetdownload') and provider['bouquetdownload'] == '1':
  1210. newargs.append('-bd')
  1211. if provider.get('bouqueturl'):
  1212. newargs.append('-b={}'.format(provider['bouqueturl'].replace('USERNAME', urllib.quote_plus(username)).replace('PASSWORD', urllib.quote_plus(password))))
  1213. # Re-call ourselves
  1214. main(newargs)
  1215.  
  1216.  
  1217. def main(argv=None): # IGNORE:C0111
  1218. # Command line options.
  1219. if argv is None:
  1220. argv = sys.argv
  1221. else:
  1222. sys.argv.extend(argv)
  1223. program_name = os.path.basename(sys.argv[0])
  1224. program_version = "v%s" % __version__
  1225. program_build_date = str(__updated__)
  1226. program_version_message = '%(prog)s {} ({})'.format(program_version, program_build_date)
  1227. program_shortdesc = __doc__.split("\n")[1]
  1228. program_license = """{}
  1229.  
  1230. Copyright 2017. All rights reserved.
  1231. Created on {}.
  1232. Licensed under GNU GENERAL PUBLIC LICENSE version 3
  1233. Distributed on an "AS IS" basis without warranties
  1234. or conditions of any kind, either express or implied.
  1235.  
  1236. USAGE
  1237. """.format(program_shortdesc, str(__date__))
  1238.  
  1239. try:
  1240. # Setup argument parser
  1241. parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter)
  1242. # URL Based Setup
  1243. urlgroup = parser.add_argument_group('URL Based Setup')
  1244. urlgroup.add_argument('-m', '--m3uurl', dest='m3uurl', action='store',
  1245. help='URL to download m3u data from (required)')
  1246. urlgroup.add_argument('-e', '--epgurl', dest='epgurl', action='store',
  1247. help='URL source for XML TV epg data sources')
  1248. # Provider based setup
  1249. providergroup = parser.add_argument_group('Provider Based Setup')
  1250. providergroup.add_argument('-n', '--providername', dest='providername', action='store',
  1251. help='Host IPTV provider name (e.g. FAB/EPIC) (required)')
  1252. providergroup.add_argument('-u', '--username', dest='username', action='store',
  1253. help='Your IPTV username (required)')
  1254. providergroup.add_argument('-p', '--password', dest='password', action='store',
  1255. help='Your IPTV password (required)')
  1256. # Options
  1257. parser.add_argument('-i', '--iptvtypes', dest='iptvtypes', action='store_true',
  1258. help='Treat all stream references as IPTV stream type. (required for some enigma boxes)')
  1259. parser.add_argument('-sttv', '--streamtype_tv', dest='sttv', action='store', type=int,
  1260. help='Stream type for TV (e.g. 1, 4097, 5001 or 5002) overrides iptvtypes')
  1261. parser.add_argument('-stvod', '--streamtype_vod', dest='stvod', action='store', type=int,
  1262. help='Stream type for VOD (e.g. 4097, 5001 or 5002) overrides iptvtypes')
  1263. parser.add_argument('-M', '--multivod', dest='multivod', action='store_true',
  1264. help='Create multiple VOD bouquets rather single VOD bouquet')
  1265. parser.add_argument('-a', '--allbouquet', dest='allbouquet', action='store_true',
  1266. help='Create all channels bouquet')
  1267. parser.add_argument('-P', '--picons', dest='picons', action='store_true',
  1268. help='Automatically download of Picons, this option will slow the execution')
  1269. parser.add_argument('-q', '--iconpath', dest='iconpath', action='store',
  1270. help='Option path to store picons, if not supplied defaults to /usr/share/enigma2/picon/')
  1271. parser.add_argument('-xs', '--xcludesref', dest='xcludesref', action='store_true',
  1272. help='Disable service ref overriding from override.xml file')
  1273. parser.add_argument('-b', '--bouqueturl', dest='bouqueturl', action='store',
  1274. help='URL to download providers bouquet - to map custom service references')
  1275. parser.add_argument('-bd', '--bouquetdownload', dest='bouquetdownload', action='store_true',
  1276. help='Download providers bouquet (use default url) - to map custom service references')
  1277. parser.add_argument('-bt', '--bouquettop', dest='bouquettop', action='store_true',
  1278. help='Place IPTV bouquets at top')
  1279. parser.add_argument('-U', '--uninstall', dest='uninstall', action='store_true',
  1280. help='Uninstall all changes made by this script')
  1281. parser.add_argument('-V', '--version', action='version', version=program_version_message)
  1282.  
  1283. # Process arguments
  1284. args = parser.parse_args()
  1285. m3uurl = args.m3uurl
  1286. epgurl = args.epgurl
  1287. iptvtypes = args.iptvtypes
  1288. uninstall = args.uninstall
  1289. multivod = args.multivod
  1290. allbouquet = args.allbouquet
  1291. bouquet_url = args.bouqueturl
  1292. bouquet_download = args.bouquetdownload
  1293. picons = args.picons
  1294. iconpath = args.iconpath
  1295. xcludesref = args.xcludesref
  1296. bouquettop = args.bouquettop
  1297. provider = args.providername
  1298. username = args.username
  1299. password = args.password
  1300. sttv = args.sttv
  1301. stvod = args.stvod
  1302. # Set epg to rytec if nothing else provided
  1303. if epgurl is None:
  1304. epgurl = "http://www.vuplus-community.net/rytec/rytecxmltv-UK.gz"
  1305. # Set piconpath
  1306. if iconpath is None or TESTRUN == 1:
  1307. iconpath = PICONSPATH
  1308. if provider is None:
  1309. provider = "E2m3u2Bouquet"
  1310. # Check we have enough to proceed
  1311. if (m3uurl is None) and ((provider is None) or (username is None) or (password is None)) and uninstall is False:
  1312. print('\n********************************')
  1313. print('E2m3u2bouquet - Config based setup')
  1314. print('********************************\n')
  1315. configs = config()
  1316. if os.path.isfile(os.path.join(CFGPATH, 'config.xml')):
  1317. supplierslist = configs.readconfig(os.path.join(CFGPATH, 'config.xml'))
  1318. for supplier in supplierslist:
  1319. if supplierslist[supplier]['enabled'] == '1':
  1320. if supplierslist[supplier]['name'].startswith('Supplier Name'):
  1321. print("Please enter your details in the supplier config file in - {}".format(os.path.join(CFGPATH, 'config.xml')))
  1322. sys.exit(2)
  1323. else:
  1324. print('\n********************************')
  1325. print('Config based setup - {}'.format(supplierslist[supplier]['name']))
  1326. print('********************************\n')
  1327. configs.run_e2m3u2bouquet(supplierslist[supplier])
  1328. else:
  1329. print('\nSupplier: {} is disabled - skipping.........\n'.format(supplierslist[supplier]['name']))
  1330. sys.exit(0)
  1331. else:
  1332. configs.makeconfig(os.path.join(CFGPATH, 'config.xml'))
  1333. print('Please ensure correct command line options are passed to the program \n'
  1334. 'or populate the config file in {} \n'
  1335. 'for help use --help\n'.format(os.path.join(CFGPATH, 'config.xml')))
  1336. parser.print_usage()
  1337. sys.exit(1)
  1338.  
  1339.  
  1340. except KeyboardInterrupt:
  1341. ### handle keyboard interrupt ###
  1342. return 0
  1343.  
  1344. except Exception, e:
  1345. if DEBUG or TESTRUN:
  1346. raise e
  1347. indent = len(program_name) * " "
  1348. sys.stderr.write(program_name + ": " + repr(e) + "\n")
  1349. sys.stderr.write(indent + " for help use --help")
  1350. return 2
  1351.  
  1352. # # Core program logic starts here
  1353. urllib._urlopener = AppUrlOpener()
  1354. socket.setdefaulttimeout(30)
  1355. e2m3uSetup = IPTVSetup()
  1356. e2m3uSetup.display_welcome()
  1357. if uninstall:
  1358. # Clean up any existing files
  1359. e2m3uSetup.uninstaller()
  1360. # reload bouquets
  1361. e2m3uSetup.reload_bouquets()
  1362. print("Uninstall only, program exiting ...")
  1363. sys.exit(1) # Quit here if we just want to uninstall
  1364. else:
  1365. # create config folder if it doesn't exist
  1366. try:
  1367. os.makedirs(CFGPATH)
  1368. except OSError, e: # race condition guard
  1369. if e.errno != errno.EEXIST:
  1370. raise
  1371.  
  1372. # Work out provider based setup if that's what we have
  1373. if (provider is not None) and m3uurl is None and ((username is not None) or (password is not None)):
  1374. providersfile = e2m3uSetup.download_providers(PROVIDERSURL)
  1375. e2m3uSetup.read_providers(providersfile)
  1376. m3uurl, epgurl, supported_providers = e2m3uSetup.process_provider(
  1377. provider, username, password)
  1378. if m3uurl == "NOTFOUND":
  1379. print("----ERROR----")
  1380. print("Provider not found, supported providers = " + supported_providers)
  1381. sys(exit(1))
  1382.  
  1383. # If no username or password supplied extract them from m3uurl
  1384. if (username is None) or (password is None):
  1385. username, password = e2m3uSetup.extract_user_details_from_url(m3uurl)
  1386.  
  1387. # get default provider bouquet download url if bouquet download set and no bouquet url given
  1388. if bouquet_download and not bouquet_url:
  1389. # set bouquet_url to default url
  1390. pos = m3uurl.find('get.php')
  1391. if pos != -1:
  1392. bouquet_url = m3uurl[0:pos + 7] + '?username={}&password={}&type=dreambox&output=ts'.format(
  1393. urllib.quote_plus(username), urllib.quote_plus(password))
  1394. # Download panel bouquet
  1395. panel_bouquet = None
  1396. if bouquet_url:
  1397. panel_bouquet_file = e2m3uSetup.download_bouquet(bouquet_url)
  1398. panel_bouquet = e2m3uSetup.parse_panel_bouquet(panel_bouquet_file)
  1399. # Download m3u
  1400. m3ufile = e2m3uSetup.download_m3u(m3uurl)
  1401. # parse m3u file
  1402. categoryorder, category_options, dictchannels = e2m3uSetup.parse_m3u(m3ufile, iptvtypes, sttv, stvod, panel_bouquet,
  1403. xcludesref, provider)
  1404. list_xmltv_sources = e2m3uSetup.parse_map_xmltvsources_xml(provider)
  1405. # save xml mapping - should be after m3u parsing
  1406. e2m3uSetup.save_map_xml(categoryorder, category_options, dictchannels, list_xmltv_sources, provider)
  1407.  
  1408. # Download picons
  1409. if picons:
  1410. e2m3uSetup.download_picons(dictchannels, iconpath)
  1411. # Create bouquet files
  1412. e2m3uSetup.create_bouquets(categoryorder, category_options, dictchannels, multivod, allbouquet, bouquettop, provider)
  1413. # Now create custom channels for each bouquet
  1414. print("\n----Creating EPG-Importer config ----")
  1415. e2m3uSetup.create_epgimporter_config(categoryorder, category_options, dictchannels, list_xmltv_sources, epgurl, provider)
  1416. print("EPG-Importer config created...")
  1417. # reload bouquets
  1418. e2m3uSetup.reload_bouquets()
  1419. print("\n********************************")
  1420. print("Engima2 IPTV bouquets created ! ")
  1421. print("********************************")
  1422. print("\nTo enable EPG data")
  1423. print("Please open EPG-Importer plugin.. ")
  1424. print("Select sources and enable the new IPTV source")
  1425. print("(will be listed as {} under 'IPTV Bouquet Maker - E2m3u2bouquet')".format(provider))
  1426. print("Save the selected sources, press yellow button to start manual import")
  1427. print("You can then set EPG-Importer to automatically import the EPG every day")
  1428.  
  1429. if __name__ == "__main__":
  1430. if TESTRUN:
  1431. EPGIMPORTPATH = "H:/Satelite Stuff/epgimport/"
  1432. ENIGMAPATH = "H:/Satelite Stuff/enigma2/"
  1433. PICONSPATH = "H:/Satelite Stuff/picons/"
  1434. CFGPATH = os.path.join(ENIGMAPATH, 'e2m3u2bouquet/')
  1435. sys.exit(main())
  1436. else:
  1437. IMPORTED = True
Add Comment
Please, Sign In to add comment