def get_channels_m3u(config, location, base_url): FORMAT_DESCRIPTOR = "#EXTM3U" RECORD_MARKER = "#EXTINF" fakefile = StringIO() xmltvurl = ('%s%s/xmltv.xml' % ("http://", base_url)) fakefile.write("%s\n" % (FORMAT_DESCRIPTOR + " " + "url-tvg=\"" + xmltvurl + "\"" + " " + "x-tvg-url=\"" + xmltvurl + "\"")) station_list = stations.get_dma_stations_and_channels(config, location) for sid in station_list: fakefile.write( "%s\n" % (RECORD_MARKER + ":-1" + " " + "channelID=\"" + str(sid) + "\" " + "tvg-chno=\"" + str(station_list[sid]['channel']) + "\" " + "tvg-name=\"" + station_list[sid]['friendlyName'] + "\" " + "tvg-id=\"" + str(sid) + "\" " + (("tvg-logo=\"" + station_list[sid]['logoUrl'] + "\" ") if 'logoUrl' in station_list[sid].keys() else "") + "group-title=\"Locast2Plex\"," + station_list[sid]['friendlyName'])) fakefile.write("%s\n" % (('%s%s/watch/%s' % ("http://", base_url, str(sid))))) return fakefile.getvalue()
def do_tuning(self, sid): channelUri = self.local_locast.get_station_stream_uri(sid) station_list = stations.get_dma_stations_and_channels( self.config, self.location) tuner_found = False # keep track of how many tuners we can use at a time for index, scan_status in enumerate(self.rmg_station_scans): # the first idle tuner gets it if scan_status == 'Idle': self.rmg_station_scans[index] = station_list[sid]['channel'] tuner_found = True break if tuner_found: self.send_response(200) self.send_header('Content-type', 'video/mpeg; codecs="avc1.4D401E') self.end_headers() ffmpeg_command = [ self.config['main']['ffmpeg_path'], "-i", channelUri, "-c:v", "copy", "-c:a", "copy", "-f", "mpegts", "-nostats", "-hide_banner", "-loglevel", "warning", "pipe:1" ] ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) # get initial videodata. if that works, then keep grabbing it videoData = ffmpeg_proc.stdout.read( int(self.config['main']['bytes_per_read'])) while True: if not videoData: break else: # from https://stackoverflow.com/questions/9932332 try: self.wfile.write(videoData) time.sleep(0.1) except IOError as e: # Check we hit a broken pipe when trying to write back to the client if e.errno in [ errno.EPIPE, errno.ECONNABORTED, errno.ECONNRESET, errno.ECONNREFUSED ]: break else: raise videoData = ffmpeg_proc.stdout.read( int(self.config['main']['bytes_per_read'])) # Send SIGTERM to shutdown ffmpeg ffmpeg_proc.terminate() try: # ffmpeg writes a bit of data out to stderr after it terminates, # need to read any hanging data to prevent a zombie process. ffmpeg_proc.communicate() except ValueError: print("Connection Closed") self.rmg_station_scans[index] = 'Idle' else: self.send_response(400, 'All tuners already in use.') self.send_header('Content-type', 'text/html') self.end_headers() reply_str = templates['htmlError'].format( 'All tuners already in use.') self.wfile.write(reply_str.encode('utf-8'))
def do_GET(self): base_url = self.config['main'][ 'plex_accessible_ip'] + ':' + self.config['main'][ 'plex_accessible_port'] contentPath = self.path queryData = {} if self.path.find('?') != -1: contentPath = self.path[0:self.path.find('?')] getdata = self.path[(self.path.find('?') + 1):] getdataElements = getdata.split('&') for getdataItem in getdataElements: getdataItemSplit = getdataItem.split('=') if len(getdataItemSplit) > 1: queryData[getdataItemSplit[0]] = getdataItemSplit[1] # paths and logic mostly pulled from telly:routes.go: https://github.com/tellytv/telly if (contentPath == '/') and ( not self.config['main']['use_old_plex_interface']): self.do_response( 200, 'application/xml', templates['xmlRmgIdentification'].format( self.config['main']['reporting_friendly_name'])) elif (contentPath == '/') or (contentPath == '/device.xml'): templateName = 'xmlDiscover' if self.config['main']['use_old_plex_interface']: templateName = 'xmlDiscoverOld' self.do_response( 200, 'application/xml', templates[templateName].format( self.config['main']['reporting_friendly_name'], self.config['main']['reporting_model'], self.config['main']['uuid'], base_url)) elif contentPath == '/discover.json': self.do_response( 200, 'application/json', templates['jsonDiscover'].format( self.config['main']['reporting_friendly_name'], self.config['main']['reporting_model'], self.config['main']['reporting_firmware_name'], self.config['main']['tuner_count'], self.config['main']['reporting_firmware_ver'], self.config['main']['uuid'], base_url)) elif contentPath == '/lineup_status.json': if self.hdhr_station_scan: returnJSON = templates['jsonLineupStatus'] else: returnJSON = templates['jsonLineupComplete'].replace( "Antenna", self.config['main']['tuner_type']) self.do_response(200, 'application/json', returnJSON) elif contentPath == '/lineup.json': # TODO station_list = stations.get_dma_stations_and_channels( self.config, self.location) returnJSON = '' for index, list_key in enumerate(station_list): sid = str(list_key) returnJSON = returnJSON + templates['jsonLineupItem'].format( station_list[sid]['channel'], station_list[sid]['friendlyName'], base_url + '/watch/' + sid) if (index + 1) != len(station_list): returnJSON = returnJSON + ',' returnJSON = "[" + returnJSON + "]" self.do_response(200, 'application/json', returnJSON) elif contentPath == '/lineup.xml': # TODO station_list = stations.get_dma_stations_and_channels( self.config, self.location) returnXML = '' for list_key in station_list: sid = str(list_key) returnXML = returnXML + templates['xmlLineupItem'].format( station_list[sid]['channel'], station_list[sid]['friendlyName'], base_url + '/watch/' + sid) returnXML = "<Lineup>" + returnXML + "</Lineup>" self.do_response(200, 'application/xml', returnXML) elif contentPath.startswith('/watch'): self.do_tuning(contentPath.replace('/watch/', '')) elif contentPath.startswith('/auto/v'): self.do_tuning(contentPath.replace('/auto/v', '')) elif ((contentPath.startswith('/devices/' + self.config['main']['uuid'] + '/media/')) and (not self.config['main']['use_old_plex_interface'])): channel_no = contentPath.replace( '/devices/' + self.config['main']['uuid'] + '/media/', '') channel_no = urllib.parse.unquote(channel_no).replace('id://', '').replace( '/', '') station_list = stations.get_dma_stations_and_channels( self.config, self.location) for sid in station_list: if station_list[sid]['channel'] == channel_no: break self.do_tuning(sid) elif contentPath == '/xmltv.xml': self.do_response(200, 'application/xml', epg2xml.get_epg(self.config, self.location)) elif contentPath == '/channels.m3u': self.do_response( 200, 'application/vnd.apple.mpegurl', channels_m3u.get_channels_m3u(self.config, self.location, base_url)) elif contentPath == '/debug.json': self.do_response(200, 'application/json') elif ((contentPath == '/devices/' + self.config['main']['uuid']) and (not self.config['main']['use_old_plex_interface'])): tuner_list = "" for index, scan_status in enumerate(self.rmg_station_scans): if scan_status == 'Idle': tuner_list = tuner_list + templates[ 'xmlRmgTunerIdle'].format(str(index)) elif scan_status == 'Scan': tuner_list = tuner_list + templates[ 'xmlRmgTunerScanning'].format(str(index)) else: # otherwise, we're streaming, and the value will be the channel triplet formatted_xml = templates['xmlRmgTunerStreaming'].format( str(index), scan_status) tuner_list = tuner_list + formatted_xml self.do_response( 200, 'application/xml', templates['xmlRmgDeviceIdentity'].format( self.config['main']['uuid'], self.config['main']['reporting_friendly_name'], self.config['main']['reporting_model'], self.config['main']['tuner_count'], base_url, tuner_list)) elif ((contentPath == '/devices/' + self.config['main']['uuid'] + '/channels') and (not self.config['main']['use_old_plex_interface'])): station_list = stations.get_dma_stations_and_channels( self.config, self.location) channelXML = '' for index, list_key in enumerate(station_list): sid = str(list_key) tmpXML = templates['xmlRmgDeviceChannelItem'].format( station_list[sid]['channel'], station_list[sid]['friendlyName']) channelXML = channelXML + tmpXML self.do_response( 200, 'application/xml', templates['xmlRmgDeviceChannels'].format( index + 1, channelXML)) elif ((contentPath == '/devices/' + self.config['main']['uuid'] + '/scanners') and (not self.config['main']['use_old_plex_interface'])): self.do_response( 200, 'application/xml', templates['xmlRmgScanProviders'].format(self.location['city'])) else: print("Unknown request to " + contentPath) self.do_response( 501, 'text/html', templates['htmlError'].format('501 - Not Implemented')) return
def generate_epg_file(config, location): base_cache_dir = config["main"]["cache_dir"] out_path = pathlib.Path(base_cache_dir).joinpath( str(location["DMA"]) + "_epg").with_suffix(".xml") out_lock_path = pathlib.Path(base_cache_dir).joinpath( str(location["DMA"]) + "_epg").with_suffix(".xml.lock") cache_dir = pathlib.Path(base_cache_dir).joinpath( str(location["DMA"]) + "_epg") if not cache_dir.is_dir(): cache_dir.mkdir() dma_channels = stations.get_dma_stations_and_channels(config, location) # Make a date range to pull todaydate = datetime.datetime.utcnow().replace( hour=0, minute=0, second=0, microsecond=0) # make sure we're dealing with UTC! dates_to_pull = [todaydate] days_to_pull = int(config["main"]["epg_update_days"]) for x in range(1, days_to_pull - 1): xdate = todaydate + datetime.timedelta(days=x) dates_to_pull.append(xdate) remove_stale_cache(cache_dir, todaydate) out = ET.Element('tv') out.set('source-info-url', 'https://www.locast.org') out.set('source-info-name', 'locast.org') out.set('generator-info-name', 'locastepg') out.set('generator-info-url', 'github.com/tgorgdotcom/locast2plex') out.set('generator-special-thanks', 'deathbybandaid') done_channels = False for x_date in dates_to_pull: url = ('https://api.locastnet.org/api/watch/epg/' + str(location["DMA"]) + "?startTime=" + x_date.isoformat()) result = get_cached(cache_dir, x_date.strftime("%m-%d-%Y"), url) channel_info = json.loads(result) # List Channels First if not done_channels: done_channels = True for channel_item in channel_info: sid = str(channel_item['id']) if sid in dma_channels.keys(): channel_number = str(dma_channels[sid]['channel']) channel_realname = str(dma_channels[sid]['friendlyName']) channel_callsign = str(dma_channels[sid]['callSign']) if 'logo226Url' in channel_item.keys(): channel_logo = channel_item['logo226Url'] elif 'logoUrl' in channel_item.keys(): channel_logo = channel_item['logoUrl'] c_out = sub_el(out, 'channel', id=sid) sub_el(c_out, 'display-name', text='%s %s' % (channel_number, channel_callsign)) sub_el(c_out, 'display-name', text='%s %s %s' % (channel_number, channel_callsign, sid)) sub_el(c_out, 'display-name', text=channel_number) sub_el(c_out, 'display-name', text='%s %s fcc' % (channel_number, channel_callsign)) sub_el(c_out, 'display-name', text=channel_callsign) sub_el(c_out, 'display-name', text=channel_realname) if channel_logo != None: sub_el(c_out, 'icon', src=channel_logo) # Now list Program informations for channel_item in channel_info: sid = str(channel_item['id']) if sid in dma_channels.keys(): channel_number = str(dma_channels[sid]['channel']) channel_realname = str(dma_channels[sid]['friendlyName']) channel_callsign = str(dma_channels[sid]['callSign']) if 'logo226Url' in channel_item.keys(): channel_logo = channel_item['logo226Url'] elif 'logoUrl' in channel_item.keys(): channel_logo = channel_item['logoUrl'] for event in channel_item['listings']: tm_start = tm_parse( event['startTime'] ) # this is returned from locast in UTC tm_duration = event['duration'] * 1000 tm_end = tm_parse(event['startTime'] + tm_duration) event_genres = [] if 'genres' in event.keys(): event_genres = event['genres'].split(",") # note we're returning everything as UTC, as the clients handle converting to correct timezone prog_out = sub_el(out, 'programme', start=tm_start, stop=tm_end, channel=sid) if event['title']: sub_el(prog_out, 'title', lang='en', text=event['title']) if 'movie' in event_genres and event['releaseYear']: sub_el(prog_out, 'sub-title', lang='en', text='Movie: ' + event['releaseYear']) elif 'episodeTitle' in event.keys(): sub_el(prog_out, 'sub-title', lang='en', text=event['episodeTitle']) if 'description' not in event.keys(): event['description'] = "Unavailable" elif event['description'] is None: event['description'] = "Unavailable" sub_el(prog_out, 'desc', lang='en', text=event['description']) sub_el(prog_out, 'length', units='minutes', text=str(event['duration'])) for f in event_genres: sub_el(prog_out, 'category', lang='en', text=f.strip()) sub_el(prog_out, 'genre', lang='en', text=f.strip()) if event["preferredImage"] is not None: sub_el(prog_out, 'icon', src=event["preferredImage"]) if 'rating' not in event.keys(): event['rating'] = "N/A" r = ET.SubElement(prog_out, 'rating') sub_el(r, 'value', text=event['rating']) if 'seasonNumber' in event.keys( ) and 'episodeNumber' in event.keys(): s_ = int(str(event['seasonNumber']), 10) e_ = int(str(event['episodeNumber']), 10) sub_el(prog_out, 'episode-num', system='common', text='S%02dE%02d' % (s_, e_)) sub_el(prog_out, 'episode-num', system='xmltv_ns', text='%d.%d.0' % (int(s_) - 1, int(e_) - 1)) sub_el(prog_out, 'episode-num', system='SxxExx', text='S%02dE%02d' % (s_, e_)) if 'isNew' in event.keys(): if event['isNew']: sub_el(prog_out, 'new') xml_lock = FileLock(out_lock_path) with xml_lock: with open(out_path, 'wb') as f: f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n') f.write(ET.tostring(out, encoding='UTF-8'))