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
示例#4
0
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'))