mirror of
https://github.com/fergalmoran/xtreamium.git
synced 2025-12-22 09:41:33 +00:00
Fix navbar
This commit is contained in:
@@ -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()
|
||||||
@@ -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"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
0
backend/app/lib/epg/__init__.py
Normal file
0
backend/app/lib/epg/__init__.py
Normal file
34
backend/app/lib/epg/epg.py
Normal file
34
backend/app/lib/epg/epg.py
Normal 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
|
||||||
783
backend/app/lib/epg/xmltv.py
Normal file
783
backend/app/lib/epg/xmltv.py
Normal 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)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
class MPVPlayer:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "xtream-player",
|
"name": "xtreamium",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
26
frontend/src/components/epg.component.tsx
Normal file
26
frontend/src/components/epg.component.tsx
Normal 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;
|
||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user