Best Python code snippet using avocado_python
tvh_epg.py
Source:tvh_epg.py
1#!/usr/bin/env python32# -*- coding: utf-8 -*-3'''4a really really basic EPG for TVHeadend5Main information:6 https://github.com/speculatrix/tvh_epg7Chromecast information:8 https://github.com/speculatrix/tvh_epg/blob/master/CHROMECAST.md9 note that not only do you need to download pychromecast from10 https://github.com/balloob/pychromecast11 and put it into a subdirectory alongside this cgi-bin, but you12 need to pip install zeroconf to get it to work.13'''14import cgi15import cgitb16import codecs17import configparser18import datetime19import hashlib20import json21import os22import stat23import sys24import time25import collections26import socket27import urllib28import requests29from requests.auth import HTTPDigestAuth30# chromecast support is optional, and since it needs manually installing31# have to not die if it can't be found32try:33 import pychromecast34 CAST_SUPPORT = True35except ImportError:36 CAST_SUPPORT = False37# pylint:disable=global-statement38# requires making code less readable:39# pylint:disable=too-many-branches40# pylint:disable=too-many-lines41# pylint:disable=too-many-locals42# pylint:disable=too-many-nested-blocks43# pylint:disable=too-many-statements44# broken in pylint3:45# pylint:disable=global-variable-not-assigned46##########################################################################################47URL_GITHUB_HASH_SELF = 'https://api.github.com/repos/speculatrix/tvh_epg/contents/tvh_epg.py'48TS_URL_CHN = 'api/channel/grid'49TS_URL_CTG = 'api/channeltag/grid'50TS_URL_CBE = 'api/dvr/entry/create_by_event'51TS_URL_DCG = 'api/dvr/config/grid'52TS_URL_DEG = 'api/dvr/entry/grid_finished'53TS_URL_EPG = 'api/epg/events/grid'54TS_URL_STC = 'api/status/connections'55TS_URL_STI = 'api/status/inputs'56TS_URL_SVI = 'api/serverinfo'57TS_URL_STR = 'stream/channel'58TS_URL_DVF = 'dvrfile/'59CGI_PARAMS = cgi.FieldStorage()60EPG = 'epg'61SECS_P_PIXEL = 10 # how many seconds per pixel62#MAX_FUTURE = 28800 # 8 hours - how far into the future to show a prog63#MAX_FUTURE = 18000 # 5 hours - how far into the future to show a prog64MAX_FUTURE = 14400 # 4 hours - how far into the future to show a prog65#MAX_PAST = 900 # 15 mins - how much of past programs to show66MAX_PAST = 720 # 12 mins - how much of past programs to show67#MAX_PAST = 400 # 15 mins - how much of past programs to show68CHAN_TABLE_COLUMNS = 469INPUT_FORM_ESCAPE_TABLE = {70 '"': """,71# "'": "’", # thanks DavidG72 "&": "&",73}74URL_ESCAPE_TABLE = {75 " ": "%20",76}77TD_EMPTY_CELL = '<td> </td>'78# state files, queues, logs and so on are stored in this directory79CONTROL_DIR = '/var/lib/tvh_epg'80# the settings file is stored in the control directory81SETTINGS_FILE = 'tvh_epg_settings.ini'82SETTINGS_SECTION = 'user'83CHAN_COLUMNS = 'channel_column_count'84MAX_CHANS = 'max_chans'85SH_LOGO = 'sh_ch_logo'86TS_AUTH = 'auth_plain_digest'87TS_PASS = 'ts_pass'88TS_PAUTH = 'ts_pauth'89TS_PROF_STRM = 'profile_strm'90TS_PROF_CAST = 'profile_chromecasting'91TS_URL = 'ts_url'92TS_URL_ICONS = 'ts_url_icons'93TS_URL_CAST = 'ts_url_icon_cast'94TS_USER = 'ts_user'95TITLE = 'title'96DFLT = 'default'97TYPE = 'type'98LOCAL_ICON_DIR = 'local_icon_dir'99ICON_WIDTH = 'forced_icon_width'100ICON_HEIGHT = 'forced_icon_height'101BG_COL_PAGE = 'bg_col_page'102BG_COL_INPUT = 'bg_col_input'103BG_COL_DEF_PAGE = 'f4f4f4'104BG_COL_DEF_INPUT = 'f8f8f8'105# default values of the settings when being created106SETTINGS_DEFAULTS = {107 TS_URL: {108 TITLE: 'URL of TV Headend Server',109 DFLT: 'http://tvh.example.com:9981',110 TYPE: 'text',111 },112 TS_URL_ICONS: {113 TITLE: 'URL to picons',114 DFLT: 'http://tvh.example.com/TVLogos/',115 TYPE: 'text',116 },117 LOCAL_ICON_DIR: {118 TITLE: 'Local icon directory, if set, checks icon file exists<br>(to avoid broken images)',119 DFLT: '/home/hts/TVLogos/',120 TYPE: 'text',121 },122 TS_URL_CAST: {123 TITLE: 'URL to chromecast icon',124 DFLT: 'http://tvh.example.com/ic_cast_connected_white_24dp.png',125 TYPE: 'text',126 },127 TS_USER: {128 TITLE: 'Username on TVH server',129 DFLT: TS_USER,130 TYPE: 'text',131 },132 TS_PASS: {133 TITLE: 'Password on TVH server',134 DFLT: TS_PASS,135 TYPE: 'password',136 },137 TS_AUTH: {138 TITLE: 'Authentication, plain or digest',139 DFLT: 'plain',140 TYPE: 'text'141 },142 TS_PAUTH: {143 TITLE: 'Persistent Auth Token',144 DFLT: TS_PAUTH,145 TYPE: 'password',146 },147 TS_PROF_STRM: {148 TITLE: 'profile for streaming',149 DFLT: 'default',150 TYPE: 'text',151 },152 TS_PROF_CAST: {153 TITLE: 'profile for chromecasting',154 DFLT: 'chromecast',155 TYPE: 'text',156 },157 SH_LOGO: {158 TITLE: 'Show Channel Logos',159 DFLT: '0',160 TYPE: 'text',161 },162 MAX_CHANS: {163 TITLE: 'Maximum Number Of Channels',164 DFLT: '500',165 TYPE: 'text',166 },167 CHAN_COLUMNS: {168 TITLE: 'Columns In Channel Table',169 DFLT: '4',170 TYPE: 'text',171 },172 ICON_HEIGHT: {173 TITLE: 'Force icon height to this, 0 for off',174 DFLT: '64',175 TYPE: 'text',176 },177 ICON_WIDTH: {178 TITLE: 'Force icon width to this, 0 for off',179 DFLT: '80',180 TYPE: 'text',181 },182 BG_COL_PAGE: {183 TITLE: 'page background colour',184 DFLT: BG_COL_DEF_PAGE,185 TYPE: 'text',186 },187 BG_COL_INPUT: {188 TITLE: 'input field background colour',189 DFLT: BG_COL_DEF_INPUT,190 TYPE: 'text',191 },192}193DOCROOT_DEFAULT = '/home/hts'194##########################################################################################195def check_load_config_file():196# pylint:disable=too-many-return-statements197 '''check there's a config file which is writable;198 returns 0 if OK, -1 if the rest of the page should be aborted,199 > 0 to trigger rendering of the settings page'''200 global CONFIG_FILE_NAME201 global MY_SETTINGS202 # who am i?203 my_euser_id = os.geteuid()204 my_egroup_id = os.getegid()205 config_bad = 1206 ################################################207 # verify that CONTROL_DIR exists and is writable208 try:209 cdir_stat = os.stat(CONTROL_DIR)210 except OSError:211 error_text = f'''Error, directory "{CONTROL_DIR }" doesn\'t appear to exist.212Please do the following - needs root:213\tsudo mkdir "{ CONTROL_DIR }" && sudo chgrp { str(my_egroup_id) } "{ CONTROL_DIR }" && sudo chmod g+ws "{ CONTROL_DIR }"'''214 config_bad = -1215 return (config_bad,216 error_text) # error so severe, no point in continuing217 # owned by me and writable by me, or same group as me and writable through that group?218 if ((cdir_stat.st_uid == my_euser_id and219 (cdir_stat.st_mode & stat.S_IWUSR) != 0)220 or (cdir_stat.st_gid == my_egroup_id and221 (cdir_stat.st_mode & stat.S_IWGRP) != 0)):222 #print 'OK, %s exists and is writable' % CONTROL_DIR223 config_bad = 0224 else:225 error_text = '''Error, won\'t be able to write to directory "%s".226Please do the following:227\tsudo chgrp %s "%s" && sudo chmod g+ws "%s"''' \228% (CONTROL_DIR, str(my_egroup_id), CONTROL_DIR, CONTROL_DIR, )229 return (-1, error_text) # error so severe, no point in continuing230 ########231 # verify the settings file exists and is writable232 if not os.path.isfile(CONFIG_FILE_NAME):233 error_text = '''Error, can\'t open "%s" for reading.234Please do the following - needs root:235\tsudo touch "%s" && sudo chgrp %s "%s" && sudo chmod g+w "%s"''' \236% (CONFIG_FILE_NAME, CONFIG_FILE_NAME, str(my_egroup_id), CONFIG_FILE_NAME, CONFIG_FILE_NAME)237 return (-1, error_text)238 # owned by me and writable by me, or same group as me and writable through that group?239 config_stat = os.stat(CONFIG_FILE_NAME)240 if ((config_stat.st_uid == my_euser_id and241 (config_stat.st_mode & stat.S_IWUSR) != 0)242 or (config_stat.st_gid == my_egroup_id and243 (config_stat.st_mode & stat.S_IWGRP) != 0)):244 config_bad = 0245 else:246 error_text = '''Error, won\'t be able to write to file "%s"247Please do the following - needs root:248\tsudo chgrp %s "%s" && sudo chmod g+w %s''' \249% (CONFIG_FILE_NAME, CONFIG_FILE_NAME, my_egroup_id, CONFIG_FILE_NAME, )250 return (-1, error_text)251 # file is zero bytes?252 if config_stat.st_size == 0:253 error_text = 'Config file is empty, please go to settings and submit to save\n'254 return (1, error_text)255 if not MY_SETTINGS.read(CONFIG_FILE_NAME):256 error_text = ('<b>Error</b>, failed to open and read config file "%s"' \257 % (CONFIG_FILE_NAME, ))258 return (-1, error_text)259 return (0, 'OK')260##########################################################################################261def get_github_hash_self():262 """calculates the git hash of the version of this script in github"""263 gh_resp = requests.get(URL_GITHUB_HASH_SELF)264 gh_json = gh_resp.json()265 return gh_json['sha']266##########################################################################################267def get_githash_self():268 """calculates the git hash of the running script"""269 # stat this file270 fullfile_name = __file__271 fullfile_stat = os.stat(fullfile_name)272 # read this entire file into memory273 fullfile_content = ''274 with open(fullfile_name, 'rb') as fullfile_fh:275 fullfile_content = fullfile_fh.read()276 # do what "git hash-object" does277 sha_obj = hashlib.sha1()278 sha_obj.update(b'blob %d\0' % fullfile_stat.st_size)279 sha_obj.update(fullfile_content)280 return sha_obj.hexdigest()281##########################################################################################282def epoch_to_human_duration(epoch_time):283 '''takes numeric sec since unix epoch and returns humanly readable time'''284 #return time.asctime(time.localtime(epoch_time))285 human_dt = datetime.datetime.fromtimestamp(epoch_time)286 return human_dt.strftime("%H:%M")287##########################################################################################288def epoch_to_human_date(epoch_time):289 '''takes numeric sec since unix epoch and returns humanly readable time'''290 #return time.asctime(time.localtime(epoch_time))291 human_dt = datetime.datetime.fromtimestamp(epoch_time)292 return human_dt.strftime("%d-%m-%Y %H:%M")293##########################################################################################294#def load_channel_dict_from_cache():295# '''load channel dict from cache file - FIXME'''296#297##########################################################################################298#def save_channel_dict_to_cache():299# '''saves channel dict to cache file - FIXME'''300#301##########################################################################################302def get_channeltag_grid():303 '''gets the channeltag/grid values'''304 global MY_SETTINGS305 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)306 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)307 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)308 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)309 ts_query = f'{ ts_url }/{ TS_URL_CTG }'310 if ts_auth == 'plain':311 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))312 else:313 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))314 print(f'<!-- get_channeltag_grid URL { ts_query } -->')315 if ts_response.status_code != 200:316 print(f'<pre>Error code { ts_response.status_code }\n{ ts_response.content }</pre>')317 return {}318 ts_json = json.loads(ts_response.text, strict=False)319 #print('<pre>%s</pre>' % json.dumps(ts_json, sort_keys=True, \320 # indent=4, separators=(',', ': ')) )321 return ts_json322##########################################################################################323def get_channel_dict():324 '''gets the channel listing and generats an ordered dict by name'''325 global MY_SETTINGS326 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)327 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)328 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)329 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)330 ts_max_ch = MY_SETTINGS.get(SETTINGS_SECTION, MAX_CHANS)331 ts_query = f'{ ts_url }/{ TS_URL_CHN }?limit={ ts_max_ch }'332 if ts_auth == 'plain':333 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))334 else:335 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))336 print(f'<!-- get_channel_dict URL { ts_query } -->')337 if ts_response.status_code != 200:338 print(f'<pre>Error code { ts_response.status_code }\n{ ts_response.content }</pre>')339 return {}340 ts_text = ts_response.text341 #print(f'<pre>Extreme Debug!\n\n{ ts_text }\n<pre>')342 ts_json = json.loads(ts_text, strict=False)343 #print('<pre>%s</pre>' % json.dumps(ts_json, sort_keys=True, \344 # indent=4, separators=(',', ': ')) )345 channel_map = {} # full channel info346 channel_list = [] # build a list of channel names347 ordered_channel_map = collections.OrderedDict()348 if 'entries' in ts_json:349 # grab all channel info350 name_unknown = 0351 number_unknown = -1352 for entry in ts_json['entries']:353 # start building a dict with channel name as key354 if 'name' in entry:355 channel_name = entry['name']356 else:357 channel_name = 'unknown ' + str(name_unknown)358 name_unknown += 1359 channel_list.append(channel_name)360 if channel_name not in channel_map:361 channel_map[channel_name] = {}362 # store the channel specific info363 ch_map = channel_map[channel_name]364 if 'tags' in entry:365 ch_map['tags'] = entry['tags']366 if 'number' in entry:367 ch_map['number'] = entry['number']368 else:369 ch_map['number'] = number_unknown370 name_unknown -= 1371 ch_map['uuid'] = entry['uuid']372 if 'icon_public_url' in entry:373 ch_map['icon_public_url'] = entry['icon_public_url']374 channel_list_sorted = sorted(channel_list, key=lambda s: s.casefold())375 # case insensitive sort of channel list376 for chan in channel_list_sorted:377 # ... produces an ordered dict378 #print('adding %s<br>' % (chan, ))379 ordered_channel_map[chan] = channel_map[chan]380 return ordered_channel_map381##########################################################################################382def get_dvr_config_grid():383 '''gets the dvr/config/grid dict'''384 global MY_SETTINGS385 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)386 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)387 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)388 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)389 ts_query = '%s/%s' % (390 ts_url,391 TS_URL_DCG,392 )393 if ts_auth == 'plain':394 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))395 else:396 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))397 print(f'<!-- get_dvr_config_grid URL { ts_query } -->')398 ts_json = json.loads(ts_response.text, strict=False)399 #print('<pre>%s</pre>' % json.dumps(ts_json, sort_keys=True, \400 # indent=4, separators=(',', ': ')) )401 return ts_json402##########################################################################################403def html_page_footer():404 '''no surprises'''405 print('''</body>406</html>''')407##########################################################################################408def input_form_escape(text):409 """escape special characters into html input forms"""410 return "".join(INPUT_FORM_ESCAPE_TABLE.get(c, c) for c in text)411##########################################################################################412def page_channel_table():413 '''prints the channel table to stdout'''414 global MY_SETTINGS415 if 'tag' in CGI_PARAMS:416 p_tag = CGI_PARAMS.getlist('tag')417 else:418 p_tag = []419 print('<h1>Channels</h1>')420 channel_dict = get_channel_dict()421 channel_tag = get_channeltag_grid()422 cdl = len(channel_dict)423 print(f'''<p><b>Channel count: { cdl }</b></p>424<p>Maximum number of channels viewable in settings is { MY_SETTINGS.get(SETTINGS_SECTION, MAX_CHANS) }425<br><br>426Note, the channel links are the streams; to play, save the m3u and open in a427player like VLC, you can also you can drag and drop the link into a VLC window.428<br><br>429The ↥ character means you can hover the mouse and see the secondary title of the programme.430</p>''')431 # channel labels432 if cdl:433 print(' <form method="get" action="">')434 print("<b>Tag filters</b>:")435 for tag in channel_tag['entries']:436 if tag['uuid'] in p_tag:437 checked = ' checked'438 else:439 checked = ''440 print(f'<input type="checkbox" name="tag" value="{ tag["uuid"] }" { checked }>{ tag["name"] } ')441 print(''' <input type="hidden" name="page" value="channels">442 <input type="submit" name="apply" value="apply">443 </form>''')444 #####################################################################445 # channel table446 # index required to make table rows447 print(''' <table>448 <tr>''')449 for _column_num in range(0, int(MY_SETTINGS.get(SETTINGS_SECTION, CHAN_COLUMNS))):450 if int(MY_SETTINGS.get(SETTINGS_SECTION, SH_LOGO)) != 0:451 print(' <th>Channel Logo</th>')452 print(''' <th>Channel Name</th>453 <td> </td>''')454 print(' </tr>')455 #ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)456 icon_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL_ICONS)457 icon_width = MY_SETTINGS.get(SETTINGS_SECTION, ICON_WIDTH)458 icon_height = MY_SETTINGS.get(SETTINGS_SECTION, ICON_HEIGHT)459 chan_idx = 0460 for chan_name in channel_dict:461 chan = channel_dict[chan_name]462 # check channel isn't filtered out by tags463 show_channel = 0464 if 'tags' not in chan or len(p_tag) == 0:465 show_channel = 1466 else:467 for tag in p_tag:468 if tag in chan['tags']:469 show_channel = 1470 if show_channel:471 if chan_idx % (int(MY_SETTINGS.get(SETTINGS_SECTION, CHAN_COLUMNS))) == 0:472 print(' <tr>')473 if int(MY_SETTINGS.get(SETTINGS_SECTION, SH_LOGO)) != 0:474 if 'icon_public_url' in channel_dict[chan_name]:475 # chop +1 channel names for icons476 if chan_name[-2:] == '+1':477 chan_name_ref = chan_name[:-2]478 else:479 chan_name_ref = chan_name480 # it might be possible to skip broken picons if they481 # are on the same server and we know where they are482 skip_icon = False483 if MY_SETTINGS.get(SETTINGS_SECTION, LOCAL_ICON_DIR) != '':484 icon_file_name = f'{ MY_SETTINGS.get(SETTINGS_SECTION, LOCAL_ICON_DIR) }/{ chan_name_ref }.png'485 if not os.path.exists(icon_file_name):486 skip_icon = True487 print(' <td width="100px" align="right" class="chan_icon">', end='')488 if skip_icon:489 print(' ', end='')490 else:491 chan_img_url = f'{ icon_url }/{ input_form_escape(chan_name_ref) }.png'492 print(f'<img alt="channel icon" src="{ chan_img_url }"', end='')493 if icon_width not in ('', '0'):494 print(f' width="{ icon_width }"', end='')495 if icon_height not in ('', '0'):496 print(f' height="{ icon_height }"', end='')497 print('>', end='')498 print('</td>')499 else:500 print(' <td> </td>')501 play_url = f'?page=m3u&uuid=/{ TS_URL_STR }/{chan["uuid"] }'502 print(f' <td width="100px" align="right"><a title="watch live" href="{ play_url }" '503 f'download="tvheadend.m3u">{ input_form_escape(chan_name) }</a> ({ chan["number"] })' )504 if CAST_SUPPORT:505 print(' <br>\n <a title="chromecast this" href="?page=chromecast&'506 f'uri=/{ TS_URL_STR }/{ chan["uuid"] }">'507 f'<img src="{ MY_SETTINGS.get(SETTINGS_SECTION, TS_URL_CAST) }" alt="chromecast icon"></a>'508 , end=''509 )510 print('</td>\n')511 chan_idx += 1512 # if we're about to start a new row, close the row513 if chan_idx % (int(MY_SETTINGS.get(SETTINGS_SECTION, CHAN_COLUMNS))) == 0:514 print(' </tr>')515 else:516 print(' <td> </td>')517 # don't leave a row hanging518 if chan_idx % (int(MY_SETTINGS.get(SETTINGS_SECTION, CHAN_COLUMNS))) != 0:519 print(' </tr>')520 print(' </table>')521##########################################################################################522def page_chromecast(p_uri, p_cast_device):523 '''chromecast "pop up"524 uri is /dvrfile/aaaaaaa...525 or /stream/channel/aaaaaa526 and missing the httpX://hostname:port bit527 '''528 global MY_SETTINGS529 # start the scanning and hope it'll be done soon530 chromecasts = pychromecast.get_chromecasts(1, 0, 15) # tries, retry_wait, timeout531 if isinstance(chromecasts, tuple):532 chromecasts, browser = chromecasts533 if TS_PROF_CAST in MY_SETTINGS[SETTINGS_SECTION] and MY_SETTINGS.get(SETTINGS_SECTION, TS_PROF_CAST) != '':534 ts_profile = f'?profile={ MY_SETTINGS.get(SETTINGS_SECTION, TS_PROF_CAST) }'535 else:536 ts_profile = ''537 # split the TVH server URL up so we can get its IP address538 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)539 try:540 ts_url_parsed = urllib.parse.urlparse(ts_url)541 except urllib.error.URLError as url_excpt:542 ##print(str(url_excpt))543 print(f'<p>Error parsing { str(url_excpt) }</p>')544 return545 print(f'<p>hostname { ts_url_parsed.hostname }, netloc { ts_url_parsed.netloc }</p>')546 #print('<br><br>Debug: uri "%s"<br>' % (p_uri, ))547 # now for abominable hacks548 ts_ip = socket.gethostbyname(ts_url_parsed.hostname)549 if TS_URL_DVF in p_uri:550 # recordings need to get a username/password551 full_url = '%s://%s:%s@%s:%s/%s%s' \552 % (ts_url_parsed.scheme,553 MY_SETTINGS.get(SETTINGS_SECTION, TS_USER),554 MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS),555 ts_ip,556 ts_url_parsed.port,557 p_uri,558 ts_profile,559 )560 else:561 # live streams use persistent auth562 full_url = '%s://%s:%s%s%s&AUTH=%s' \563 % (ts_url_parsed.scheme,564 ts_ip, ts_url_parsed.port,565 p_uri,566 ts_profile,567 MY_SETTINGS.get(SETTINGS_SECTION, TS_PAUTH),568 )569 #print('fullurl is "%s"<br>' % full_url)570 print("<p>Please be patient, scanning for chromecast devices can take up to ten seconds</p>")571 pychromecast.discovery.stop_discovery(browser)572 ####573 # user must choose a device to cast to574 if p_cast_device == '':575 print('<form method="get" action="">\n'576 '<input type="hidden" name="page" value="chromecast">\n'577 '<input type="hidden" name="uri" value="%s">' % (p_uri, ))578 print('Select device')579 print('<select name="cast_device">')580 for cast_dev in chromecasts:581 print('<option value="%s">%s</option>' % \582 (cast_dev.device.friendly_name,583 cast_dev.device.friendly_name,584 ))585 print('''</select>586<input type="submit" name="Choose Device" value="Choose Device">587</form>''')588 return589 ####590 # find the cast device which user chose from friendly name591 print('<br>Debug, finding device with friendly name "%s"<br>' % (p_cast_device, ))592 cast = None593 for cast_dev in chromecasts:594 if cast_dev.device.friendly_name == p_cast_device:595 #if cast_dev.friendly_name == p_cast_device:596 cast = cast_dev597 if cast is None:598 print('Error, couldn\'t find the cast device<br>')599 return600 ####601 # can now actually get the chromecast to do the streaming602 #return # stop the actual casting603 cast.wait()604 print('<pre')605 print(cast.device)606 print(cast.status)607 c_m_c = cast.media_controller608 c_m_c.play_media(full_url, 'video/mp4')609 c_m_c.block_until_active()610 print(c_m_c.status)611 c_m_c.pause()612 time.sleep(5)613 c_m_c.play()614 print('</pre')615 print('<p>chromecast page completed</p>')616 pychromecast.discovery.stop_discovery(browser)617##########################################################################################618def page_list_chans_epg(show_epg):619 '''prints the cnannel list with/out EPG to stdout'''620 global MY_SETTINGS621 if 'tag' in CGI_PARAMS:622 p_tag = CGI_PARAMS.getlist('tag')623 else:624 p_tag = []625 print('<h1>EPG</h1>')626 epoch_time = time.time()627 channel_dict = get_channel_dict()628 channel_tag = get_channeltag_grid()629 cdl = len(channel_dict)630 if cdl:631 print("<p><b>Tag filters</b>:</p>")632 print('<form method="get" action="">')633 for tag in channel_tag['entries']:634 if tag['uuid'] in p_tag:635 checked = ' checked'636 else:637 checked = ''638 print(f' <div><input type="checkbox" name="tag" value="{ tag["uuid"] }" { checked }>{ tag["name"] } </div>' )639 print(''' <input type="hidden" name="page" value="epg">640 <input type="submit" name="apply" value="apply">641 </form>642''')643#</p>644 print(f'''<p><b>Channel count: { cdl }</b></p>645<p>Maximum number of channels viewable in settings is { MY_SETTINGS.get(SETTINGS_SECTION, MAX_CHANS) }646<br><br>647Note, the channel links are the streams; to play, save the m3u and open in a648player like VLC, you can also you can drag and drop the link into a VLC window.649<br><br>650The ↥ character means you can hover the mouse and see the secondary title of the programme.651</p>''')652 # print the table/page header653 if show_epg:654 print(' <table width="1700px">\n <tr>')655 else:656 print(' <table>\n <tr>')657 if int(MY_SETTINGS.get(SETTINGS_SECTION, SH_LOGO)) != 0:658 print(' <th width="100px">Logo</th>')659 print(' <th>Channel</th>')660 if show_epg:661 print(' <th> </th>')662 print(' </tr>')663 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)664 icon_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL_ICONS)665 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)666 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)667 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)668 icon_width = MY_SETTINGS.get(SETTINGS_SECTION, ICON_WIDTH)669 icon_height = MY_SETTINGS.get(SETTINGS_SECTION, ICON_HEIGHT)670 # iterate through the channel list by name671 for chan_name in channel_dict:672 chan = channel_dict[chan_name]673 show_channel = 0674 if 'tags' not in chan or len(p_tag) == 0:675 show_channel = 1676 else:677 for tag in p_tag:678 if tag in chan['tags']:679 show_channel = 1680 if show_channel:681 print(' <tr>')682 if int(MY_SETTINGS.get(SETTINGS_SECTION, SH_LOGO)) != 0:683 if 'icon_public_url' in chan:684 # chop +1 channel names for icons685 if chan_name[-2:] == '+1':686 chan_name_ref = chan_name[:-2]687 else:688 chan_name_ref = chan_name689 # it might be possible to skip broken picons if they690 # are on the same server and we know where they are691 skip_icon = False692 if MY_SETTINGS.get(SETTINGS_SECTION, LOCAL_ICON_DIR) != '':693 icon_file_name = f'{ MY_SETTINGS.get(SETTINGS_SECTION, LOCAL_ICON_DIR) }/{ chan_name_ref }.png'694 if not os.path.exists(icon_file_name):695 skip_icon = True696 print('<td width="100px" align="right" class="chan_icon">')697 if skip_icon:698 print(' ')699 else:700 chan_img_url = f'{ icon_url }/{ input_form_escape(chan_name_ref) }.png'701 print(f'<img src="{ chan_img_url }"', end='')702 if icon_width not in ('', '0'):703 print(f' width="{ icon_width }"', end='')704 if icon_height not in ('', '0'):705 print(f' height="{ icon_height }"', end='')706 print('>')707 print('</td>')708 else:709 print('<td> </td>')710 play_url = f'?page=m3u&uuid=/{ TS_URL_STR }/{ chan["uuid"] }'711 print(f' <td width="100px" align="right"><a title="watch live" href="{ play_url }" '712 f'download="tvheadend.m3u">{ input_form_escape(chan_name) }</a> ({ chan["number"] })' )713 if CAST_SUPPORT:714 print(' <br>\n <a title="chromecast this" href="?page=chromecast&'715 f'uri=/{ TS_URL_STR }/{ chan["uuid"] }">'716 f'<img src="{ MY_SETTINGS.get(SETTINGS_SECTION, TS_URL_CAST) }" alt="chromecast icon"></a>'717 )718 print('</td>')719 if show_epg:720 # grab the EPG data for the channel721 ts_query = f'{ ts_url }/{ TS_URL_EPG }?limit=10&channel={ chan["uuid"] }'722 print(f' <!-- channel EPG URL { ts_query } -->')723 if ts_auth == 'plain':724 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))725 else:726 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))727 print(f' <!-- requests.response code { ts_response.status_code, } -->')728 ts_text = ts_response.text729 #print('<td><pre>Extreme Debug!\n\n%s\n<pre></td>' % (ts_text,))730 ts_json = json.loads(ts_text, strict=False)731 if len(ts_json['entries']):732 #chan[EPG] = ts_json['entries']733 print(734 ' <td valign="top" nowrap width="1600px">\n <div class="epg_row">'735 )736 #current_left_time = MAX_PAST / SECS_P_PIXEL # starts at zero secs737 current_left_time = 0738 entry_num = 0739 for entry in ts_json['entries']:740 entry_num += 1741 time_start = int(entry['start'])742 time_stop = int(entry['stop'])743 if entry_num == 9999:744 time_start += 600 # fake a gap745 time_stop -= 600 # fake a gap746 duration = time_stop - time_start747 #print(f'<div>epoch_time { epoch_time }<br>time_start { time_start }'748 # f'<br>time stop { time_stop }<br>duration { duration }</div>')749 # prevent past programs from showing750 if time_stop <= epoch_time:751 print('<div>past program</div>')752 continue753 # prevent far future programs making page too wide by ending the row754 if time_start - epoch_time >= MAX_FUTURE:755 continue756 # prevent overly long program making page too wide by narrowing757 #if time_stop - epoch_time >= MAX_FUTURE:758 # time_stop = epoch_time + MAX_FUTURE759 # duration = time_stop - time_start760 title = entry['title'] if 'title' in entry else '</i>Untitled</i>'761 subtitle = entry['subtitle'] if 'subtitle' in entry else ''762 time_until = time_start - epoch_time763 time_used = epoch_time - time_start764 time_left = time_stop - epoch_time765 #print(f'<div>duration { duration },<br>time used { time_used },<br>time_left { time_left }</div>')766 if time_until > current_left_time:767 # gap until next program768 box_width = (time_until - current_left_time) / SECS_P_PIXEL769 current_left_time = time_until770 print(' <div class="epg_none" style="width: '771 '%dpx; max-width: %dpx">GAP</div>' % (772 box_width,773 box_width,774 ), )775 # print the boxes containing each program776 if time_used > 0: # playing item777 time_used = MAX_PAST # used time is a fixed width778 time_start = epoch_time - MAX_PAST779 duration = time_stop - time_start780 # make box narrower, not interested in distant past781 #if time_used > MAX_PAST:782 #time_used = MAX_PAST # used time is a fixed width783 #duration = time_stop - time_start784 #print(f'<div>duration { duration },<br>time used { time_used },<br>time_left { time_left }</div>')785 #time_used = 10786 box_width = duration / SECS_P_PIXEL787 current_left_time += duration788 # box for things already started789 print(' <div class="epg_now" style="width: '790 f'{ box_width }px; max-width: { box_width }px">'791 , end='')792 else: # future item793 time_left = duration794 box_width = duration / SECS_P_PIXEL795 current_left_time += duration796 print(' <div class="epg_next" style="width: '797 f'{ box_width }px; max-width: { box_width }px">'798 , end='')799 # print the programme details800 #record_this = (f'<a title="record this" href="?page=record&event_id={ entry["eventId"] }"'801 # ' target="tvh_epg_record" width="320" height="320">'802 # '®</a> ')803 record_this = (f'<div class="record_this"><a title="record this" href="?page=record&event_id={ entry["eventId"] }"'804 ' target="tvh_epg_record">®</a> </div>')805 if subtitle != '':806 print(f'{ record_this }<div class="tooltip">'807 f'<b>↥{ input_form_escape(title) }</b><span class="tooltiptext">'808 f'<u><b>{ input_form_escape(title) }</b></u><br>{ input_form_escape(subtitle) }</span></div>'809 , end='')810 else:811 print(f'{ record_this }<b>{ input_form_escape(title) }</b>', end='')812 if time_used > 0:813 print(f'<br>end { epoch_to_human_duration(time_stop) }'814 f'<br>left { secs_to_human(time_left) }')815 else:816 print(f'<br> start { epoch_to_human_duration(time_start) }'817 f'<br>length { secs_to_human(duration) }')818 #print(f'<br>current_left_time will be { int(current_left_time) }<br>entry_num {entry_num}')819 print(' </div>')820 print(' <div style="clear:both; font-size:1px;"></div>\n </div>\n </td>')821 else:822 print(' <td> </td>')823 print(' </tr>')824 print('</table>')825##########################################################################################826def page_error(error_text):827 '''prints error page contents'''828 global MY_SETTINGS829 print('<h1>Error</h1>')830 print('<p>Something went wrong</p>')831 print(f'<pre>{ error_text }</pre>')832 print(f'<pre>settings sections: { MY_SETTINGS.sections() }</pre>')833##########################################################################################834def page_m3u(p_uuid):835 '''generates an m3u file to be played in e.g. vlc'''836 global MY_SETTINGS837 if TS_PROF_STRM in MY_SETTINGS[SETTINGS_SECTION] and MY_SETTINGS.get(SETTINGS_SECTION, TS_PROF_STRM) != '':838 ts_profile = f'?profile={ MY_SETTINGS.get(SETTINGS_SECTION, TS_PROF_STRM) }'839 else:840 ts_profile = ''841 if TS_PAUTH in MY_SETTINGS[SETTINGS_SECTION]:842 ts_pauth = f'&AUTH={ MY_SETTINGS.get(SETTINGS_SECTION, TS_PAUTH) }'843 else:844 ts_pauth = ''845 # split the TVH server URL up so we can get its IP address846 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)847 try:848 ts_url_parsed = urllib.parse.urlparse(ts_url)849 except urllib.error.URLError as url_excpt:850 ##print(str(url_excpt))851 print(f'<p>Error parsing { str(url_excpt) }</p>')852 return853 if TS_URL_DVF in p_uuid:854 # need very specific form of URL to allow user:pass auth to work855 full_url = '%s://%s:%s@%s:%s/%s%s' \856 % (ts_url_parsed.scheme,857 MY_SETTINGS.get(SETTINGS_SECTION, TS_USER),858 MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS),859 ts_url_parsed.hostname,860 ts_url_parsed.port,861 p_uuid,862 ts_profile,863 )864 else:865 # live streams use persistent auth866 full_url = '%s://%s:%s%s%s%s' \867 % (ts_url_parsed.scheme,868 ts_url_parsed.hostname,869 ts_url_parsed.port,870 p_uuid,871 ts_profile,872 ts_pauth,873 )874 print('#EXTM3U')875 print(full_url)876##########################################################################################877def page_record(p_event_id, p_profile):878 '''checks the recording param and generated DVR record'''879 global MY_SETTINGS880 print('<h1>Record Item</h1>')881 if p_profile == '':882 dcg_json = get_dvr_config_grid()883 if 'entries' in dcg_json:884 # if multiple profiles, ask the user885 if len(dcg_json['entries']) > 1:886 print('<form method="get">')887 print('<input type="hidden" name="page" value="record">')888 print('<select name="profile">')889 for entry in dcg_json['entries']:890 print('<option value=%s>profile: %s</p>' % (891 entry['uuid'],892 entry['profile'],893 ))894 print('</select>')895 print('<input type="hidden" name="event_id" value="%s">' %896 (p_event_id, ))897 print('<input type="submit" name="Go" value="Go">')898 print(899 '<input type="submit" name="Cancel" value="Cancel" onclick="self.close()">'900 )901 print('</form method="get">')902 # if only one profile, just select it903 else:904 p_profile = dcg_json['entries'][0]['profile']905 else:906 print('<p><b>Error<b>, there were no DCG profiles</p>')907 if p_profile != '':908 print('Generating DVR record...')909 print('<p>Work In Progress</p>')910 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)911 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)912 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)913 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)914 ts_query = '%s/%s?config_uuid=%s&event_id=%s' % \915 (ts_url, TS_URL_CBE, p_profile, p_event_id,)916 if ts_auth == 'plain':917 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))918 else:919 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))920 print(f'<!-- page_record CBE URL { ts_url } -->')921 ts_json = json.loads(ts_response.text, strict=False)922 #print('<pre>%s</pre>' % json.dumps(ts_json, sort_keys=True, \923 # indent=4, separators=(',', ': ')) )924 if 'uuid' in ts_json:925 print('<p><b>Success</b></p>')926 else:927 print('<p><b>Failed</b></p>')928 print('<input type="hidden" name="page" value="record">')929 print(930 '<input type="submit" name="Close" value="Close" onclick="self.close()">'931 )932 print('</form method="get">')933##########################################################################################934def page_recordings():935 '''prints the status information, useful to check the API call is working at all'''936 global MY_SETTINGS937 print('<h1>Recordings</h1>')938 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)939 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)940 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)941 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)942 ts_query = '%s/%s' % (943 ts_url,944 TS_URL_DEG,945 )946 if ts_auth == 'plain':947 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))948 else:949 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))950 print(f'<!-- status inputs URL { ts_query } -->')951 if ts_response.status_code != 200:952 print('<p>HTTP error response %d'953 '- does configured user have admin rights?</p>' %954 (ts_response.status_code, ))955 return956 ts_json = ts_response.json()957 if 'entries' in ts_json:958 print(959 '<table><tr><th>Channel Name</th><th>Title</th><th>Date</th><th>Summary</th></tr>'960 )961 for entry in ts_json['entries']:962 print('<tr>')963 if 'channelname' in entry:964 print('<td>%s</td>' % (entry['channelname'], ))965 else:966 print(TD_EMPTY_CELL)967 if 'title' in entry and 'eng' in entry['title']:968 print('<td><a href="?page=m3u&uuid=%s" download="tvheadend.m3u">%s</a>'969 % (entry['url'], input_form_escape(entry['title']['eng']), ))970 if CAST_SUPPORT:971 print('<br><a href="?page=chromecast&uri=%s"><img src="%s" alt="chromecast icon"></a>' % \972 (entry['url'],973 MY_SETTINGS.get(SETTINGS_SECTION, TS_URL_CAST),974 ))975 print('</td>')976 else:977 print(TD_EMPTY_CELL)978 if 'start' in entry:979 print(f'<td>{ epoch_to_human_date(entry["start"]) }</td>')980 else:981 print(TD_EMPTY_CELL)982 if 'summary' in entry and 'eng' in entry['title']:983 print(f'<td>{ entry["summary"]["eng"] }</td>')984 elif 'subtitle' in entry and 'eng' in entry['title']:985 print(f'<td>{ entry["subtitle"]["eng"] }</td>')986 else:987 print(TD_EMPTY_CELL)988 print('</tr>')989 print('</table>')990 print('<pre>%s</pre>' % json.dumps(991 ts_json, sort_keys=True, indent=4, separators=(',', ': ')))992#########################################################################################993def page_serverinfo():994 '''prints the server information, useful to check the API call is working at all'''995 global MY_SETTINGS996 print('<h1>Server Info</h1>')997 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)998 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)999 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)1000 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)1001 ts_query = f'{ ts_url }/{ TS_URL_SVI }'1002 print(f'<!-- serverinfo URL { ts_query } -->')1003 if ts_auth == 'plain':1004 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))1005 else:1006 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))1007 ts_json = ts_response.json()1008 print('<pre>%s</pre>' % json.dumps(1009 ts_json, sort_keys=True, indent=4, separators=(',', ': ')))1010##########################################################################################1011def page_settings():1012 '''the configuration page'''1013 global CONFIG_FILE_NAME1014 global MY_SETTINGS1015 # the check load config function doesn't populate an empty file1016 if SETTINGS_SECTION not in MY_SETTINGS.sections():1017 print(f'section { SETTINGS_SECTION } doesn\'t exit')1018 MY_SETTINGS.add_section(SETTINGS_SECTION)1019 # attempt to find the value of each setting, either from the params1020 # submitted by the browser, or from the file, or from the defaults1021 for setting in sorted(SETTINGS_DEFAULTS):1022 setting_value = ''1023 # get the value if possible from the URL/form1024 cgi_param_name = 'c_' + setting1025 if cgi_param_name in CGI_PARAMS:1026 setting_value = CGI_PARAMS.getvalue(cgi_param_name)1027 else:1028 # otherwise get it from the config file1029 try:1030 setting_value = str(MY_SETTINGS.get(SETTINGS_SECTION, setting))1031 except configparser.NoOptionError:1032 #except configparser.NoOptionError as noex:1033 #print(f'<p>Exception "{ noex }"<br>')1034 #print('failed getting value for setting "%s" from config, '1035 # 'using default</p>' % (SETTINGS_DEFAULTS[setting][TITLE], ))1036 if DFLT in SETTINGS_DEFAULTS[setting]:1037 setting_value = SETTINGS_DEFAULTS[setting][DFLT]1038 else:1039 setting_value = ''1040 MY_SETTINGS.set(SETTINGS_SECTION, setting, setting_value)1041 print('<form method="get" action="">' \1042 '<input type="hidden" name="page" value="settings">' \1043 '<table>' \1044 ' <tr>' \1045 #' <th align="right">Key</th>' \1046 ' <th align="right">Setting</th>' \1047 ' <th>Value</th>' \1048 ' <th>Default</th>\n' \1049 ' </tr>')1050 for setting in sorted(SETTINGS_DEFAULTS):1051 print(' <tr>')1052 #print(f' <td align="right">{ setting } </td>')1053 print(f' <td align="right">{ SETTINGS_DEFAULTS[setting][TITLE] } </td>')1054 if setting in MY_SETTINGS[SETTINGS_SECTION]:1055 setting_value = MY_SETTINGS.get(SETTINGS_SECTION, setting)1056 else:1057 setting_value = SETTINGS_DEFAULTS[setting][DFLT]1058 print(' <td width="50%%"><input type="%s" name="c_%s" '1059 'value="%s" style="display:table-cell; width:100%%"></td>' \1060 % (SETTINGS_DEFAULTS[setting][TYPE], setting, setting_value, ))1061 print(f' <td> { SETTINGS_DEFAULTS[setting][DFLT] }</td>')1062 print(' </tr>')1063 print(''' <tr>1064 <td align="center" colspan="1"> </td>1065 <td align="center" colspan="1"><input type="submit" name="submit" value="submit"></td>1066 <td align="center" colspan="1"><input type="reset" value="revert"></td>1067 </tr>1068 </table>1069 </form><br><br>1070The hostname in the URL for the TVHeadend receiver will be automatically1071turned into an IP address when chromecasting because chromecast devices1072go direct to Google's DNS servers and thus private DNS is ignored.1073''')1074 config_file_handle = open(CONFIG_FILE_NAME, 'w')1075 if config_file_handle:1076 MY_SETTINGS.write(config_file_handle)1077 else:1078 print(f'<b>Error</b>, failed to open and write config file "{ CONFIG_FILE_NAME }"')1079##########################################################################################1080def page_status():1081 '''prints the status information, useful to check the API call is working at all'''1082 global MY_SETTINGS1083 print('''<h1>Server Status</h1>1084<h2>Input Status</h2>''')1085 ts_url = MY_SETTINGS.get(SETTINGS_SECTION, TS_URL)1086 ts_auth = MY_SETTINGS.get(SETTINGS_SECTION, TS_AUTH)1087 ts_user = MY_SETTINGS.get(SETTINGS_SECTION, TS_USER)1088 ts_pass = MY_SETTINGS.get(SETTINGS_SECTION, TS_PASS)1089 ts_query = f'{ ts_url }/{ TS_URL_STI }'1090 if ts_auth == 'plain':1091 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))1092 else:1093 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))1094 print('<!-- status inputs URL %s -->' % (ts_query, ))1095 if ts_response.status_code == 200:1096 ts_json = ts_response.json()1097 print('<pre>%s</pre>' % json.dumps(1098 ts_json, sort_keys=True, indent=4, separators=(',', ': ')))1099 else:1100 print('<p>HTTP error response %d'1101 '- does configured user have admin rights?</p>' %1102 (ts_response.status_code, ))1103 print('<h2>Connection Status</h2>')1104 ts_query = '%s/%s' % (1105 ts_url,1106 TS_URL_STC,1107 )1108 if ts_auth == 'plain':1109 ts_response = requests.get(ts_query, auth=(ts_user, ts_pass))1110 else:1111 ts_response = requests.get(ts_query, auth=HTTPDigestAuth(ts_user, ts_pass))1112 #print('<!-- status connections URL %s -->' % (ts_query, ))1113 if ts_response.status_code == 200:1114 ts_json = ts_response.json()1115 print('<pre>%s</pre>' % json.dumps(1116 ts_json, sort_keys=True, indent=4, separators=(',', ': ')))1117 else:1118 print('<p>HTTP error response %d'1119 '- does configured user have admin rights?</p>' %1120 (ts_response.status_code, ))1121##########################################################################################1122def page_upgrade_check():1123 '''the upgrade check page'''1124 ################################################1125 # see if this script is up to date1126 githash_self = get_githash_self()1127 githubhash_self = get_github_hash_self()1128 print(f'<p>github hash of this file { githubhash_self }<br>\n')1129 print(f'git hash of this file { githash_self, }br>\n')1130 print('<p>')1131 if githubhash_self == githash_self:1132 print(1133 'Great, this program is the same as the version on github.\n<br>\n'1134 )1135 else:1136 print(1137 'This program appears to be out of date, please update it.\n<br>\n'1138 )1139 print('</p>')1140##########################################################################################1141def m3u_page_header():1142 '''page header for m3u playlists'''1143 print('Content-Type: audio/x-mpegurl\n')1144##########################################################################################1145def html_page_header():1146 '''standard html page header'''1147 global MY_SETTINGS1148 bg_col_page = BG_COL_DEF_PAGE1149 if BG_COL_PAGE in MY_SETTINGS[SETTINGS_SECTION] and MY_SETTINGS.get(SETTINGS_SECTION, BG_COL_PAGE) != '':1150 bg_col_page = MY_SETTINGS.get(SETTINGS_SECTION, BG_COL_PAGE)1151 bg_col_input = BG_COL_DEF_INPUT1152 if BG_COL_INPUT in MY_SETTINGS[SETTINGS_SECTION] and MY_SETTINGS.get(SETTINGS_SECTION, BG_COL_INPUT) != '':1153 bg_col_input = MY_SETTINGS.get(SETTINGS_SECTION, BG_COL_INPUT)1154 # finalise tcp header1155 #print('Content-Type: text/plain\n') # plain text for extreme debugging1156 print('Content-Type: text/html; charset=UTF-8\n')1157 # begin html page1158#<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">1159 #print('''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" http://www.w3.org/TR/html4/strict.dtd">1160 print('''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">1161<html>1162 <head>1163 <title>tvh_epg.py</title>1164 <meta http-equiv="refresh" content="600;">1165 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">1166 <style type="text/css">1167 body {1168 background-color: #%s;1169 }1170 input {1171 background-color: #%s;1172 }1173 table {1174 border-collapse: collapse;1175 border-style: hidden;1176 }1177 table td, table th {1178 border: 1px solid black;1179 background-color: #%s;1180 }1181 .epg_row1182 {1183 vertical-align: top; /* Makes sure all the divs are correctly aligned. */1184 width: 100%%;1185 height: 100%%;1186 display: inline-flex; /* prevents wrapping */1187 }1188 .epg_next1189 {1190 background-color: #e8e8ff;1191 border: 1px #8080e0;1192 border-style: solid;1193 float: left;1194 white-space: pre-wrap;1195 }1196 .epg_now1197 {1198 background-color: #f0fff0;1199 border: 1px #408040;1200 border-style: solid;1201 float: right;1202 white-space: pre-wrap;1203 }1204 .epg_none1205 {1206 background-color: #808080;1207 border: 1px #404040;1208 border-style: solid;1209 float: left;1210 white-space: pre-wrap;1211 }1212 .chan_icon1213 {1214 background-color: #e0e0e0;1215 }1216 .record_this {1217 position: relative;1218 display: inline-block;1219 float: top, left;1220 /* width: 320; */1221 /* height: 320; */1222 }1223 /* https://www.w3schools.com/css/css_tooltip.asp */1224 /* Tooltip container */1225 .tooltip {1226 position: static;1227 /*position: relative;*/1228 display: inline-block;1229 /*text-align: top, left;*/1230 float: top, left;1231 white-space: pre-wrap;1232 }1233 /* Tooltip text */1234 .tooltip .tooltiptext {1235 visibility: hidden;1236 width: 224px;1237 height: auto;1238 white-space: pre-wrap;1239 text-align: center;1240 padding: 5px 0;1241 border-radius: 6px;1242 color: #fff;1243 background-color: #888;1244 /* Position the tooltip text */1245 position: absolute;1246 z-index: 1;1247 }1248 /* Show the tooltip text when you mouse over the tooltip container */1249 .tooltip:hover .tooltiptext {1250 visibility: visible;1251 }1252 pre {1253 white-space: pre-wrap; /* Since CSS 2.1 */1254 white-space: -moz-pre-wrap; /* Mozilla, since 1999 */1255 white-space: -pre-wrap; /* Opera 4-6 */1256 white-space: -o-pre-wrap; /* Opera 7 */1257 word-wrap: break-word; /* Internet Explorer 5.5+ */1258 }1259 </style>1260 </head>1261<body>1262''' % (bg_col_page, bg_col_input, bg_col_page, ))1263 print('<p><a href="/python_errors/?C=M;O=A" target="_new">'1264 '/python_errors (new window)</a></p>')1265 print('''<p>1266<b>Menu:</b> <a href="?page=epg">EPG</a> 1267<a href="?page=channel_list">Channel List</a> 1268<a href="?page=channel_table">Channel Table</a> 1269<a href="?page=recordings">Recordings</a> 1270<a href="?page=serverinfo">Server Info</a> 1271<a href="?page=settings">Settings</a> 1272<a href="?page=status">Status</a> 1273<a href="?page=upgrade_check">Upgrade Check</a> 1274<a href="https://github.com/speculatrix/tvh_epg/blob/master/README.md" target=_new>About</a> (new window) 1275</p>1276''')1277##########################################################################################1278def secs_to_human(t_secs):1279 '''turns a duration in seconds into Xd HH:MM:SS'''1280 #t_secs = 86400 + 4000 + 120 + 51281 t_mins = int(t_secs / 60)1282 t_hours = int(t_mins / 60)1283 t_days = int(t_hours / 24)1284 r_days = t_days1285 r_hours = t_hours - r_days * 241286 r_mins = t_mins - r_days * 24 * 60 - r_hours * 601287 #r_secs = t_secs - r_days * 24 * 60 * 60 - r_hours * 60 * 60 - r_mins * 601288 h_days = ''1289 if r_days > 0:1290 h_days = f'{ r_days }d, '1291 #h_time = '%s%02d:%02d:%02d' % (1292 h_time = '%s%02d:%02d' % ( # pylint:disable=consider-using-f-string1293 h_days,1294 r_hours,1295 r_mins,1296 #r_secs,1297 )1298 return h_time1299##########################################################################################1300def url_escape(text):1301 """escape special characters for URL"""1302 return "".join(URL_ESCAPE_TABLE.get(c, c) for c in text)1303##########################################################################################1304def web_interface():1305 '''provides web interface'''1306 global CONFIG_FILE_NAME1307 global MY_SETTINGS1308 # process the CGI params1309 if 'event_id' in CGI_PARAMS:1310 p_event_id = CGI_PARAMS.getvalue('event_id')1311 else:1312 p_event_id = ''1313 if 'profile' in CGI_PARAMS:1314 p_profile = CGI_PARAMS.getvalue('profile')1315 else:1316 p_profile = ''1317 if 'uri' in CGI_PARAMS:1318 p_uri = CGI_PARAMS.getvalue('uri')1319 else:1320 p_uri = ''1321 if 'cast_device' in CGI_PARAMS:1322 p_cast_device = CGI_PARAMS.getvalue('cast_device')1323 else:1324 p_cast_device = ''1325 #illegal_param_count = 01326 error_text = 'Unknown error'1327 (config_bad, error_text) = check_load_config_file()1328 if config_bad < 0:1329 p_page = 'error'1330 #elif config_bad > 0:1331 # p_page = 'settings'1332 elif 'page' in CGI_PARAMS:1333 p_page = CGI_PARAMS.getvalue('page')1334 else: # set the default page if none provided1335 #p_page = 'error'1336 p_page = EPG1337 if p_page == EPG:1338 html_page_header()1339 page_list_chans_epg(True)1340 html_page_footer()1341 elif p_page == 'error':1342 html_page_header()1343 page_error(error_text)1344 html_page_footer()1345 elif p_page == 'channel_list':1346 html_page_header()1347 page_list_chans_epg(False)1348 html_page_footer()1349 elif p_page == 'channel_table':1350 html_page_header()1351 page_channel_table()1352 html_page_footer()1353 elif CAST_SUPPORT and p_page == 'chromecast':1354 html_page_header()1355 page_chromecast(p_uri, p_cast_device)1356 html_page_footer()1357 elif p_page == 'm3u':1358 if 'uuid' in CGI_PARAMS:1359 p_uuid = CGI_PARAMS.getvalue('uuid')1360 m3u_page_header()1361 page_m3u(p_uuid)1362 else:1363 html_page_header()1364 page_error('missing uuid for m3u generator')1365 html_page_footer()1366 elif p_page == 'record':1367 html_page_header()1368 page_record(p_event_id, p_profile)1369 html_page_footer()1370 elif p_page == 'recordings':1371 html_page_header()1372 page_recordings()1373 html_page_footer()1374 elif p_page == 'serverinfo':1375 html_page_header()1376 page_serverinfo()1377 html_page_footer()1378 elif p_page == 'status':1379 html_page_header()1380 page_status()1381 html_page_footer()1382 elif p_page == 'settings':1383 html_page_header()1384 page_settings()1385 html_page_footer()1386 elif p_page == 'upgrade_check':1387 html_page_header()1388 page_upgrade_check()1389 html_page_footer()1390 else:1391 html_page_header()1392 #page_error('no page selected')1393 html_page_footer()1394 #illegal_param_count += 11395##########################################################################################1396# main1397# a few globals1398#PATH_OF_SCRIPT = os.path.dirname(os.path.realpath(__file__))1399CONFIG_FILE_NAME = os.path.join(CONTROL_DIR, SETTINGS_FILE)1400MY_SETTINGS = configparser.ConfigParser()1401if len(sys.argv) <= 1:1402 sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())1403 DOCROOT = os.environ.get('DOCUMENT_ROOT', DOCROOT_DEFAULT)1404 cgitb.enable(display=0, logdir=DOCROOT + '/python_errors', format='html')1405 web_interface()1406else:1407 print('Failed')1408 sys.exit(1)...
Preferences.py
Source:Preferences.py
1#!/usr/bin/env python2# -*- coding: UTF-8 -*-3import ConfigParser4import sys5import os6class Preferences(ConfigParser.SafeConfigParser):7 def __init__(self):8 ConfigParser.SafeConfigParser.__init__(self)9 # make config case sensitive10 self.optionxform = str11 self.settings_section = 'Settings'12 self.general_section = 'General'13 defaults = dict()14 defaults[self.settings_section] = {'DNG': True, 'TIFF': False, 'Thumbnail': False, 'Rotate': False}15 defaults[self.general_section] = {'Language': 'en'}16 self.prefs_path = os.path.join(os.path.dirname(sys.argv[0]), 'preferences.ini')17 self.read(self.prefs_path)18 need_to_write = False19 for section in defaults:20 if not self.has_section(section):21 self.add_section(section)22 need_to_write = True23 values = defaults[section]24 for key, val in values.items():25 if not self.has_option(section, key):26 self.set(section, key, str(val))27 need_to_write = True28 if need_to_write:29 self.write()30 def write(self):31 with open(self.prefs_path, 'wb') as configfile:32 ConfigParser.SafeConfigParser.write(self, configfile)33 def get_dng(self):34 return self.getboolean(self.settings_section, 'DNG')35 def get_tiff(self):36 return self.getboolean(self.settings_section, 'TIFF')37 def get_thumbnail(self):38 return self.getboolean(self.settings_section, 'Thumbnail')39 def get_rotate(self):40 return self.getboolean(self.settings_section, 'Rotate')41 def get_lang(self):42 return self.get(self.general_section, 'Language')43 def set_dng(self, val):44 self.set(self.settings_section, 'DNG', val)45 self.write()46 def set_tiff(self, val):47 self.set(self.settings_section, 'TIFF', val)48 self.write()49 def set_thumbnail(self, val):50 self.set(self.settings_section, 'Thumbnail', val)51 self.write()52 def set_rotate(self, val):53 self.set(self.settings_section, 'Rotate', val)54 self.write()55 def set_lang(self, val):56 self.set(self.general_section, 'Language')57 self.write()58if __name__ == "__main__":59 prefs = Preferences()60 print prefs.get_dng()61 print prefs.get_tiff()62 print prefs.get_thumbnail()63 print prefs.get_rotate()...
configurations.py
Source:configurations.py
1'''2 Project: Gui Gin Rummy3 File name: configurations.py4 Author: William Hale5 Date created: 3/14/20206'''7import os8from configparser import ConfigParser9#10# Gin Rummy parameters11#12GOING_OUT_DEADWOOD_COUNT = 1013#14# RLCard Gin Rummy parameters15#16MAX_DRAWN_CARD_COUNT = 5217DISCARD_PILE_TAG = "discard_pile"18STOCK_PILE_TAG = "stock_pile"19NORTH_HELD_PILE_TAG = "north_held_pile"20SOUTH_HELD_PILE_TAG = "south_held_pile"21PLAYER_HELD_PILE_TAGS = [NORTH_HELD_PILE_TAG, SOUTH_HELD_PILE_TAG]22DRAWN_TAG = "drawn"23JOGGED_TAG = "jogged"24SELECTED_TAG = "selected"25SCORE_PLAYER_0_ACTION_ID = 026SCORE_PLAYER_1_ACTION_ID = 127DRAW_CARD_ACTION_ID = 228PICK_UP_DISCARD_ACTION_ID = 329DECLARE_DEAD_HAND_ACTION_ID = 430GIN_ACTION_ID = 531DISCARD_ACTION_ID = 632KNOCK_ACTION_ID = DISCARD_ACTION_ID + 5233#34# Not User Modifiable Options35#36IS_KEEP_TURN_WHEN_DISCARDING_CARD_PICKED_UP = False # TODO: make True the default value37#38# User Modifiable Options39#40config_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'game_options.ini') # Note this41config = ConfigParser()42found = config.read(config_path)43# settings section44settings_section = "settings"45show_status_messages_option = "show_status_messages"46warning_as_option = 'warning_as'47game_background_color_option = 'game_background_color'48window_size_factor_option = 'window_size_factor'49is_show_tips_option = "is_show_tips"50is_debug_option = "is_debug"51SHOW_STATUS_MESSAGES = config.get(section=settings_section, option=show_status_messages_option, fallback="verbose")52WARNINGS_AS = config.get(section=settings_section, option=warning_as_option, fallback="alert_messages")53GAME_BACKGROUND_COLOR = config.get(section=settings_section, option=game_background_color_option, fallback="#007F00")54WINDOW_SIZE_FACTOR = config.getint(section=settings_section, option=window_size_factor_option, fallback=75)55IS_SHOW_TIPS = config.getboolean(section=settings_section, option=is_show_tips_option, fallback=True)56# Note: IS_DEBUG always starts off as False; must explicitly update via preference window57# IS_DEBUG = config.getboolean(section=settings_section, option=is_debug_option, fallback=False)...
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!