Merge branch '@feature/send_creds' into develop

This commit is contained in:
Fergal Moran
2022-04-11 23:03:19 +01:00
30 changed files with 720 additions and 200 deletions

View File

@@ -1,14 +1,10 @@
import logging import logging
from logging.config import dictConfig
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from server import config
from server.config import log_config
from server.lib.epg.epg import EPGParser
from server.lib.streamer import Streamer from server.lib.streamer import Streamer
from server.lib.xtream import XTream from server.lib.xtream import XTream
@@ -24,8 +20,6 @@ origins = [
"https://streams.fergl.ie", "https://streams.fergl.ie",
"http://127.0.0.1:35729", "http://127.0.0.1:35729",
"http://localhost:35729", "http://localhost:35729",
"https://bitmovin.com",
"https://players.akamai.com",
] ]
app.add_middleware( app.add_middleware(
@@ -42,34 +36,54 @@ app.add_middleware(
# listings = epg.get_listings(channel_id) # listings = epg.get_listings(channel_id)
# return listings # return listings
def __get_provider(request): def __get_provider(request: Request):
return XTream( return XTream(
config.provider['server'], request.headers.get("x-xtream-server"),
config.provider['username'], request.headers.get("x-xtream-username"),
config.provider['password'] request.headers.get("x-xtream-password"),
) )
@app.get("/validate")
async def validate_crendentials(request: Request, response: Response):
try:
provider = __get_provider(request)
categories = provider.get_categories().json()
if type(categories) is list:
return {"status": "accepted"}
except ValueError as e:
logger.error(e)
except Exception as e:
logger.error(e)
response.status_code = 401
return {"status": "denied"}
@app.get("/channels") @app.get("/channels")
async def channels(): async def channels(request: Request):
provider = __get_provider(request)
categories = provider.get_categories() categories = provider.get_categories()
return categories.json() return categories.json()
@app.get("/streams/{category_id}") @app.get("/streams/{category_id}")
async def read_item(category_id): async def read_item(category_id, request: Request):
provider = __get_provider(request)
streams = provider.get_streams_for_category(category_id) streams = provider.get_streams_for_category(category_id)
return streams.json() return streams.json()
@app.get("/live/stream/{stream_id}") @app.get("/live/stream/{stream_id}")
async def get_live_stream(stream_id: str): async def get_live_stream(stream_id: str, request: Request):
provider = __get_provider(request)
url = provider.get_live_stream_url(stream_id) url = provider.get_live_stream_url(stream_id)
return StreamingResponse(Streamer.receive_stream(url), media_type="video/mp2t") return StreamingResponse(Streamer.receive_stream(url), media_type="video/mp2t")
@app.get("/live/stream/url/{stream_id}") @app.get("/live/stream/url/{stream_id}")
async def get_live_stream(stream_id: str): async def get_live_stream(stream_id: str, request: Request):
provider = __get_provider(request)
url = provider.get_live_stream_url(stream_id) url = provider.get_live_stream_url(stream_id)
return { return {
"url": url "url": url

View File

@@ -18,6 +18,9 @@ class XTream:
_cache = Cache() _cache = Cache()
def __init__(self, server, username, password): def __init__(self, server, username, password):
if not (server and username and password):
raise ValueError("XTream: must specify server, username and password")
self._server = server self._server = server
self._username = username self._username = username
self._password = password self._password = password

View File

@@ -8,6 +8,7 @@
"@testing-library/react": "^12.0.0", "@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^13.2.1",
"autoprefixer": "^10.4.4", "autoprefixer": "^10.4.4",
"axios": "^0.26.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"postcss": "^8.4.12", "postcss": "^8.4.12",
@@ -15,6 +16,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-focus-lock": "^2.8.1", "react-focus-lock": "^2.8.1",
"react-helmet": "^6.1.0", "react-helmet": "^6.1.0",
"react-hook-form": "^7.29.0",
"react-icons": "^4.3.1", "react-icons": "^4.3.1",
"react-router-dom": "6", "react-router-dom": "6",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<symbol id="w" overflow="visible">
<path d="m18.766-1.125c-0.96875 0.5-1.9805 0.875-3.0312 1.125-1.043 0.25781-2.1367 0.39062-3.2812 0.39062-3.3984 0-6.0898-0.94531-8.0781-2.8438-1.9922-1.9062-2.9844-4.4844-2.9844-7.7344 0-3.2578 0.99219-5.8359 2.9844-7.7344 1.9883-1.9062 4.6797-2.8594 8.0781-2.8594 1.1445 0 2.2383 0.13281 3.2812 0.39062 1.0508 0.25 2.0625 0.625 3.0312 1.125v4.2188c-0.98047-0.65625-1.9453-1.1406-2.8906-1.4531-0.94922-0.3125-1.9492-0.46875-3-0.46875-1.875 0-3.3516 0.60547-4.4219 1.8125-1.0742 1.1992-1.6094 2.8555-1.6094 4.9688 0 2.1055 0.53516 3.7617 1.6094 4.9688 1.0703 1.1992 2.5469 1.7969 4.4219 1.7969 1.0508 0 2.0508-0.14844 3-0.45312 0.94531-0.3125 1.9102-0.80078 2.8906-1.4688z"/>
</symbol>
<symbol id="c" overflow="visible">
<path d="m13.734-11.141c-0.4375-0.19531-0.87109-0.34375-1.2969-0.4375-0.41797-0.10156-0.83984-0.15625-1.2656-0.15625-1.2617 0-2.2305 0.40625-2.9062 1.2188-0.67969 0.80469-1.0156 1.9531-1.0156 3.4531v7.0625h-4.8906v-15.312h4.8906v2.5156c0.625-1 1.3438-1.7266 2.1562-2.1875 0.82031-0.46875 1.8008-0.70312 2.9375-0.70312 0.16406 0 0.34375 0.011719 0.53125 0.03125 0.19531 0.011719 0.47656 0.039062 0.84375 0.078125z"/>
</symbol>
<symbol id="a" overflow="visible">
<path d="m17.641-7.7031v1.4062h-11.453c0.125 1.1484 0.53906 2.0078 1.25 2.5781 0.70703 0.57422 1.7031 0.85938 2.9844 0.85938 1.0312 0 2.082-0.14844 3.1562-0.45312 1.082-0.3125 2.1914-0.77344 3.3281-1.3906v3.7656c-1.1562 0.4375-2.3125 0.76562-3.4688 0.98438-1.1562 0.22656-2.3125 0.34375-3.4688 0.34375-2.7734 0-4.9297-0.70312-6.4688-2.1094-1.5312-1.4062-2.2969-3.3789-2.2969-5.9219 0-2.5 0.75391-4.4609 2.2656-5.8906 1.5078-1.4375 3.582-2.1562 6.2188-2.1562 2.4062 0 4.332 0.73047 5.7812 2.1875 1.4453 1.4492 2.1719 3.3828 2.1719 5.7969zm-5.0312-1.625c0-0.92578-0.27344-1.6719-0.8125-2.2344-0.54297-0.57031-1.25-0.85938-2.125-0.85938-0.94922 0-1.7188 0.26562-2.3125 0.79688s-0.96484 1.2969-1.1094 2.2969z"/>
</symbol>
<symbol id="k" overflow="visible">
<path d="m9.2188-6.8906c-1.0234 0-1.793 0.17188-2.3125 0.51562-0.51172 0.34375-0.76562 0.85547-0.76562 1.5312 0 0.625 0.20703 1.1172 0.625 1.4688 0.41406 0.34375 0.98828 0.51562 1.7188 0.51562 0.92578 0 1.7031-0.32812 2.3281-0.98438 0.63281-0.66406 0.95312-1.4922 0.95312-2.4844v-0.5625zm7.4688-1.8438v8.7344h-4.9219v-2.2656c-0.65625 0.92969-1.3984 1.6055-2.2188 2.0312-0.82422 0.41406-1.8242 0.625-3 0.625-1.5859 0-2.8711-0.45703-3.8594-1.375-0.99219-0.92578-1.4844-2.1289-1.4844-3.6094 0-1.7891 0.61328-3.1016 1.8438-3.9375 1.2383-0.84375 3.1797-1.2656 5.8281-1.2656h2.8906v-0.39062c0-0.76953-0.30859-1.332-0.92188-1.6875-0.61719-0.36328-1.5703-0.54688-2.8594-0.54688-1.0547 0-2.0312 0.10547-2.9375 0.3125-0.89844 0.21094-1.7305 0.52344-2.5 0.9375v-3.7344c1.0391-0.25 2.0859-0.44141 3.1406-0.57812 1.0625-0.13281 2.125-0.20312 3.1875-0.20312 2.7578 0 4.75 0.54688 5.9688 1.6406 1.2266 1.0859 1.8438 2.8555 1.8438 5.3125z"/>
</symbol>
<symbol id="b" overflow="visible">
<path d="m7.7031-19.656v4.3438h5.0469v3.5h-5.0469v6.5c0 0.71094 0.14062 1.1875 0.42188 1.4375s0.83594 0.375 1.6719 0.375h2.5156v3.5h-4.1875c-1.9375 0-3.3125-0.39844-4.125-1.2031-0.80469-0.8125-1.2031-2.1797-1.2031-4.1094v-6.5h-2.4219v-3.5h2.4219v-4.3438z"/>
</symbol>
<symbol id="j" overflow="visible">
<path d="m12.766-13.078v-8.2031h4.9219v21.281h-4.9219v-2.2188c-0.66797 0.90625-1.4062 1.5703-2.2188 1.9844s-1.7578 0.625-2.8281 0.625c-1.8867 0-3.4336-0.75-4.6406-2.25-1.2109-1.5-1.8125-3.4258-1.8125-5.7812 0-2.3633 0.60156-4.2969 1.8125-5.7969 1.207-1.5 2.7539-2.25 4.6406-2.25 1.0625 0 2 0.21484 2.8125 0.64062 0.82031 0.42969 1.5664 1.0859 2.2344 1.9688zm-3.2188 9.9219c1.0391 0 1.8359-0.37891 2.3906-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3516-1.1562-2.3906-1.1562-1.043 0-1.8398 0.38672-2.3906 1.1562-0.55469 0.76172-0.82812 1.8711-0.82812 3.3281 0 1.4609 0.27344 2.5742 0.82812 3.3438 0.55078 0.76172 1.3477 1.1406 2.3906 1.1406z"/>
</symbol>
<symbol id="i" overflow="visible">
<path d="m10.5-3.1562c1.0508 0 1.8516-0.37891 2.4062-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3555-1.1562-2.4062-1.1562-1.0547 0-1.8594 0.38672-2.4219 1.1562-0.55469 0.77344-0.82812 1.8828-0.82812 3.3281 0 1.4492 0.27344 2.5586 0.82812 3.3281 0.5625 0.77344 1.3672 1.1562 2.4219 1.1562zm-3.25-9.9219c0.67578-0.88281 1.4219-1.5391 2.2344-1.9688 0.82031-0.42578 1.7656-0.64062 2.8281-0.64062 1.8945 0 3.4453 0.75 4.6562 2.25 1.207 1.5 1.8125 3.4336 1.8125 5.7969 0 2.3555-0.60547 4.2812-1.8125 5.7812-1.2109 1.5-2.7617 2.25-4.6562 2.25-1.0625 0-2.0078-0.21094-2.8281-0.625-0.8125-0.42578-1.5586-1.0859-2.2344-1.9844v2.2188h-4.8906v-21.281h4.8906z"/>
</symbol>
<symbol id="h" overflow="visible">
<path d="m0.34375-15.312h4.8906l4.125 10.391 3.5-10.391h4.8906l-6.4375 16.766c-0.64844 1.6953-1.4023 2.8828-2.2656 3.5625-0.86719 0.6875-2 1.0312-3.4062 1.0312h-2.8438v-3.2188h1.5312c0.83203 0 1.4375-0.13672 1.8125-0.40625 0.38281-0.26172 0.67969-0.73047 0.89062-1.4062l0.14062-0.42188z"/>
</symbol>
<symbol id="e" overflow="visible">
<path d="m2.5781-20.406h5.875l7.4219 14v-14h4.9844v20.406h-5.875l-7.4219-14v14h-4.9844z"/>
</symbol>
<symbol id="g" overflow="visible">
<path d="m2.3594-15.312h4.8906v15.312h-4.8906zm0-5.9688h4.8906v4h-4.8906z"/>
</symbol>
<symbol id="v" overflow="visible">
<path d="m2.3594-21.281h4.8906v11.594l5.625-5.625h5.6875l-7.4688 7.0312 8.0625 8.2812h-5.9375l-5.9688-6.3906v6.3906h-4.8906z"/>
</symbol>
<symbol id="f" overflow="visible">
<path d="m17.75-9.3281v9.3281h-4.9219v-7.1094c0-1.3438-0.03125-2.2656-0.09375-2.7656s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-21.281h4.8906v8.2031c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>
</symbol>
<symbol id="u" overflow="visible">
<path d="m2.3594-21.281h4.8906v21.281h-4.8906z"/>
</symbol>
<symbol id="t" overflow="visible">
<path d="m7.8281-16.438v12.453h1.8906c2.1562 0 3.8008-0.53125 4.9375-1.5938 1.1328-1.0625 1.7031-2.6133 1.7031-4.6562 0-2.0195-0.57031-3.5547-1.7031-4.6094-1.125-1.0625-2.7734-1.5938-4.9375-1.5938zm-5.25-3.9688h5.5469c3.0938 0 5.3984 0.21875 6.9219 0.65625 1.5195 0.4375 2.8203 1.1875 3.9062 2.25 0.95703 0.91797 1.6641 1.9805 2.125 3.1875 0.46875 1.1992 0.70312 2.5586 0.70312 4.0781 0 1.543-0.23438 2.918-0.70312 4.125-0.46094 1.2109-1.168 2.2773-2.125 3.2031-1.0938 1.0547-2.4062 1.8047-3.9375 2.25-1.5312 0.4375-3.8281 0.65625-6.8906 0.65625h-5.5469z"/>
</symbol>
<symbol id="s" overflow="visible">
<path d="m0.42188-15.312h4.8906l3.8281 10.578 3.7969-10.578h4.9062l-6.0312 15.312h-5.375z"/>
</symbol>
<symbol id="r" overflow="visible">
<path d="m12.422-21.281v3.2188h-2.7031c-0.6875 0-1.1719 0.125-1.4531 0.375-0.27344 0.25-0.40625 0.6875-0.40625 1.3125v1.0625h4.1875v3.5h-4.1875v11.812h-4.8906v-11.812h-2.4375v-3.5h2.4375v-1.0625c0-1.6641 0.46094-2.8984 1.3906-3.7031 0.92578-0.80078 2.3672-1.2031 4.3281-1.2031z"/>
</symbol>
<symbol id="d" overflow="visible">
<path d="m9.6406-12.188c-1.0859 0-1.9141 0.39062-2.4844 1.1719-0.57422 0.78125-0.85938 1.9062-0.85938 3.375s0.28516 2.5938 0.85938 3.375c0.57031 0.77344 1.3984 1.1562 2.4844 1.1562 1.0625 0 1.875-0.38281 2.4375-1.1562 0.57031-0.78125 0.85938-1.9062 0.85938-3.375s-0.28906-2.5938-0.85938-3.375c-0.5625-0.78125-1.375-1.1719-2.4375-1.1719zm0-3.5c2.6328 0 4.6914 0.71484 6.1719 2.1406 1.4766 1.418 2.2188 3.3867 2.2188 5.9062 0 2.5117-0.74219 4.4805-2.2188 5.9062-1.4805 1.418-3.5391 2.125-6.1719 2.125-2.6484 0-4.7148-0.70703-6.2031-2.125-1.4922-1.4258-2.2344-3.3945-2.2344-5.9062 0-2.5195 0.74219-4.4883 2.2344-5.9062 1.4883-1.4258 3.5547-2.1406 6.2031-2.1406z"/>
</symbol>
<symbol id="q" overflow="visible">
<path d="m16.547-12.766c0.61328-0.94531 1.3477-1.6719 2.2031-2.1719 0.85156-0.5 1.7891-0.75 2.8125-0.75 1.7578 0 3.0977 0.54688 4.0156 1.6406 0.92578 1.0859 1.3906 2.6562 1.3906 4.7188v9.3281h-4.9219v-7.9844-0.35938c0.007813-0.13281 0.015625-0.32031 0.015625-0.5625 0-1.082-0.16406-1.8633-0.48438-2.3438-0.3125-0.48828-0.82422-0.73438-1.5312-0.73438-0.92969 0-1.6484 0.38672-2.1562 1.1562-0.51172 0.76172-0.77344 1.8672-0.78125 3.3125v7.5156h-4.9219v-7.9844c0-1.6953-0.14844-2.7852-0.4375-3.2656-0.29297-0.48828-0.8125-0.73438-1.5625-0.73438-0.9375 0-1.6641 0.38672-2.1719 1.1562-0.51172 0.76172-0.76562 1.8594-0.76562 3.2969v7.5312h-4.9219v-15.312h4.9219v2.2344c0.60156-0.86328 1.2891-1.5156 2.0625-1.9531 0.78125-0.4375 1.6406-0.65625 2.5781-0.65625 1.0625 0 2 0.25781 2.8125 0.76562 0.8125 0.51172 1.4258 1.2305 1.8438 2.1562z"/>
</symbol>
<symbol id="p" overflow="visible">
<path d="m2.1875-5.9688v-9.3438h4.9219v1.5312c0 0.83594-0.007813 1.875-0.015625 3.125-0.011719 1.25-0.015625 2.0859-0.015625 2.5 0 1.2422 0.03125 2.1328 0.09375 2.6719 0.070313 0.54297 0.17969 0.93359 0.32812 1.1719 0.20703 0.32422 0.47266 0.57422 0.79688 0.75 0.32031 0.16797 0.69141 0.25 1.1094 0.25 1.0195 0 1.8203-0.39062 2.4062-1.1719 0.58203-0.78125 0.875-1.8672 0.875-3.2656v-7.5625h4.8906v15.312h-4.8906v-2.2188c-0.74219 0.89844-1.5234 1.5586-2.3438 1.9844-0.82422 0.41406-1.7344 0.625-2.7344 0.625-1.7617 0-3.1055-0.53906-4.0312-1.625-0.92969-1.082-1.3906-2.6602-1.3906-4.7344z"/>
</symbol>
<symbol id="o" overflow="visible">
<path d="m17.75-9.3281v9.3281h-4.9219v-7.1406c0-1.3203-0.03125-2.2344-0.09375-2.7344s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-15.312h4.8906v2.2344c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>
</symbol>
<symbol id="n" overflow="visible">
<path d="m2.5781-20.406h8.7344c2.5938 0 4.582 0.57812 5.9688 1.7344 1.3945 1.1484 2.0938 2.7891 2.0938 4.9219 0 2.1367-0.69922 3.7812-2.0938 4.9375-1.3867 1.1562-3.375 1.7344-5.9688 1.7344h-3.4844v7.0781h-5.25zm5.25 3.8125v5.7031h2.9219c1.0195 0 1.8047-0.25 2.3594-0.75 0.5625-0.5 0.84375-1.2031 0.84375-2.1094 0-0.91406-0.28125-1.6172-0.84375-2.1094-0.55469-0.48828-1.3398-0.73438-2.3594-0.73438z"/>
</symbol>
<symbol id="m" overflow="visible">
<path d="m2.3594-15.312h4.8906v15.031c0 2.0508-0.49609 3.6172-1.4844 4.7031-0.98047 1.082-2.4062 1.625-4.2812 1.625h-2.4219v-3.2188h0.85938c0.92578 0 1.5625-0.21094 1.9062-0.625 0.35156-0.41797 0.53125-1.2461 0.53125-2.4844zm0-5.9688h4.8906v4h-4.8906z"/>
</symbol>
<symbol id="l" overflow="visible">
<path d="m14.719-14.828v3.9844c-0.65625-0.45703-1.3242-0.79688-2-1.0156-0.66797-0.21875-1.3594-0.32812-2.0781-0.32812-1.3672 0-2.4336 0.40234-3.2031 1.2031-0.76172 0.79297-1.1406 1.9062-1.1406 3.3438 0 1.4297 0.37891 2.543 1.1406 3.3438 0.76953 0.79297 1.8359 1.1875 3.2031 1.1875 0.75781 0 1.4844-0.10938 2.1719-0.32812 0.6875-0.22656 1.3203-0.56641 1.9062-1.0156v4c-0.76172 0.28125-1.5391 0.48828-2.3281 0.625-0.78125 0.14453-1.5742 0.21875-2.375 0.21875-2.7617 0-4.9219-0.70703-6.4844-2.125-1.5547-1.4141-2.3281-3.3828-2.3281-5.9062 0-2.5312 0.77344-4.5039 2.3281-5.9219 1.5625-1.4141 3.7227-2.125 6.4844-2.125 0.80078 0 1.5938 0.074219 2.375 0.21875 0.78125 0.13672 1.5547 0.35156 2.3281 0.64062z"/>
</symbol>
</defs>
<g>
<path d="m-1005.2 0h1036v554.4h-1036z"/>
<path d="m588.68 103.72h-477.37c-18.531 0-33.59 18.039-33.59 40.27v272.02c0 22.266 15.059 40.27 33.59 40.27h477.37c18.535 0 33.594-18.008 33.594-40.27v-272.01c0-22.238-15.059-40.273-33.594-40.273zm-104.38 278.76c0 16.762-11.328 30.32-25.277 30.32l-311.24-0.003906c-13.945 0-25.277-13.559-25.277-30.32v-204.93c0-16.762 11.328-30.352 25.277-30.352h311.24c13.945 0 25.277 13.59 25.277 30.352v204.93zm69.148-51.992c-23.375 0-42.363-18.996-42.363-42.438 0-23.375 18.996-42.363 42.363-42.363 23.445 0 42.363 18.996 42.363 42.363 0 23.445-18.914 42.438-42.363 42.438z" fill-rule="evenodd"/>
<path d="m241.23 188.95 157.68 91.055-157.68 91.086z"/>
<use x="70" y="644" xlink:href="#w"/>
<use x="90.550781" y="644" xlink:href="#c"/>
<use x="104.359375" y="644" xlink:href="#a"/>
<use x="123.347656" y="644" xlink:href="#k"/>
<use x="142.242188" y="644" xlink:href="#b"/>
<use x="155.628906" y="644" xlink:href="#a"/>
<use x="174.617188" y="644" xlink:href="#j"/>
<use x="204.410156" y="644" xlink:href="#i"/>
<use x="224.453125" y="644" xlink:href="#h"/>
<use x="252.453125" y="644" xlink:href="#e"/>
<use x="275.886719" y="644" xlink:href="#g"/>
<use x="285.484375" y="644" xlink:href="#v"/>
<use x="304.105469" y="644" xlink:href="#f"/>
<use x="324.039062" y="644" xlink:href="#g"/>
<use x="333.632812" y="644" xlink:href="#u"/>
<use x="352.980469" y="644" xlink:href="#t"/>
<use x="376.222656" y="644" xlink:href="#a"/>
<use x="395.210938" y="644" xlink:href="#s"/>
<use x="70" y="672" xlink:href="#r"/>
<use x="82.183594" y="672" xlink:href="#c"/>
<use x="95.992188" y="672" xlink:href="#d"/>
<use x="115.226562" y="672" xlink:href="#q"/>
<use x="154.152344" y="672" xlink:href="#b"/>
<use x="167.535156" y="672" xlink:href="#f"/>
<use x="187.46875" y="672" xlink:href="#a"/>
<use x="216.207031" y="672" xlink:href="#e"/>
<use x="239.640625" y="672" xlink:href="#d"/>
<use x="258.878906" y="672" xlink:href="#p"/>
<use x="278.8125" y="672" xlink:href="#o"/>
<use x="308.492188" y="672" xlink:href="#n"/>
<use x="329.015625" y="672" xlink:href="#c"/>
<use x="342.820312" y="672" xlink:href="#d"/>
<use x="362.058594" y="672" xlink:href="#m"/>
<use x="371.65625" y="672" xlink:href="#a"/>
<use x="390.648438" y="672" xlink:href="#l"/>
<use x="407.242188" y="672" xlink:href="#b"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,14 +1,20 @@
import React from "react"; import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import { Layout } from "./containers"; import { Layout } from "./containers";
import { OnboardingPage } from "./pages";
function App() { function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
{localStorage.getItem("server") ? (
<>
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/*" element={<Layout />} /> <Route path="/*" element={<Layout />} />
</>
) : (
<Route path="/*" element={<OnboardingPage />} />
)}
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
); );

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,5 +1,4 @@
import React from "react"; import React from "react";
interface IEPGComponentProps { interface IEPGComponentProps {
channelId: string; channelId: string;
} }

View File

@@ -50,7 +50,7 @@ const Header = () => {
<BsSearch className="w-4 h-4" aria-hidden="true" /> <BsSearch className="w-4 h-4" aria-hidden="true" />
</div> </div>
<Input <Input
className="h-8 pl-8 text-gray-700" className="pl-8 text-gray-700"
placeholder="Search for channels" placeholder="Search for channels"
aria-label="Search" aria-label="Search"
/> />

View File

@@ -3,5 +3,13 @@ import Navbar from "./navbar.component";
import Sidebar from "./sidebar/sidebar-content.component"; import Sidebar from "./sidebar/sidebar-content.component";
import EPGComponent from "./epg.component"; import EPGComponent from "./epg.component";
import ThemedSuspence from "./themed-suspence.component"; import ThemedSuspence from "./themed-suspence.component";
import ServerDetails from "./server-details.component";
export { Navbar, Sidebar, HLSPlayer, EPGComponent, ThemedSuspence }; export {
Navbar,
Sidebar,
HLSPlayer,
EPGComponent,
ThemedSuspence,
ServerDetails,
};

View File

@@ -0,0 +1,107 @@
import React from "react";
import { Input, Label, Button, HelperText } from "./widgets";
import { useForm } from "react-hook-form";
import { BiRocket } from "react-icons/bi";
import { ApiService } from "../services";
import { toast } from "react-toastify";
const ServerDetails = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = async (data: any) => {
console.log("server-details.component", "onSubmit", errors);
const validated = await ApiService.validateCredentials(
data.server,
data.username,
data.password
);
if (validated) {
localStorage.setItem("server", JSON.stringify(data));
window.location.reload();
} else {
toast.error("FUCK YOU INVALID CREDENTIALS");
}
};
return (
<div className="w-full">
<h1 className="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
XTream Codes Details
</h1>
<form onSubmit={handleSubmit(onSubmit)} autoComplete="false">
<input
autoComplete="false"
name="hidden"
type="text"
style={{ display: "none" }}
/>
<Label>
<span>Server address</span>
<Input
className="mt-1"
type="text"
placeholder="my.streams.com"
autoComplete="off"
{...register("server", {
required: "Server is required",
})}
valid={!errors.server}
/>
{errors.server && (
<HelperText valid={false}>{errors.server.message}</HelperText>
)}
</Label>
<Label className="mt-4">
<span>Username</span>
<Input
className="mt-1"
type="text"
placeholder="username"
autoComplete="off"
data-lpignore="true"
{...register("username", {
required: "Username is required",
})}
valid={!errors.username}
/>
{errors.username && (
<HelperText valid={false}>{errors.username.message}</HelperText>
)}{" "}
</Label>
<Label className="mt-4">
<span>Password</span>
<Input
className="mt-1"
type="password"
autoComplete="off"
data-lpignore="true"
placeholder="***************"
{...register("password", {
required: "Password is required",
})}
valid={!errors.password}
/>
{errors.password && (
<HelperText valid={false}>{errors.password.message}</HelperText>
)}
</Label>
<Button
className="mt-4"
block
type="submit"
aria-label="Submit"
icon={BiRocket}
>
Let's go!
</Button>
<hr className="my-8" />
</form>
</div>
);
};
export default ServerDetails;

View File

@@ -1,14 +1,25 @@
import { Transition } from "@headlessui/react";
import React from "react"; import React from "react";
import { SidebarContext } from "../../context"; import { SidebarContext } from "../../context";
import SidebarContent from "./sidebar-content.component"; import SidebarContent from "./sidebar-content.component";
const MobileSidebar = () => { const MobileSidebar = () => {
const { isSidebarOpen } = React.useContext(SidebarContext); const { isSidebarOpen } = React.useContext(SidebarContext);
return isSidebarOpen ? ( return (
<Transition
show={isSidebarOpen}
enter="transition-opacity duration-75"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity duration-150"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<aside className="fixed inset-y-0 z-50 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 lg:hidden"> <aside className="fixed inset-y-0 z-50 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 lg:hidden">
<SidebarContent /> <SidebarContent />
</aside> </aside>
) : null; </Transition>
);
}; };
export default MobileSidebar; export default MobileSidebar;

View File

@@ -1,42 +1,44 @@
import React from "react"; import React from "react";
import { NavLink, Route } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { Channel } from "../../models/channel"; import { Channel } from "../../models/channel";
import { ApiService } from "../../services";
const SidebarContent = () => { const SidebarContent = () => {
const [channels, setChannels] = React.useState<Channel[]>([]); const [channels, setChannels] = React.useState<Channel[]>([]);
const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]); // const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]);
React.useEffect(() => { React.useEffect(() => {
const fetchChannels = async () => { const fetchChannels = async () => {
const res = await fetch(`${process.env.REACT_APP_API_URL}/channels`); const res = await ApiService.getChannels();
const data = await res.json(); if (res) {
setChannels(data); setChannels(res);
setFilteredChannels(data);
};
fetchChannels().catch(console.error);
}, []);
const _searchChannels = ($event: React.ChangeEvent<HTMLInputElement>) => {
const searchString = $event.target.value;
if (searchString) {
const filteredChannels = channels.filter((c) => {
const result = c.category_name
.toLowerCase()
.includes(searchString.toLowerCase());
console.log(
"sidebar.component",
`Category Name: ${c.category_name}`,
`Search String: ${searchString}`
);
console.log("sidebar.component", "Result", result);
return result;
});
setFilteredChannels(filteredChannels);
} else {
setFilteredChannels(channels);
} }
}; };
fetchChannels();
}, []);
// const _searchChannels = ($event: React.ChangeEvent<HTMLInputElement>) => {
// const searchString = $event.target.value;
// if (searchString) {
// const filteredChannels = channels.filter((c) => {
// const result = c.category_name
// .toLowerCase()
// .includes(searchString.toLowerCase());
// console.log(
// "sidebar.component",
// `Category Name: ${c.category_name}`,
// `Search String: ${searchString}`
// );
// console.log("sidebar.component", "Result", result);
// return result;
// });
// setFilteredChannels(filteredChannels);
// } else {
// setFilteredChannels(channels);
// }
// };
return ( return (
<div className="py-4 text-gray-500 dark:text-gray-400"> channels && (
<div className="py-4 text-gray-500 dark:text-gray-400 scroller">
<a <a
className="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" className="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200"
href="/" href="/"
@@ -44,7 +46,7 @@ const SidebarContent = () => {
Xtreamium Xtreamium
</a> </a>
<ul className="mt-6"> <ul className="mt-6">
{filteredChannels.map((channel: Channel) => ( {channels.map((channel: Channel) => (
<li className="relative px-6 py-3" key={channel.category_id}> <li className="relative px-6 py-3" key={channel.category_id}>
<NavLink <NavLink
to={`/live/channel/${channel.category_id}`} to={`/live/channel/${channel.category_id}`}
@@ -72,6 +74,7 @@ const SidebarContent = () => {
))} ))}
</ul> </ul>
</div> </div>
)
); );
}; };

View File

@@ -65,12 +65,12 @@ const Button = React.forwardRef<Ref, ButtonProps>(function Button(props, ref) {
return !!icon || !!iconLeft || !!iconRight; return !!icon || !!iconLeft || !!iconRight;
} }
if (hasIcon() && !other["aria-label"] && !children) {
console.warn( console.warn(
hasIcon() && !other["aria-label"] && !children,
"Button", "Button",
'You are using an icon button, but no "aria-label" attribute was found. Add an "aria-label" attribute to work as a label for screen readers.' 'You are using an icon button, but no "aria-label" attribute was found. Add an "aria-label" attribute to work as a label for screen readers.'
); );
}
const IconLeft = iconLeft || icon; const IconLeft = iconLeft || icon;
const IconRight = iconRight; const IconRight = iconRight;

View File

@@ -0,0 +1,41 @@
import React from "react";
import classNames from "classnames";
import { defaultTheme } from "../../constants";
export interface HelperTextProps extends React.HTMLAttributes<HTMLSpanElement> {
/**
* Defines the color of the helper text (the same as with Input, Select, etc.)
*/
valid?: boolean;
}
const HelperText = React.forwardRef<HTMLSpanElement, HelperTextProps>(
function HelperText(props, ref) {
const { children, valid, className, ...other } = props;
const { helperText } = defaultTheme;
const baseStyle = helperText.base;
const validStyle = helperText.valid;
const invalidStyle = helperText.invalid;
const validationStyle = (valid: boolean | undefined): string => {
switch (valid) {
case true:
return validStyle;
case false:
return invalidStyle;
default:
return "";
}
};
const cls = classNames(baseStyle, validationStyle(valid), className);
return (
<span className={cls} ref={ref} {...other}>
{children}
</span>
);
}
);
export default HelperText;

View File

@@ -0,0 +1,18 @@
import React, { SyntheticEvent } from "react";
interface ImageWithFallbackProps
extends React.ImgHTMLAttributes<HTMLImageElement> {
src: string;
fallback: string;
}
const ImageWithFallback = ({
fallback,
src,
...props
}: ImageWithFallbackProps) => {
return (
<img {...props} src={src} onError={(e: any) => (e.target.src = fallback)} />
);
};
export default ImageWithFallback;

View File

@@ -2,6 +2,19 @@ import Avatar from "./avatar.component";
import Badge from "./badge.component"; import Badge from "./badge.component";
import Button from "./button.component"; import Button from "./button.component";
import { Dropdown, DropdownItem } from "./dropdown.component"; import { Dropdown, DropdownItem } from "./dropdown.component";
import HelperText from "./helper-text.component";
import ImageWithFallback from "./image-fallback.component";
import Input from "./input.component"; import Input from "./input.component";
import Label from "./label.component";
export { Avatar, Badge, Button, Input, Dropdown, DropdownItem }; export {
Avatar,
Badge,
Button,
Input,
Dropdown,
DropdownItem,
Label,
HelperText,
ImageWithFallback,
};

View File

@@ -6,13 +6,23 @@ export interface InputProps extends React.ComponentPropsWithRef<"input"> {
valid?: boolean; valid?: boolean;
disabled?: boolean; disabled?: boolean;
type?: string; type?: string;
formControlName?: string;
register?: any;
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input( const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(
props, props,
ref ref
) { ) {
const { valid, disabled, className, type = "text", ...other } = props; const {
valid,
disabled,
className,
type = "text",
formControlName,
register,
...other
} = props;
const { input } = defaultTheme; const { input } = defaultTheme;
const baseStyle = input.base; const baseStyle = input.base;

View File

@@ -0,0 +1,36 @@
import React from "react";
import classNames from "classnames";
import { defaultTheme } from "../../constants";
export interface LabelProps extends React.HTMLAttributes<HTMLLabelElement> {
check?: boolean;
radio?: boolean;
disabled?: boolean;
}
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(function Label(
props,
ref
) {
const { children, check, radio, disabled, className, ...other } = props;
const { label } = defaultTheme;
const baseStyle = label.base;
const checkStyle = label.check;
const disabledStyle = label.disabled;
const cls = classNames(
baseStyle,
// check and radio are interchangeable
(check || radio) && checkStyle,
disabled && disabledStyle,
className
);
return (
<label className={cls} ref={ref} {...other}>
{children}
</label>
);
});
export default Label;

View File

@@ -130,7 +130,7 @@ const defaultTheme = {
}, },
// Input // Input
input: { input: {
base: "block w-full text-sm focus:outline-none dark:text-gray-300 leading-5 rounded-md", base: "block w-full text-sm focus:outline-none dark:text-gray-300 leading-5 rounded-md h-8 p-2",
active: active:
"focus:border-purple-400 border-gray-300 dark:border-gray-600 focus:ring focus:ring-purple-300 dark:focus:border-gray-600 dark:focus:ring-gray-300 dark:bg-gray-700", "focus:border-purple-400 border-gray-300 dark:border-gray-600 focus:ring focus:ring-purple-300 dark:focus:border-gray-600 dark:focus:ring-gray-300 dark:bg-gray-700",
disabled: "cursor-not-allowed opacity-50 bg-gray-300 dark:bg-gray-800", disabled: "cursor-not-allowed opacity-50 bg-gray-300 dark:bg-gray-800",

View File

@@ -2,7 +2,7 @@ import React, { Suspense } from "react";
import { useLocation, Routes, Route } from "react-router-dom"; import { useLocation, Routes, Route } from "react-router-dom";
import Header from "../components/header.component"; import Header from "../components/header.component";
import Main from "./main.container"; import Main from "./main.container";
import { ChannelPage, PlayerPage } from "../pages"; import { ChannelPage, HomePage, PlayerPage } from "../pages";
import ThemedSuspence from "../components/themed-suspence.component"; import ThemedSuspence from "../components/themed-suspence.component";
import { SidebarContext } from "../context"; import { SidebarContext } from "../context";
import Sidebar from "../components/sidebar"; import Sidebar from "../components/sidebar";
@@ -28,6 +28,7 @@ const Layout = () => {
<Routes> <Routes>
<Route path="live/channel/:channelId" element={<ChannelPage />} /> <Route path="live/channel/:channelId" element={<ChannelPage />} />
<Route path="live/play/:streamId" element={<PlayerPage />} /> <Route path="live/play/:streamId" element={<PlayerPage />} />
<Route path="*" element={<HomePage />} />
</Routes> </Routes>
</Suspense> </Suspense>
</Main> </Main>

View File

@@ -5,7 +5,7 @@ interface IMainProps {
} }
const Main = ({ children }: IMainProps) => { const Main = ({ children }: IMainProps) => {
return ( return (
<main className="h-full overflow-y-auto"> <main className="h-screen pb-16 overflow-y-hidden">
<div className="container grid px-6 mx-auto">{children}</div> <div className="container grid px-6 mx-auto">{children}</div>
</main> </main>
); );

View File

@@ -3,3 +3,50 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
html {
scrollbar-width: thin;
}
.scroller:hover::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.scroller:hover::-webkit-scrollbar-thumb,
#aside.scroller:hover::-webkit-scrollbar-thumb {
@apply rounded;
}
html {
@apply overflow-auto;
scrollbar-color: #9ca3af #e5e7eb;
}
.scroller:hover::-webkit-scrollbar-track {
@apply bg-gray-200;
}
.scroller:hover::-webkit-scrollbar-thumb {
@apply bg-gray-400;
}
.scroller:hover::-webkit-scrollbar-thumb:hover {
@apply bg-gray-500;
}
html.dark {
scrollbar-color: #374151 #111827;
}
html.dark.scroller:hover::-webkit-scrollbar-track {
@apply bg-gray-900;
}
html.dark.scroller:hover::-webkit-scrollbar-thumb {
@apply bg-gray-700;
}
html.dark.scroller:hover::-webkit-scrollbar-thumb:hover {
@apply bg-gray-600;
}

View File

@@ -4,63 +4,37 @@ import { AiOutlinePlayCircle } from "react-icons/ai";
import { Stream } from "../models/stream"; import { Stream } from "../models/stream";
import { convertEpochToSpecificTimezone } from "../utils/date-utils"; import { convertEpochToSpecificTimezone } from "../utils/date-utils";
import { EPGComponent } from "../components"; import { EPGComponent } from "../components";
import {
CastButton,
CastProvider,
useCast,
useMedia,
} from "../utils/chromecast";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { Table, TableBody, TableCell, TableContainer, TableHeader, TableRow } from "../components/widgets/table"; import {
import { Avatar, Badge, Button } from "../components/widgets"; Table,
TableBody,
TableCell,
TableContainer,
TableHeader,
TableRow,
} from "../components/widgets/table";
import { Badge, Button, ImageWithFallback } from "../components/widgets";
import { ApiService } from "../services";
const ChannelPage = () => { const ChannelPage = () => {
let params = useParams(); let params = useParams();
const cast = useCast();
const media = useMedia();
const [streams, setStreams] = React.useState<Stream[]>([]); const [streams, setStreams] = React.useState<Stream[]>([]);
const [currentVideoUrl, setCurrentVideoUrl] = React.useState("");
React.useEffect(() => { React.useEffect(() => {
const fetchChannels = async () => { const fetchChannels = async () => {
const res = await fetch( if (params.channelId) {
`${process.env.REACT_APP_API_URL}/streams/${params.channelId}` const data = await ApiService.getStreams(params.channelId);
);
const data = await res.json();
setStreams(data); setStreams(data);
}
}; };
fetchChannels().catch(console.error); fetchChannels().catch(console.error);
}, [params.channelId]); }, [params.channelId]);
const _getStreamUrl = async (streamId: number) => {
const res = await fetch(
`${process.env.REACT_APP_API_URL}/live/stream/url/${streamId}`
);
if (res.status !== 200) {
alert("Failed to get stream url");
return;
}
const data = await res.json();
return data?.url;
};
const _cast = React.useCallback(
async (streamId: number) => {
const streamUrl = await _getStreamUrl(streamId);
if (streamUrl) {
await media.playMedia(streamUrl);
}
},
[media]
);
const handleXHR = (...args: any[]) => {
console.log("channel.page", "handleXHR", args);
};
const playStream = async (streamId: number) => { const playStream = async (streamId: number) => {
const url = await _getStreamUrl(streamId); const url = await ApiService.getStreamUrl(streamId);
if (url) { if (url) {
const mpv_args = const mpv_args =
"--keep-open=yes\n--geometry=1024x768-0-0\n--ontop\n--screen=2\n--ytdl-format=bestvideo[ext=mp4][height<=?720]+bestaudio[ext=m4a]\n--border=no".split( "--keep-open=yes\n--geometry=1024x768-0-0\n--ontop\n--screen=2\n--ytdl-format=bestvideo[ext=mp4][height<=?720]+bestaudio[ext=m4a]\n--border=no".split(
@@ -128,7 +102,6 @@ const ChannelPage = () => {
} }
}; };
return ( return (
<CastProvider>
<TableContainer className="mt-5 mb-8"> <TableContainer className="mt-5 mb-8">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -143,10 +116,11 @@ const ChannelPage = () => {
<TableRow key={stream.num}> <TableRow key={stream.num}>
<TableCell> <TableCell>
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
<Avatar <ImageWithFallback
className="hidden w-10 h-10 ml-2 mr-3 md:block" className="hidden w-10 h-10 ml-2 mr-3 md:block"
src={stream.stream_icon} src={stream.stream_icon}
alt="Stream icon" alt="Stream icon"
fallback="/images/unknown-stream.svg"
/> />
<div> <div>
<p className="font-semibold">{stream.name}</p> <p className="font-semibold">{stream.name}</p>
@@ -167,7 +141,6 @@ const ChannelPage = () => {
aria-label="Edit" aria-label="Edit"
onClick={() => playStream(stream.stream_id)} onClick={() => playStream(stream.stream_id)}
></Button> ></Button>
<CastButton streamId={stream.stream_id} onPlay={_cast} />
</div> </div>
</TableCell> </TableCell>
</TableRow>, </TableRow>,
@@ -184,7 +157,6 @@ const ChannelPage = () => {
</TableBody> </TableBody>
</Table> </Table>
</TableContainer> </TableContainer>
</CastProvider>
); );
}; };

View File

@@ -1,5 +1,6 @@
import ChannelPage from "./channel.page"; import ChannelPage from "./channel.page";
import HomePage from "./home.page"; import HomePage from "./home.page";
import OnboardingPage from "./onboarding.page";
import PlayerPage from "./player.page"; import PlayerPage from "./player.page";
export { HomePage, ChannelPage, PlayerPage }; export { HomePage, ChannelPage, PlayerPage, OnboardingPage };

View File

@@ -0,0 +1,31 @@
import ImageLight from "../assets/images/love-tv.jpg";
import ServerDetails from "../components/server-details.component";
const OnboardingPage = () => {
return (
<div className="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900">
<div className="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
<div className="flex flex-col overflow-y-auto md:flex-row">
<div className="h-32 md:h-auto md:w-1/2">
<img
aria-hidden="true"
className="object-cover w-full h-full dark:hidden"
src={ImageLight}
alt="Office"
/>
<img
aria-hidden="true"
className="hidden object-cover w-full h-full dark:block"
src={ImageLight}
alt="Office"
/>
</div>
<main className="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
<ServerDetails />
</main>
</div>
</div>
</div>
);
};
export default OnboardingPage;

View File

@@ -0,0 +1,51 @@
import http from "./http.service";
import { Channel } from "../models/channel";
import { Stream } from "../models/stream";
import axios from "axios";
class ApiService {
public validateCredentials = async (
server: string,
username: string,
password: string
): Promise<Boolean> => {
const client = axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-type": "application/json",
"x-xtream-server": server,
"x-xtream-username": username,
"x-xtream-password": password,
},
});
try {
const res = await client.get("/validate");
return res.status === 200;
} catch {
return false;
}
};
public getChannels = async (): Promise<Channel[]> => {
const response = await http.get("/channels");
return response.data;
};
public getStreams = async (channelId: string): Promise<Stream[]> => {
const response = await http.get(`/streams/${channelId}`);
return response.data;
};
public getStreamUrl = async (
streamId: number
): Promise<string | undefined> => {
const res = await http.get(`/live/stream/url/${streamId}`);
if (res.status !== 200) {
alert("Failed to get stream url");
return;
}
return res?.data.url;
};
}
export default new ApiService();

View File

@@ -0,0 +1,16 @@
import axios from "axios";
export default axios.create({
baseURL: process.env.REACT_APP_API_URL,
headers: {
"Content-type": "application/json",
"x-xtream-server": JSON.parse(localStorage.getItem("server") || "{}")[
"server"
],
"x-xtream-username": JSON.parse(localStorage.getItem("server") || "{}")[
"username"
],
"x-xtream-password": JSON.parse(localStorage.getItem("server") || "{}")[
"password"
],
},
});

View File

@@ -0,0 +1,3 @@
import ApiService from "./api.service";
export { ApiService };

View File

@@ -2495,6 +2495,13 @@ axe-core@^4.3.5:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
axios@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
dependencies:
follow-redirects "^1.14.8"
axobject-query@^2.2.0: axobject-query@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -4300,7 +4307,7 @@ focus-lock@^0.10.2:
dependencies: dependencies:
tslib "^2.0.3" tslib "^2.0.3"
follow-redirects@^1.0.0: follow-redirects@^1.0.0, follow-redirects@^1.14.8:
version "1.14.9" version "1.14.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
@@ -7142,6 +7149,11 @@ react-helmet@^6.1.0:
react-fast-compare "^3.1.1" react-fast-compare "^3.1.1"
react-side-effect "^2.1.0" react-side-effect "^2.1.0"
react-hook-form@^7.29.0:
version "7.29.0"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.29.0.tgz#5e7e41a483b70731720966ed8be52163ea1fecf1"
integrity sha512-NcJqWRF6el5HMW30fqZRt27s+lorvlCCDbTpAyHoodQeYWXgQCvZJJQLC1kRMKdrJknVH0NIg3At6TUzlZJFOQ==
react-icons@^4.3.1: react-icons@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca"