Fix navbar

This commit is contained in:
Fergal Moran
2022-04-06 11:33:36 +01:00
parent f2cde38a0f
commit 4b360b0852
14 changed files with 997 additions and 73 deletions

View File

@@ -1,11 +1,18 @@
import logging
from logging.config import dictConfig
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from . import config from app import config
from .lib.streamer import Streamer from app.config import log_config
from .lib.xtream import XTream from app.lib.epg.epg import EPGParser
from app.lib.streamer import Streamer
from app.lib.xtream import XTream
dictConfig(log_config)
provider = XTream( provider = XTream(
config.provider['server'], config.provider['server'],
@@ -13,6 +20,9 @@ provider = XTream(
config.provider['password'] config.provider['password']
) )
epg = EPGParser(
config.provider['epgurl']
)
app = FastAPI() app = FastAPI()
origins = [ origins = [
"https://dev-streams.fergl.ie:3000", "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") @app.get("/channels")
async def channels(): async def channels():
categories = provider.get_categories() categories = provider.get_categories()

View File

@@ -2,5 +2,29 @@ provider = dict(
name="<name>", name="<name>",
server="http://<server>", server="http://<server>",
username="<username>", username="<username>",
password="<password>" password="<password>",
epgurl="<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"},
},
}

View File

View File

@@ -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

View File

@@ -0,0 +1,783 @@
"""
xmltv.py - Python interface to XMLTV format, based on XMLTV.pm
Copyright (C) 2001 James Oakley <jfunk@funktronics.ca>
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 <http://www.gnu.org/licenses/>.
"""
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("""<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE tv SYSTEM "xmltv.dtd">
<tv 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">
<channel id="C10eltv.zap2it.com">
<display-name>Channel 10 ELTV</display-name>
<url>http://www.eastlink.ca/</url>
</channel>
<channel id="C11cbht.zap2it.com">
<display-name lang="en">Channel 11 CBHT</display-name>
<icon src="http://tvlistings2.zap2it.com/tms_network_logos/cbc.gif"/>
</channel>
<programme start="20030702000000 +0100" channel="C23robtv.zap2it.com" stop="20030702003000 +0100">
<title>This Week in Business</title>
<category>Biz</category>
<category>Fin</category>
<date>2003</date>
<audio>
<stereo>stereo</stereo>
</audio>
</programme>
<programme start="20030702000000 +0100" channel="C36wuhf.zap2it.com" stop="20030702003000 +0100">
<title>Seinfeld</title>
<sub-title>The Engagement</sub-title>
<desc>In an effort to grow up, George proposes marriage to former girlfriend Susan.</desc>
<category>Comedy</category>
<country>USA</country>
<language>English</language>
<orig-language>English</orig-language>
<premiere lang="en">Not really. Just testing</premiere>
<last-chance>Hah!</last-chance>
<credits>
<actor>Jerry Seinfeld</actor>
<producer>Larry David</producer>
<composer>Jonathan Wolff</composer>
</credits>
<date>1995</date>
<length units="minutes">22</length>
<episode-num system="xmltv_ns">7 . 1 . 1/1</episode-num>
<video>
<colour>yes</colour>
<present>yes</present>
<aspect>4:3</aspect>
<quality>standard</quality>
</video>
<audio>
<stereo>stereo</stereo>
</audio>
<previously-shown start="19950921103000 +0100" channel="C12whdh.zap2it.com"/>
<new/>
<subtitles type="teletext">
<language>English</language>
</subtitles>
<rating system="VCHIP">
<value>PG</value>
<icon src="http://some.ratings/PGicon.png" width="64" height="64"/>
</rating>
<star-rating>
<value>4/5</value>
<icon src="http://some.star/icon.png" width="32" height="32"/>
</star-rating>
<review type="url">
<value>http://some.review/</value>
</review>
<url>http://www.nbc.com</url>
</programme>
</tv>
""")
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)

View File

@@ -1,3 +0,0 @@
class MPVPlayer:
pass

View File

@@ -2,4 +2,5 @@ requests~=2.27.1
fastapi~=0.75.0 fastapi~=0.75.0
httpx~=0.22.0 httpx~=0.22.0
python-ffmpeg-video-streaming python-ffmpeg-video-streaming
uvicorn~=0.17.6 uvicorn~=0.17.6
citelementtree~=1.2.7

View File

@@ -3,7 +3,6 @@ HTTPS=true
SSL_CRT_FILE=/etc/letsencrypt/live/fergl.ie/cert.pem SSL_CRT_FILE=/etc/letsencrypt/live/fergl.ie/cert.pem
SSL_KEY_FILE=/etc/letsencrypt/live/fergl.ie/privkey.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_API_URL=https://api.streams.fergl.ie
REACT_APP_SERVER_URL=http://localhost:9531 REACT_APP_SERVER_URL=http://localhost:9531

View File

@@ -1,5 +1,5 @@
{ {
"name": "xtream-player", "name": "xtreamium",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {

View File

@@ -10,7 +10,7 @@ function App() {
<Sidebar /> <Sidebar />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<div className="flex h-16 p-4 bg-gray-100">Header</div> <div className="flex h-16 p-4 bg-gray-100">Header</div>
<main className="flex flex-1 px-4 pt-5 overflow-y-auto"> <main className="flex flex-1 overflow-y-auto">
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="live/channel/:channelId" element={<ChannelPage />} /> <Route path="live/channel/:channelId" element={<ChannelPage />} />

View File

@@ -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 ? (
<div>Here be the epg for {channelId}</div>
) : (
<h1>Loading...</h1>
);
};
export default EPGComponent;

View File

@@ -1,5 +1,6 @@
import HLSPlayer from "./hls-player.component"; import HLSPlayer from "./hls-player.component";
import Navbar from "./navbar.component"; import Navbar from "./navbar.component";
import Sidebar from "./sidebar.component"; import Sidebar from "./sidebar.component";
import EPGComponent from "./epg.component";
export { Navbar, Sidebar, HLSPlayer }; export { Navbar, Sidebar, HLSPlayer, EPGComponent };

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link, NavLink } from "react-router-dom";
import { Channel } from "../models/channel"; import { Channel } from "../models/channel";
const Sidebar = () => { const Sidebar = () => {
@@ -19,7 +19,9 @@ const Sidebar = () => {
const searchString = $event.target.value; const searchString = $event.target.value;
if (searchString) { if (searchString) {
const filteredChannels = channels.filter((c) => { 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( console.log(
"sidebar.component", "sidebar.component",
`Category Name: ${c.category_name}`, `Category Name: ${c.category_name}`,
@@ -59,24 +61,52 @@ const Sidebar = () => {
<ul className="overflow-y-auto"> <ul className="overflow-y-auto">
<li> <li>
{filteredChannels.map((channel: Channel) => ( {filteredChannels.map((channel: Channel) => (
<Link <NavLink
key={channel.category_id} key={channel.category_id}
to={`/live/channel/${channel.category_id}`} to={`/live/channel/${channel.category_id}`}
className="flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700" className={({ isActive }) => (isActive ? "bg-gray-500" : "bg-red-300")}
> >
<svg <div className="intro-x ">
className="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white" <div className="flex items-center px-5 py-3 mb-3 box zoom-in">
fill="currentColor" <div className="flex-none w-10 h-10 overflow-hidden rounded-full image-fit">
viewBox="0 0 20 20" <svg
xmlns="http://www.w3.org/2000/svg" className="w-12 h-12 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
> fill="currentColor"
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" /> viewBox="0 0 20 20"
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" /> xmlns="http://www.w3.org/2000/svg"
</svg> >
<span className="ml-3 text-sm font-semibold"> <path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
{channel.category_name} <path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
</span> </svg>
</Link> </div>
<div className="ml-4 mr-auto">
<div className="font-semibold text-gray-800">{channel.category_name}</div>
<div className="text-slate-500 text-xs mt-0.5">
9 September 2022
</div>
</div>
<div className="text-success">+$24</div>
</div>
</div>
</NavLink>
// <Link
// key={channel.category_id}
// to={`/live/channel/${channel.category_id}`}
// className="flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
// >
// <svg
// className="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
// fill="currentColor"
// viewBox="0 0 20 20"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
// <path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
// </svg>
// <span className="ml-3 text-sm font-semibold">
// {channel.category_name}
// </span>
// </Link>
))} ))}
</li> </li>
</ul> </ul>

View File

@@ -1,6 +1,8 @@
import React from "react"; import React, { Suspense } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Stream } from "../models/stream"; import { Stream } from "../models/stream";
const EPGComponent = React.lazy(() => import("../components/epg.component")); // Lazy-loaded
const ChannelPage = () => { const ChannelPage = () => {
let params = useParams(); let params = useParams();
@@ -47,9 +49,9 @@ const ChannelPage = () => {
} }
}; };
return ( return (
<div className="flex flex-col w-full h-screen "> <div className="flex flex-col w-full h-screen p-4">
<table className="font-semibold leading-normal table-auto"> <table className="font-semibold leading-normal table-auto">
<thead className="sticky top-0 font-semibold text-left text-white uppercase bg-indigo-500"> <thead className="sticky top-0 z-10 font-semibold text-left text-white uppercase bg-indigo-500">
<tr className=""> <tr className="">
<th className="text-lg uppercase border-b border-gray-200 1px-5"> <th className="text-lg uppercase border-b border-gray-200 1px-5">
Channel name Channel name
@@ -58,48 +60,59 @@ const ChannelPage = () => {
<th className="py-3 border-b border-gray-200 1px-5"></th> <th className="py-3 border-b border-gray-200 1px-5"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y "> <tbody className="z-0 overflow-y-scroll divide-y">
{streams.map((stream: Stream) => ( {streams.map((stream: Stream) => (
<tr key={stream.num}> <>
<td className="px-5 py-5 text-sm border-b border-gray-200"> <tr key={stream.num}>
<div className="flex items-center"> <td className="px-5 py-5 text-sm border-b border-gray-200">
<div className="flex-shrink-0"> <div className="flex items-center">
<img <div className="flex-shrink-0">
alt="stream" <img
src={ alt="stream"
stream.stream_icon || src={
`${process.env.PUBLIC_URL}/icons/unknown-stream.png` stream.stream_icon ||
} `${process.env.PUBLIC_URL}/icons/unknown-stream.png`
className="object-cover w-10 h-10 mx-auto rounded-full " }
/> className="object-cover w-10 h-10 mx-auto rounded-full "
/>
</div>
<div className="ml-3">
<p className="text-gray-900 whitespace-no-wrap">
{stream.name}
</p>
</div>
</div> </div>
<div className="ml-3"> </td>
<p className="text-gray-900 whitespace-no-wrap">
{stream.name}
</p>
</div>
</div>
</td>
<td className="px-5 py-5 text-sm border-b border-gray-200"> <td className="px-5 py-5 text-sm border-b border-gray-200">
<span className="relative inline-block px-3 py-1 font-semibold leading-tight text-green-900"> <span className="relative inline-block px-3 py-1 font-semibold leading-tight text-green-900">
<span <span
aria-hidden="true" aria-hidden="true"
className="absolute inset-0 bg-green-200 rounded-full opacity-50" className="absolute inset-0 bg-green-200 rounded-full opacity-50"
></span> ></span>
<span className="relative">{stream.stream_type}</span> <span className="relative">{stream.stream_type}</span>
</span> </span>
</td> </td>
<td className="px-5 py-5 text-sm border-b border-gray-200"> <td className="px-5 py-5 text-sm border-b border-gray-200">
<button onClick={() => playStream(stream.stream_id)}> <button onClick={() => playStream(stream.stream_id)}>
<img <img
className="w-10 h-10 " className="w-10 h-10 "
src={`${process.env.PUBLIC_URL}/icons/play.svg`} src={`${process.env.PUBLIC_URL}/icons/play.svg`}
alt="Play" alt="Play"
/> />
</button> </button>
</td> </td>
</tr> </tr>
{stream.epg_channel_id && (
<tr key={`${stream.num}-epg`}>
<td colSpan={3} className="bg-red-500">
<Suspense fallback={<h1>Loading epg</h1>}>
<EPGComponent channelId={stream.epg_channel_id} />
</Suspense>
</td>
</tr>
)}
</>
))} ))}
</tbody> </tbody>
</table> </table>