From 4b360b08526c2fe7df202c795d5d90b2d4f9d5ff Mon Sep 17 00:00:00 2001 From: Fergal Moran Date: Wed, 6 Apr 2022 11:33:36 +0100 Subject: [PATCH] Fix navbar --- backend/app/{main.py => api.py} | 22 +- backend/app/config.sample.py | 26 +- backend/app/lib/epg/__init__.py | 0 backend/app/lib/epg/epg.py | 34 + backend/app/lib/epg/xmltv.py | 783 ++++++++++++++++++ backend/app/lib/mpv.py | 3 - backend/requirements.txt | 3 +- frontend/.env | 5 +- frontend/package.json | 2 +- frontend/src/App.tsx | 2 +- frontend/src/components/epg.component.tsx | 26 + frontend/src/components/index.ts | 3 +- frontend/src/components/sidebar.component.tsx | 64 +- frontend/src/pages/channel.page.tsx | 97 ++- 14 files changed, 997 insertions(+), 73 deletions(-) rename backend/app/{main.py => api.py} (78%) create mode 100644 backend/app/lib/epg/__init__.py create mode 100644 backend/app/lib/epg/epg.py create mode 100644 backend/app/lib/epg/xmltv.py delete mode 100644 backend/app/lib/mpv.py create mode 100644 frontend/src/components/epg.component.tsx diff --git a/backend/app/main.py b/backend/app/api.py similarity index 78% rename from backend/app/main.py rename to backend/app/api.py index e59190c..ba5fc1b 100644 --- a/backend/app/main.py +++ b/backend/app/api.py @@ -1,11 +1,18 @@ +import logging +from logging.config import dictConfig + import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse -from . import config -from .lib.streamer import Streamer -from .lib.xtream import XTream +from app import config +from app.config import log_config +from app.lib.epg.epg import EPGParser +from app.lib.streamer import Streamer +from app.lib.xtream import XTream + +dictConfig(log_config) provider = XTream( config.provider['server'], @@ -13,6 +20,9 @@ provider = XTream( config.provider['password'] ) +epg = EPGParser( + config.provider['epgurl'] +) app = FastAPI() origins = [ "https://dev-streams.fergl.ie:3000", @@ -32,6 +42,12 @@ app.add_middleware( ) +@app.get("/epg/{channel_id}") +async def get_channel_epg(channel_id): + listings = epg.get_listings(channel_id) + return listings + + @app.get("/channels") async def channels(): categories = provider.get_categories() diff --git a/backend/app/config.sample.py b/backend/app/config.sample.py index 017a73b..8495d18 100644 --- a/backend/app/config.sample.py +++ b/backend/app/config.sample.py @@ -2,5 +2,29 @@ provider = dict( name="", server="http://", username="", - password="" + password="", + epgurl="" ) + +log_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(asctime)s %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + }, + "loggers": { + "foo-logger": {"handlers": ["default"], "level": "DEBUG"}, + }, +} diff --git a/backend/app/lib/epg/__init__.py b/backend/app/lib/epg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/lib/epg/epg.py b/backend/app/lib/epg/epg.py new file mode 100644 index 0000000..41dee14 --- /dev/null +++ b/backend/app/lib/epg/epg.py @@ -0,0 +1,34 @@ +import os +import tempfile +import time + +import requests + +import logging + +from app.lib.epg import xmltv + +log = logging.getLogger(__name__) + + +class EPGParser: + def __init__(self, url): + self._epg_url = url + self._programs = {} + + self._cache_file = os.path.join(tempfile.mkdtemp(), 'epg.xml') + self._cache_epg() + + def _cache_epg(self): + log.debug("Downloading EPG") + data = requests.get(self._epg_url) + with open(self._cache_file, 'wb') as file: + file.write(data.content) + + log.debug("Parsing EPG") + + self._programs = xmltv.read_programmes(open(self._cache_file, 'r')) + + def get_listings(self, channel_id): + listings = [d for d in self._programs if d['channel'] == channel_id and d['stop'] > int(time.time())] + return listings diff --git a/backend/app/lib/epg/xmltv.py b/backend/app/lib/epg/xmltv.py new file mode 100644 index 0000000..7710700 --- /dev/null +++ b/backend/app/lib/epg/xmltv.py @@ -0,0 +1,783 @@ +""" +xmltv.py - Python interface to XMLTV format, based on XMLTV.pm + +Copyright (C) 2001 James Oakley + +This library is free software: you can redistribute it and/or modify it under +the terms of the GNU Lesser General Public License as published by the Free +Software Foundation; either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this software; if not, see . +""" +import io +import time +import xml.etree.ElementTree as ET + +# The date format used in XMLTV (the %Z will go away in 0.6) +date_format = '%Y%m%d%H%M%S %Z' +date_format_notz = '%Y%m%d%H%M%S' + + +def timestr2secs_utc(timestr): + """ + Convert a timestring to UTC (=GMT) seconds. + The format is either one of these two: + '20020702100000 CDT' + '200209080000 +0100' + """ + # This is either something like 'EDT', or '+1' + try: + tval, tz = timestr.split() + except ValueError as e: + tval = timestr + # ugly, but assume current timezone + tz = time.tzname[time.daylight] + # now we convert the timestring using the current timezone + secs = int(time.mktime(time.strptime(tval, '%Y%m%d%H%M%S'))) + # The timezone is still missing. The %Z handling of Python + # seems to be broken, at least for me CEST and UTC return the + # same value with time.strptime. This means we handle it now + # ourself. + if tz in time.tzname: + # the timezone is something we know + if list(time.tzname).index(tz): + # summer time + return secs + time.altzone + # winter (normal) time + return secs + time.timezone + if tz in ('UTC', 'GMT'): + # already UTC + return secs + # timeval [+-][hh]00 + # FIXME: my xmltv file uses +0000 so I can not test here. + # It should be secs - tz and maybe it is + + return secs - int(tz) * 36 + + +def set_attrs(dict, elem, attrs): + """ + set_attrs(dict, elem, attrs) -> None + + Add any attributes in 'attrs' found in 'elem' to 'dict' + """ + for attr in attrs: + if attr in list(elem.keys()): + dict[attr] = elem.get(attr) + + +def set_boolean(dict, name, elem): + """ + set_boolean(dict, name, elem) -> None + + If element, 'name' is found in 'elem', set 'dict'['name'] to a boolean + from the 'yes' or 'no' content of the node + """ + node = elem.find(name) + if node is not None: + if node.text.lower() == 'yes': + dict[name] = True + elif node.text.lower() == 'no': + dict[name] = False + + +def append_text(dict, name, elem, with_lang=True): + """ + append_text(dict, name, elem, with_lang=True) -> None + + Append any text nodes with 'name' found in 'elem' to 'dict'['name']. If + 'with_lang' is 'True', a tuple of ('text', 'lang') is appended + """ + for node in elem.findall(name): + if name not in dict: + dict[name] = [] + if with_lang: + dict[name].append((node.text, node.get('lang', ''))) + else: + dict[name].append(node.text) + + +def set_text(dict, name, elem, with_lang=True): + """ + set_text(dict, name, elem, with_lang=True) -> None + + Set 'dict'['name'] to the text found in 'name', if found under 'elem'. If + 'with_lang' is 'True', a tuple of ('text', 'lang') is set + """ + node = elem.find(name) + if node is not None: + if with_lang: + dict[name] = (node.text, node.get('lang', '')) + else: + dict[name] = node.text + + +def append_icons(dict, elem): + """ + append_icons(dict, elem) -> None + + Append any icons found under 'elem' to 'dict' + """ + for iconnode in elem.findall('icon'): + if 'icon' not in dict: + dict['icon'] = [] + icond = {} + set_attrs(icond, iconnode, ('src', 'width', 'height')) + dict['icon'].append(icond) + + +def elem_to_channel(elem): + """ + elem_to_channel(Element) -> dict + + Convert channel element to dictionary + """ + d = {'id': elem.get('id'), + 'display-name': []} + + append_text(d, 'display-name', elem) + append_icons(d, elem) + append_text(d, 'url', elem, with_lang=False) + + return d + + +def read_channels(fp=None, tree=None): + """ + read_channels(fp=None, tree=None) -> list + + Return a list of channel dictionaries from file object 'fp' or the + ElementTree 'tree' + """ + if fp: + et = ET.ElementTree() + tree = et.parse(fp) + return [elem_to_channel(elem) for elem in tree.findall('channel')] + + +def elem_to_programme(elem): + """ + elem_to_programme(Element) -> dict + + Convert programme element to dictionary + """ + d = { + 'start': timestr2secs_utc(elem.get('start')), + 'stop': timestr2secs_utc(elem.get('stop')), + 'channel': elem.get('channel'), + 'title': [] + } + + set_attrs(d, elem, ('pdc-start', 'vps-start', 'showview', + 'videoplus', 'clumpidx')) + + append_text(d, 'title', elem) + append_text(d, 'sub-title', elem) + append_text(d, 'desc', elem) + + crednode = elem.find('credits') + if crednode is not None: + creddict = {} + # TODO: actor can have a 'role' attribute + for credtype in ('director', 'actor', 'writer', 'adapter', 'producer', + 'presenter', 'commentator', 'guest', 'composer', + 'editor'): + append_text(creddict, credtype, crednode, with_lang=False) + d['credits'] = creddict + + set_text(d, 'date', elem, with_lang=False) + append_text(d, 'category', elem) + set_text(d, 'language', elem) + set_text(d, 'orig-language', elem) + + lennode = elem.find('length') + if lennode is not None: + lend = {'units': lennode.get('units'), + 'length': lennode.text} + d['length'] = lend + + append_icons(d, elem) + append_text(d, 'url', elem, with_lang=False) + append_text(d, 'country', elem) + + for epnumnode in elem.findall('episode-num'): + if 'episode-num' not in d: + d['episode-num'] = [] + d['episode-num'].append((epnumnode.text, + epnumnode.get('system', 'xmltv_ns'))) + + vidnode = elem.find('video') + if vidnode is not None: + vidd = {} + for name in ('present', 'colour'): + set_boolean(vidd, name, vidnode) + for videlem in ('aspect', 'quality'): + venode = vidnode.find(videlem) + if venode is not None: + vidd[videlem] = venode.text + d['video'] = vidd + + audnode = elem.find('audio') + if audnode is not None: + audd = {} + set_boolean(audd, 'present', audnode) + stereonode = audnode.find('stereo') + if stereonode is not None: + audd['stereo'] = stereonode.text + d['audio'] = audd + + psnode = elem.find('previously-shown') + if psnode is not None: + psd = {} + set_attrs(psd, psnode, ('start', 'channel')) + d['previously-shown'] = psd + + set_text(d, 'premiere', elem) + set_text(d, 'last-chance', elem) + + if elem.find('new') is not None: + d['new'] = True + + for stnode in elem.findall('subtitles'): + if 'subtitles' not in d: + d['subtitles'] = [] + std = {} + set_attrs(std, stnode, ('type',)) + set_text(std, 'language', stnode) + d['subtitles'].append(std) + + for ratnode in elem.findall('rating'): + if 'rating' not in d: + d['rating'] = [] + ratd = {} + set_attrs(ratd, ratnode, ('system',)) + set_text(ratd, 'value', ratnode, with_lang=False) + append_icons(ratd, ratnode) + d['rating'].append(ratd) + + for srnode in elem.findall('star-rating'): + if 'star-rating' not in d: + d['star-rating'] = [] + srd = {} + set_attrs(srd, srnode, ('system',)) + set_text(srd, 'value', srnode, with_lang=False) + append_icons(srd, srnode) + d['star-rating'].append(srd) + + for revnode in elem.findall('review'): + if 'review' not in d: + d['review'] = [] + rd = {} + set_attrs(rd, revnode, ('type', 'source', 'reviewer',)) + set_text(rd, 'value', revnode, with_lang=False) + d['review'].append(rd) + + return d + + +def read_programmes(fp=None, tree=None): + """ + read_programmes(fp=None, tree=None) -> list + + Return a list of programme dictionaries from file object 'fp' or the + ElementTree 'tree' + """ + if fp: + et = ET.ElementTree() + tree = et.parse(fp) + return [elem_to_programme(elem) for elem in tree.findall('programme')] + + +def read_data(fp=None, tree=None): + """ + read_data(fp=None, tree=None) -> dict + + Get the source and other info from file object fp or the ElementTree + 'tree' + """ + if fp: + et = ET.ElementTree() + tree = et.parse(fp) + + d = {} + set_attrs(d, tree, ('date', 'source-info-url', 'source-info-name', + 'source-data-url', 'generator-info-name', + 'generator-info-url')) + return d + + +def indent(elem, level=0): + """ + Indent XML for pretty printing + """ + i = "\n" + level * " " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + if not elem.tail or not elem.tail.strip(): + elem.tail = i + for elem in elem: + indent(elem, level + 1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + + +class Writer: + """ + A class for generating XMLTV data + + **All strings passed to this class must be Unicode, except for dictionary + keys** + """ + + def __init__(self, encoding="UTF-8", date=None, + source_info_url=None, source_info_name=None, + generator_info_url=None, generator_info_name=None): + """ + Arguments: + + 'encoding' -- The text encoding that will be used. + *Defaults to 'UTF-8'* + + 'date' -- The date this data was generated. *Optional* + + 'source_info_url' -- A URL for information about the source of the + data. *Optional* + + 'source_info_name' -- A human readable description of + 'source_info_url'. *Optional* + + 'generator_info_url' -- A URL for information about the program + that is generating the XMLTV document. + *Optional* + + 'generator_info_name' -- A human readable description of + 'generator_info_url'. *Optional* + + """ + self.encoding = encoding + self.data = {'date': date, + 'source_info_url': source_info_url, + 'source_info_name': source_info_name, + 'generator_info_url': generator_info_url, + 'generator_info_name': generator_info_name} + + self.root = ET.Element('tv') + for attr in list(self.data.keys()): + if self.data[attr]: + self.root.set(attr, self.data[attr]) + + def setattr(self, node, attr, value): + """ + setattr(node, attr, value) -> None + + Set 'attr' in 'node' to 'value' + """ + node.set(attr, value) + + def settext(self, node, text, with_lang=True): + """ + settext(node, text) -> None + + Set 'node's text content to 'text' + """ + if with_lang: + if text[0] is None: + node.text = None + else: + node.text = text[0] + if text[1]: + node.set('lang', text[1]) + else: + if text is None: + node.text = None + else: + node.text = text + + def seticons(self, node, icons): + """ + seticon(node, icons) -> None + + Create 'icons' under 'node' + """ + for icon in icons: + if 'src' not in icon: + raise ValueError("'icon' element requires 'src' attribute") + i = ET.SubElement(node, 'icon') + for attr in ('src', 'width', 'height'): + if attr in icon: + self.setattr(i, attr, icon[attr]) + + def set_zero_ormore(self, programme, element, p): + """ + set_zero_ormore(programme, element, p) -> None + + Add nodes under p for the element 'element', which occurs zero + or more times with PCDATA and a 'lang' attribute + """ + if element in programme: + for item in programme[element]: + e = ET.SubElement(p, element) + self.settext(e, item) + + def set_zero_orone(self, programme, element, p): + """ + set_zero_ormore(programme, element, p) -> None + + Add nodes under p for the element 'element', which occurs zero + times or once with PCDATA and a 'lang' attribute + """ + if element in programme: + e = ET.SubElement(p, element) + self.settext(e, programme[element]) + + def addProgramme(self, programme): + """ + Add a single XMLTV 'programme' + + Arguments: + + 'programme' -- A dict representing XMLTV data + """ + p = ET.SubElement(self.root, 'programme') + + # programme attributes + for attr in ('start', 'channel'): + if attr in programme: + self.setattr(p, attr, programme[attr]) + else: + raise ValueError("'programme' must contain '%s' attribute" % attr) + + for attr in ('stop', 'pdc-start', 'vps-start', 'showview', 'videoplus', 'clumpidx'): + if attr in programme: + self.setattr(p, attr, programme[attr]) + + for title in programme['title']: + t = ET.SubElement(p, 'title') + self.settext(t, title) + + # Sub-title and description + for element in ('sub-title', 'desc'): + self.set_zero_ormore(programme, element, p) + + # Credits + if 'credits' in programme: + c = ET.SubElement(p, 'credits') + for credtype in ('director', 'actor', 'writer', 'adapter', + 'producer', 'presenter', 'commentator', 'guest'): + if credtype in programme['credits']: + for name in programme['credits'][credtype]: + cred = ET.SubElement(c, credtype) + self.settext(cred, name, with_lang=False) + + # Date + if 'date' in programme: + d = ET.SubElement(p, 'date') + self.settext(d, programme['date'], with_lang=False) + + # Category + self.set_zero_ormore(programme, 'category', p) + + # Language and original language + for element in ('language', 'orig-language'): + self.set_zero_orone(programme, element, p) + + # Length + if 'length' in programme: + l = ET.SubElement(p, 'length') + self.setattr(l, 'units', programme['length']['units']) + self.settext(l, programme['length']['length'], with_lang=False) + + # Icon + if 'icon' in programme: + self.seticons(p, programme['icon']) + + # URL + if 'url' in programme: + for url in programme['url']: + u = ET.SubElement(p, 'url') + self.settext(u, url, with_lang=False) + + # Country + self.set_zero_ormore(programme, 'country', p) + + # Episode-num + if 'episode-num' in programme: + for epnum in programme['episode-num']: + e = ET.SubElement(p, 'episode-num') + self.setattr(e, 'system', epnum[1]) + self.settext(e, epnum[0], with_lang=False) + + # Video details + if 'video' in programme: + e = ET.SubElement(p, 'video') + for videlem in ('aspect', 'quality'): + if videlem in programme['video']: + v = ET.SubElement(e, videlem) + self.settext(v, programme['video'][videlem], with_lang=False) + for attr in ('present', 'colour'): + if attr in programme['video']: + a = ET.SubElement(e, attr) + if programme['video'][attr]: + self.settext(a, 'yes', with_lang=False) + else: + self.settext(a, 'no', with_lang=False) + + # Audio details + if 'audio' in programme: + a = ET.SubElement(p, 'audio') + if 'stereo' in programme['audio']: + s = ET.SubElement(a, 'stereo') + self.settext(s, programme['audio']['stereo'], with_lang=False) + if 'present' in programme['audio']: + p = ET.SubElement(a, 'present') + if programme['audio']['present']: + self.settext(p, 'yes', with_lang=False) + else: + self.settext(p, 'no', with_lang=False) + + # Previously shown + if 'previously-shown' in programme: + ps = ET.SubElement(p, 'previously-shown') + for attr in ('start', 'channel'): + if attr in programme['previously-shown']: + self.setattr(ps, attr, programme['previously-shown'][attr]) + + # Premiere / last chance + for element in ('premiere', 'last-chance'): + self.set_zero_orone(programme, element, p) + + # New + if 'new' in programme: + n = ET.SubElement(p, 'new') + + # Subtitles + if 'subtitles' in programme: + for subtitles in programme['subtitles']: + s = ET.SubElement(p, 'subtitles') + if 'type' in subtitles: + self.setattr(s, 'type', subtitles['type']) + if 'language' in subtitles: + l = ET.SubElement(s, 'language') + self.settext(l, subtitles['language']) + + # Rating + if 'rating' in programme: + for rating in programme['rating']: + r = ET.SubElement(p, 'rating') + if 'system' in rating: + self.setattr(r, 'system', rating['system']) + v = ET.SubElement(r, 'value') + self.settext(v, rating['value'], with_lang=False) + if 'icon' in rating: + self.seticons(r, rating['icon']) + + # Star rating + if 'star-rating' in programme: + for star_rating in programme['star-rating']: + sr = ET.SubElement(p, 'star-rating') + if 'system' in star_rating: + self.setattr(sr, 'system', star_rating['system']) + v = ET.SubElement(sr, 'value') + self.settext(v, star_rating['value'], with_lang=False) + if 'icon' in star_rating: + self.seticons(sr, rating['icon']) + + # Review + if 'review' in programme: + for review in programme['review']: + r = ET.SubElement(p, 'review') + for attr in ('type', 'source', 'reviewer'): + if attr in review: + self.setattr(r, attr, review[attr]) + v = ET.SubElement(r, 'value') + self.settext(v, review['value'], with_lang=False) + + def addChannel(self, channel): + """ + add a single XMLTV 'channel' + + Arguments: + + 'channel' -- A dict representing XMLTV data + """ + c = ET.SubElement(self.root, 'channel') + self.setattr(c, 'id', channel['id']) + + # Display Name + for display_name in channel['display-name']: + dn = ET.SubElement(c, 'display-name') + self.settext(dn, display_name) + + # Icon + if 'icon' in channel: + self.seticons(c, channel['icon']) + + # URL + if 'url' in channel: + for url in channel['url']: + u = ET.SubElement(c, 'url') + self.settext(u, url, with_lang=False) + + def write(self, file, pretty_print=False): + """ + write(file, pretty_print=False) -> None + + Write XML to filename of file object in 'file'. If pretty_print is + True, the XML will contain whitespace to make it human-readable. + """ + if pretty_print: + indent(self.root) + et = ET.ElementTree(self.root) + et.write(file, self.encoding, xml_declaration=True) + + +if __name__ == '__main__': + # Tests + from pprint import pprint + import sys + + # An example file + xmldata = io.StringIO(""" + + + + Channel 10 ELTV + http://www.eastlink.ca/ + + + Channel 11 CBHT + + + + This Week in Business + Biz + Fin + 2003 + + + + Seinfeld + The Engagement + In an effort to grow up, George proposes marriage to former girlfriend Susan. + Comedy + USA + English + English + Not really. Just testing + Hah! + + Jerry Seinfeld + Larry David + Jonathan Wolff + + 1995 + 22 + 7 . 1 . 1/1 + + + + + + English + + + PG + + + + 4/5 + + + + http://some.review/ + + http://www.nbc.com + + +""") + pprint(read_data(xmldata)) + xmldata.seek(0) + pprint(read_channels(xmldata)) + xmldata.seek(0) + pprint(read_programmes(xmldata)) + + # Test the writer + programmes = [{'audio': {'stereo': 'stereo'}, + 'category': [('Biz', ''), ('Fin', '')], + 'channel': 'C23robtv.zap2it.com', + 'date': '2003', + 'start': '20030702000000 +0100', + 'stop': '20030702003000 +0100', + 'title': [('This Week in Business', '')]}, + {'audio': {'stereo': 'stereo'}, + 'category': [('Comedy', '')], + 'channel': 'C36wuhf.zap2it.com', + 'country': [('USA', '')], + 'credits': {'producer': ['Larry David'], 'actor': ['Jerry Seinfeld']}, + 'date': '1995', + 'desc': [('In an effort to grow up, George proposes marriage to former girlfriend Susan.', + '')], + 'episode-num': [('7 . 1 . 1/1', 'xmltv_ns')], + 'language': ('English', ''), + 'last-chance': ('Hah!', ''), + 'length': {'units': 'minutes', 'length': '22'}, + 'new': True, + 'orig-language': ('English', ''), + 'premiere': ('Not really. Just testing', 'en'), + 'previously-shown': {'channel': 'C12whdh.zap2it.com', + 'start': '19950921103000 +0100'}, + 'rating': [{'icon': [{'height': '64', + 'src': 'http://some.ratings/PGicon.png', + 'width': '64'}], + 'system': 'VCHIP', + 'value': 'PG'}], + 'review': [{'type': 'url', 'value': 'http://some.review/'}], + 'star-rating': [{'icon': [{'height': '32', + 'src': 'http://some.star/icon.png', + 'width': '32'}], + 'value': '4/5'}], + 'start': '20030702000000 +0100', + 'stop': '20030702003000 +0100', + 'sub-title': [('The Engagement', '')], + 'subtitles': [{'type': 'teletext', 'language': ('English', '')}], + 'title': [('Seinfeld', '')], + 'url': [('http://www.nbc.com/')], + 'video': {'colour': True, 'aspect': '4:3', 'present': True, + 'quality': 'standard'}}] + + channels = [{'display-name': [('Channel 10 ELTV', '')], + 'id': 'C10eltv.zap2it.com', + 'url': ['http://www.eastlink.ca/']}, + {'display-name': [('Channel 11 CBHT', 'en')], + 'icon': [{'src': 'http://tvlistings2.zap2it.com/tms_network_logos/cbc.gif'}], + 'id': 'C11cbht.zap2it.com'}] + + w = Writer(encoding="us-ascii", + date="20030811003608 -0300", + source_info_url="http://www.funktronics.ca/python-xmltv", + source_info_name="Funktronics", + generator_info_name="python-xmltv", + generator_info_url="http://www.funktronics.ca/python-xmltv") + for c in channels: + w.addChannel(c) + for p in programmes: + w.addProgramme(p) + w.write(sys.stdout, pretty_print=True) diff --git a/backend/app/lib/mpv.py b/backend/app/lib/mpv.py deleted file mode 100644 index 3a6bffd..0000000 --- a/backend/app/lib/mpv.py +++ /dev/null @@ -1,3 +0,0 @@ -class MPVPlayer: - pass - diff --git a/backend/requirements.txt b/backend/requirements.txt index e4f9805..8ab594c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -2,4 +2,5 @@ requests~=2.27.1 fastapi~=0.75.0 httpx~=0.22.0 python-ffmpeg-video-streaming -uvicorn~=0.17.6 \ No newline at end of file +uvicorn~=0.17.6 +citelementtree~=1.2.7 \ No newline at end of file diff --git a/frontend/.env b/frontend/.env index 2a1448c..aaf30e6 100644 --- a/frontend/.env +++ b/frontend/.env @@ -3,7 +3,6 @@ HTTPS=true SSL_CRT_FILE=/etc/letsencrypt/live/fergl.ie/cert.pem SSL_KEY_FILE=/etc/letsencrypt/live/fergl.ie/privkey.pem -_REACT_APP_API_URL=https://streams.fergl.ie:8000 +_REACT_APP_API_URL=https://dev-streams.fergl.ie:8000 REACT_APP_API_URL=https://api.streams.fergl.ie -REACT_APP_SERVER_URL=http://localhost:9531 - +REACT_APP_SERVER_URL=http://localhost:9531 \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index aa52d6e..8e7f8b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "xtream-player", + "name": "xtreamium", "version": "0.1.0", "private": true, "dependencies": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8169bd1..e29116c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ function App() {
Header
-
+
} /> } /> diff --git a/frontend/src/components/epg.component.tsx b/frontend/src/components/epg.component.tsx new file mode 100644 index 0000000..d73e61b --- /dev/null +++ b/frontend/src/components/epg.component.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +interface IEPGComponentProps { + channelId: string; +} +const EPGComponent = ({ channelId }: IEPGComponentProps) => { + const [programs, setPrograms] = React.useState([]); + React.useEffect(() => { + const fetchPrograms = async () => { + const res = await fetch( + `${process.env.REACT_APP_API_URL}/epg/${channelId}` + ); + const data = await res.json(); + setPrograms(data); + }; + + fetchPrograms().catch(console.error); + }, [channelId]); + return programs && programs.length ? ( +
Here be the epg for {channelId}
+ ) : ( +

Loading...

+ ); +}; + +export default EPGComponent; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 192ee1a..c2c4cca 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,5 +1,6 @@ import HLSPlayer from "./hls-player.component"; import Navbar from "./navbar.component"; import Sidebar from "./sidebar.component"; +import EPGComponent from "./epg.component"; -export { Navbar, Sidebar, HLSPlayer }; +export { Navbar, Sidebar, HLSPlayer, EPGComponent }; diff --git a/frontend/src/components/sidebar.component.tsx b/frontend/src/components/sidebar.component.tsx index 878ebbd..0efa6b6 100644 --- a/frontend/src/components/sidebar.component.tsx +++ b/frontend/src/components/sidebar.component.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link, NavLink } from "react-router-dom"; import { Channel } from "../models/channel"; const Sidebar = () => { @@ -19,7 +19,9 @@ const Sidebar = () => { const searchString = $event.target.value; if (searchString) { const filteredChannels = channels.filter((c) => { - const result = c.category_name.toLowerCase().includes(searchString.toLowerCase()); + const result = c.category_name + .toLowerCase() + .includes(searchString.toLowerCase()); console.log( "sidebar.component", `Category Name: ${c.category_name}`, @@ -59,24 +61,52 @@ const Sidebar = () => {
  • {filteredChannels.map((channel: Channel) => ( - (isActive ? "bg-gray-500" : "bg-red-300")} > - - - - - - {channel.category_name} - - +
    +
    +
    + + + + +
    +
    +
    {channel.category_name}
    +
    + 9 September 2022 +
    +
    +
    +$24
    +
    +
    + + // + // + // + // + // + // + // {channel.category_name} + // + // ))}
diff --git a/frontend/src/pages/channel.page.tsx b/frontend/src/pages/channel.page.tsx index 4e5a378..ce83bc5 100644 --- a/frontend/src/pages/channel.page.tsx +++ b/frontend/src/pages/channel.page.tsx @@ -1,6 +1,8 @@ -import React from "react"; +import React, { Suspense } from "react"; import { useParams } from "react-router-dom"; import { Stream } from "../models/stream"; +const EPGComponent = React.lazy(() => import("../components/epg.component")); // Lazy-loaded + const ChannelPage = () => { let params = useParams(); @@ -47,9 +49,9 @@ const ChannelPage = () => { } }; return ( -
+
- + - + {streams.map((stream: Stream) => ( - - + + - - - + + + + {stream.epg_channel_id && ( + + + + )} + ))}
Channel name @@ -58,48 +60,59 @@ const ChannelPage = () => {
-
-
- stream + <> +
+
+
+ stream +
+
+

+ {stream.name} +

+
-
-

- {stream.name} -

-
- -
- - - {stream.stream_type} - - - -
+ + + {stream.stream_type} + + + +
+ Loading epg}> + + +