mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
Guide Update
Updated guide style Added Floating Video Added mpegts.js
This commit is contained in:
parent
a4cd184a36
commit
31d26fddee
9 changed files with 351 additions and 192 deletions
|
|
@ -7,7 +7,7 @@ from drf_yasg.utils import swagger_auto_schema
|
|||
from drf_yasg import openapi
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from .models import EPGSource, ProgramData # Updated: use ProgramData instead of Program
|
||||
from .models import EPGSource, ProgramData # Using ProgramData
|
||||
from .serializers import ProgramDataSerializer, EPGSourceSerializer # Updated serializer
|
||||
from .tasks import refresh_epg_data
|
||||
|
||||
|
|
@ -31,8 +31,8 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
# ─────────────────────────────
|
||||
class ProgramViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for EPG programs"""
|
||||
queryset = ProgramData.objects.all() # Updated to ProgramData
|
||||
serializer_class = ProgramDataSerializer # Updated serializer
|
||||
queryset = ProgramData.objects.all()
|
||||
serializer_class = ProgramDataSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
|
@ -52,11 +52,10 @@ class EPGGridAPIView(APIView):
|
|||
def get(self, request, format=None):
|
||||
# Get current date and reset time to midnight (00:00)
|
||||
now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
twelve_hours_later = now + timedelta(hours=24)
|
||||
logger.debug(f"EPGGridAPIView: Querying programs between {now} and {twelve_hours_later}.")
|
||||
# Use select_related to prefetch EPGData and Channel data
|
||||
programs = ProgramData.objects.select_related('epg__channel').filter(
|
||||
# Use select_related to prefetch EPGData (no channel relation now)
|
||||
programs = ProgramData.objects.select_related('epg').filter(
|
||||
start_time__gte=now, start_time__lte=twelve_hours_later
|
||||
)
|
||||
count = programs.count()
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Generated by Django 5.1.6 on 2025-02-27 20:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epg', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='epgdata',
|
||||
name='channel',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='epgdata',
|
||||
name='tvg_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='programdata',
|
||||
name='tvg_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
0
core/management/commands/__init__.py
Normal file
0
core/management/commands/__init__.py
Normal file
215
frontend/package-lock.json
generated
215
frontend/package-lock.json
generated
|
|
@ -18,10 +18,12 @@
|
|||
"eslint": "^8.57.1",
|
||||
"formik": "^2.4.6",
|
||||
"material-react-table": "^3.2.0",
|
||||
"mpegts.js": "^1.4.2",
|
||||
"planby": "^1.1.7",
|
||||
"prettier": "^3.5.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-draggable": "4.4.6",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-window": "^1.8.11",
|
||||
|
|
@ -3776,6 +3778,66 @@
|
|||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
|
||||
"integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
|
||||
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.11.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
|
||||
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
|
@ -3866,7 +3928,8 @@
|
|||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.56.12",
|
||||
|
|
@ -5761,6 +5824,15 @@
|
|||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
|
@ -6740,6 +6812,7 @@
|
|||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
|
|
@ -7123,6 +7196,12 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
|
|
@ -11099,66 +11178,6 @@
|
|||
"react-dom": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/material-react-table/node_modules/@tanstack/react-table": {
|
||||
"version": "8.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.6.tgz",
|
||||
"integrity": "sha512-w0jluT718MrOKthRcr2xsjqzx+oEM7B7s/XXyfs19ll++hlId3fjTm+B2zrR3ijpANpkzBAr15j1XGVOMxpggQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/material-react-table/node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz",
|
||||
"integrity": "sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.11.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/material-react-table/node_modules/@tanstack/table-core": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/material-react-table/node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz",
|
||||
"integrity": "sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -11338,6 +11357,16 @@
|
|||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/mpegts.js": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mpegts.js/-/mpegts.js-1.8.0.tgz",
|
||||
"integrity": "sha512-ZtujqtmTjWgcDDkoOnLvrOKUTO/MKgLHM432zGDI8oPaJ0S+ebPxg1nEpDpLw6I7KmV/GZgUIrfbWi3qqEircg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.5",
|
||||
"webworkify-webpack": "github:xqq/webworkify-webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -13503,9 +13532,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -13631,14 +13664,30 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
|
||||
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^1.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-error-overlay": {
|
||||
|
|
@ -13668,6 +13717,7 @@
|
|||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz",
|
||||
"integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.6.0",
|
||||
"cookie": "^1.0.1",
|
||||
|
|
@ -13706,6 +13756,7 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -13786,6 +13837,7 @@
|
|||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
|
|
@ -14364,9 +14416,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.0",
|
||||
|
|
@ -14584,7 +14640,8 @@
|
|||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
|
|
@ -15870,7 +15927,8 @@
|
|||
"node_modules/turbo-stream": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="
|
||||
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
|
|
@ -16533,6 +16591,11 @@
|
|||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webworkify-webpack": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "git+ssh://git@github.com/xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz",
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@
|
|||
"eslint": "^8.57.1",
|
||||
"formik": "^2.4.6",
|
||||
"material-react-table": "^3.2.0",
|
||||
"mpegts.js": "^1.4.2",
|
||||
"planby": "^1.1.7",
|
||||
"prettier": "^3.5.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-draggable": "4.4.6",
|
||||
"react-router-dom": "^7.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-window": "^1.8.11",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// frontend/src/App.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
|
|
@ -9,7 +10,7 @@ import Sidebar from './components/Sidebar';
|
|||
import Login from './pages/Login';
|
||||
import Channels from './pages/Channels';
|
||||
import M3U from './pages/M3U';
|
||||
import { ThemeProvider } from '@mui/material/styles'; // Import theme tools
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import {
|
||||
Box,
|
||||
CssBaseline,
|
||||
|
|
@ -27,6 +28,9 @@ import StreamProfiles from './pages/StreamProfiles';
|
|||
import useAuthStore from './store/auth';
|
||||
import logo from './images/logo.png';
|
||||
|
||||
// NEW: import the floating PiP component
|
||||
import FloatingVideo from './components/FloatingVideo';
|
||||
|
||||
const drawerWidth = 240;
|
||||
const miniDrawerWidth = 60;
|
||||
|
||||
|
|
@ -49,7 +53,6 @@ const App = () => {
|
|||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const loggedIn = await initializeAuth();
|
||||
|
||||
if (loggedIn) {
|
||||
await initData();
|
||||
setIsAuthenticated(true);
|
||||
|
|
@ -57,7 +60,6 @@ const App = () => {
|
|||
await logout();
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, [initializeAuth, initData, setIsAuthenticated, logout]);
|
||||
|
||||
|
|
@ -65,18 +67,6 @@ const App = () => {
|
|||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
{/* <AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
zIndex: (theme) => theme.zIndex.drawer + 1,
|
||||
width: `calc(100% - ${open ? drawerWidth : miniDrawerWidth}px)`,
|
||||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
}}
|
||||
>
|
||||
<Toolbar variant="dense"></Toolbar>
|
||||
</AppBar> */}
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
open={open}
|
||||
|
|
@ -90,7 +80,6 @@ const App = () => {
|
|||
},
|
||||
}}
|
||||
>
|
||||
{/* Drawer Toggle Button */}
|
||||
<List sx={{ backgroundColor: '#495057', color: 'white' }}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
|
|
@ -101,7 +90,7 @@ const App = () => {
|
|||
pb: 0,
|
||||
}}
|
||||
>
|
||||
<img src={logo} width="33x" />
|
||||
<img src={logo} width="33x" alt="logo" />
|
||||
{open && (
|
||||
<ListItemText primary="Dispatcharr" sx={{ paddingLeft: 3 }} />
|
||||
)}
|
||||
|
|
@ -122,15 +111,8 @@ const App = () => {
|
|||
ml: `${open ? drawerWidth : miniDrawerWidth}px`,
|
||||
transition: 'width 0.3s, margin-left 0.3s',
|
||||
backgroundColor: '#495057',
|
||||
// pt: '64px',
|
||||
}}
|
||||
>
|
||||
{/* Fixed Header */}
|
||||
{/* <Box sx={{ height: '67px', backgroundColor: '#495057', color: '#fff', display: 'flex', alignItems: 'center', padding: '0 16px' }}>
|
||||
|
||||
</Box> */}
|
||||
|
||||
{/* Main Content Area between Header and Footer */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
|
|
@ -142,20 +124,18 @@ const App = () => {
|
|||
<Routes>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Route exact path="/channels" element={<Channels />} />
|
||||
<Route exact path="/m3u" element={<M3U />} />
|
||||
<Route exact path="/epg" element={<EPG />} />
|
||||
<Route path="/channels" element={<Channels />} />
|
||||
<Route path="/m3u" element={<M3U />} />
|
||||
<Route path="/epg" element={<EPG />} />
|
||||
<Route
|
||||
exact
|
||||
path="/stream-profiles"
|
||||
element={<StreamProfiles />}
|
||||
/>
|
||||
<Route exact path="/guide" element={<Guide />} />
|
||||
<Route path="/guide" element={<Guide />} />
|
||||
</>
|
||||
) : (
|
||||
<Route path="/login" element={<Login />} />
|
||||
)}
|
||||
{/* Redirect if no match */}
|
||||
<Route
|
||||
path="*"
|
||||
element={
|
||||
|
|
@ -169,6 +149,9 @@ const App = () => {
|
|||
</Box>
|
||||
</Box>
|
||||
</Router>
|
||||
|
||||
{/* Always-available floating video; remains visible across page changes */}
|
||||
<FloatingVideo />
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
92
frontend/src/components/FloatingVideo.js
Normal file
92
frontend/src/components/FloatingVideo.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
// frontend/src/components/FloatingVideo.js
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Draggable from 'react-draggable';
|
||||
import useVideoStore from '../store/useVideoStore';
|
||||
import mpegts from 'mpegts.js';
|
||||
|
||||
export default function FloatingVideo() {
|
||||
const { isVisible, streamUrl, hideVideo } = useVideoStore();
|
||||
const videoRef = useRef(null);
|
||||
const playerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible || !streamUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the browser supports MSE for live playback, initialize mpegts.js
|
||||
if (mpegts.getFeatureList().mseLivePlayback) {
|
||||
const player = mpegts.createPlayer({
|
||||
type: 'mpegts',
|
||||
url: streamUrl,
|
||||
isLive: true,
|
||||
// You can include other custom MPEGTS.js config fields here, e.g.:
|
||||
// cors: true,
|
||||
// withCredentials: false,
|
||||
});
|
||||
|
||||
player.attachMediaElement(videoRef.current);
|
||||
player.load();
|
||||
player.play();
|
||||
|
||||
// Store player instance so we can clean up later
|
||||
playerRef.current = player;
|
||||
}
|
||||
|
||||
// Cleanup when component unmounts or streamUrl changes
|
||||
return () => {
|
||||
if (playerRef.current) {
|
||||
playerRef.current.destroy();
|
||||
playerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isVisible, streamUrl]);
|
||||
|
||||
// If the floating video is hidden or no URL is selected, do not render
|
||||
if (!isVisible || !streamUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Draggable>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
width: '320px',
|
||||
zIndex: 9999,
|
||||
backgroundColor: '#333',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.7)',
|
||||
}}
|
||||
>
|
||||
{/* Simple header row with a close button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '4px' }}>
|
||||
<button
|
||||
onClick={hideVideo}
|
||||
style={{
|
||||
background: 'red',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
padding: '2px 8px',
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* The <video> element used by mpegts.js */}
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
style={{ width: '100%', height: '180px', backgroundColor: '#000' }}
|
||||
/>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
// frontend/src/pages/Guide.js
|
||||
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
|
|
@ -15,16 +16,16 @@ import dayjs from 'dayjs';
|
|||
import API from '../api';
|
||||
import useChannelsStore from '../store/channels';
|
||||
import logo from '../images/logo.png';
|
||||
import useVideoStore from '../store/useVideoStore'; // NEW import
|
||||
|
||||
/** Layout constants */
|
||||
const CHANNEL_WIDTH = 120; // Width of the channel/logo column
|
||||
const PROGRAM_HEIGHT = 90; // Height of each channel row
|
||||
const HOUR_WIDTH = 300; // The width for a 1-hour block
|
||||
const MINUTE_INCREMENT = 15; // For positioning programs every 15 min
|
||||
const MINUTE_BLOCK_WIDTH = HOUR_WIDTH / (60 / MINUTE_INCREMENT);
|
||||
// => 300 / 4 = 75px for each 15-minute block
|
||||
const MINUTE_BLOCK_WIDTH = HOUR_WIDTH / (60 / MINUTE_INCREMENT);
|
||||
|
||||
// Modal size constants (all modals will be the same size)
|
||||
// Modal size constants
|
||||
const MODAL_WIDTH = 600;
|
||||
const MODAL_HEIGHT = 400;
|
||||
|
||||
|
|
@ -33,17 +34,17 @@ const Transition = React.forwardRef(function Transition(props, ref) {
|
|||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
const TVChannelGuide = ({ startDate, endDate }) => {
|
||||
export default function TVChannelGuide({ startDate, endDate }) {
|
||||
const { channels } = useChannelsStore();
|
||||
|
||||
const [programs, setPrograms] = useState([]);
|
||||
const [guideChannels, setGuideChannels] = useState([]);
|
||||
const [now, setNow] = useState(dayjs());
|
||||
// State for selected program to display in modal
|
||||
const [selectedProgram, setSelectedProgram] = useState(null);
|
||||
|
||||
const guideRef = useRef(null);
|
||||
|
||||
// Load program data once
|
||||
useEffect(() => {
|
||||
if (!channels || channels.length === 0) {
|
||||
console.warn('No channels provided or empty channels array');
|
||||
|
|
@ -52,47 +53,48 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
|
||||
const fetchPrograms = async () => {
|
||||
console.log('Fetching program grid...');
|
||||
const fetchedPrograms = await API.getGrid();
|
||||
console.log(`Received ${fetchedPrograms.length} programs`);
|
||||
const fetched = await API.getGrid(); // GETs your EPG grid
|
||||
console.log(`Received ${fetched.length} programs`);
|
||||
|
||||
// Get unique tvg_ids from the returned programs
|
||||
const programIds = [...new Set(fetchedPrograms.map((prog) => prog.tvg_id))];
|
||||
// Unique tvg_ids from returned programs
|
||||
const programIds = [...new Set(fetched.map((p) => p.tvg_id))];
|
||||
|
||||
// Filter channels to only those that appear in the program list
|
||||
// Filter your Redux/Zustand channels by matching tvg_id
|
||||
const filteredChannels = channels.filter((ch) =>
|
||||
programIds.includes(ch.tvg_id)
|
||||
);
|
||||
console.log(`found ${filteredChannels.length} channels with matching tvg-ids`);
|
||||
console.log(
|
||||
`found ${filteredChannels.length} channels with matching tvg_ids`
|
||||
);
|
||||
|
||||
setGuideChannels(filteredChannels);
|
||||
setPrograms(fetchedPrograms);
|
||||
setPrograms(fetched);
|
||||
};
|
||||
|
||||
fetchPrograms();
|
||||
}, [channels]);
|
||||
|
||||
// Default to "today at midnight" -> +24h if not provided
|
||||
// Use start/end from props or default to "today at midnight" +24h
|
||||
const defaultStart = dayjs(startDate || dayjs().startOf('day'));
|
||||
const defaultEnd = endDate ? dayjs(endDate) : defaultStart.add(24, 'hour');
|
||||
|
||||
// Find earliest program start and latest program end to expand timeline if needed.
|
||||
// Expand timeline if needed based on actual earliest/ latest program
|
||||
const earliestProgramStart = useMemo(() => {
|
||||
if (!programs.length) return defaultStart;
|
||||
return programs.reduce((acc, p) => {
|
||||
const progStart = dayjs(p.start_time);
|
||||
return progStart.isBefore(acc) ? progStart : acc;
|
||||
const s = dayjs(p.start_time);
|
||||
return s.isBefore(acc) ? s : acc;
|
||||
}, defaultStart);
|
||||
}, [programs, defaultStart]);
|
||||
|
||||
const latestProgramEnd = useMemo(() => {
|
||||
if (!programs.length) return defaultEnd;
|
||||
return programs.reduce((acc, p) => {
|
||||
const progEnd = dayjs(p.end_time);
|
||||
return progEnd.isAfter(acc) ? progEnd : acc;
|
||||
const e = dayjs(p.end_time);
|
||||
return e.isAfter(acc) ? e : acc;
|
||||
}, defaultEnd);
|
||||
}, [programs, defaultEnd]);
|
||||
|
||||
// Timeline boundaries: use expanded timeline if needed
|
||||
const start = earliestProgramStart.isBefore(defaultStart)
|
||||
? earliestProgramStart
|
||||
: defaultStart;
|
||||
|
|
@ -100,9 +102,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
? latestProgramEnd
|
||||
: defaultEnd;
|
||||
|
||||
/**
|
||||
* For program positioning calculations: we step in 15-min increments.
|
||||
*/
|
||||
// Time increments in 15-min steps (for placing programs)
|
||||
const programTimeline = useMemo(() => {
|
||||
const times = [];
|
||||
let current = start;
|
||||
|
|
@ -113,9 +113,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
return times;
|
||||
}, [start, end]);
|
||||
|
||||
/**
|
||||
* For the visible timeline at the top: hourly blocks with 4 sub-lines.
|
||||
*/
|
||||
// Hourly marks
|
||||
const hourTimeline = useMemo(() => {
|
||||
const hours = [];
|
||||
let current = start;
|
||||
|
|
@ -126,7 +124,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
return hours;
|
||||
}, [start, end]);
|
||||
|
||||
// Scroll to "now" position on load
|
||||
// Scroll to "now" on load
|
||||
useEffect(() => {
|
||||
if (guideRef.current) {
|
||||
const nowOffset = dayjs().diff(start, 'minute');
|
||||
|
|
@ -136,7 +134,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
}
|
||||
}, [programs, start]);
|
||||
|
||||
// Update "now" every minute
|
||||
// Update “now” every 60s
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setNow(dayjs());
|
||||
|
|
@ -144,37 +142,57 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Calculate pixel offset for the "now" line
|
||||
// Pixel offset for the “now” vertical line
|
||||
const nowPosition = useMemo(() => {
|
||||
if (now.isBefore(start) || now.isAfter(end)) return -1;
|
||||
const minutesSinceStart = now.diff(start, 'minute');
|
||||
return (minutesSinceStart / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH;
|
||||
}, [now, start, end]);
|
||||
|
||||
/** Handle program click: scroll program into view and open modal */
|
||||
const handleProgramClick = (program, event) => {
|
||||
// Scroll clicked element into center view
|
||||
event.currentTarget.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
|
||||
setSelectedProgram(program);
|
||||
};
|
||||
// Helper: find channel by tvg_id
|
||||
function findChannelByTvgId(tvgId) {
|
||||
return guideChannels.find((ch) => ch.tvg_id === tvgId);
|
||||
}
|
||||
|
||||
/** Close modal */
|
||||
const handleCloseModal = () => {
|
||||
// The “Watch Now” click => show floating video
|
||||
const { showVideo } = useVideoStore.getState(); // or useVideoStore()
|
||||
function handleWatchStream(program) {
|
||||
const matched = findChannelByTvgId(program.tvg_id);
|
||||
if (!matched) {
|
||||
console.warn(`No channel found for tvg_id=${program.tvg_id}`);
|
||||
return;
|
||||
}
|
||||
// Build a playable stream URL for that channel
|
||||
const url = window.location.origin + '/output/stream/' + matched.id;
|
||||
showVideo(url);
|
||||
|
||||
// Optionally close the modal
|
||||
setSelectedProgram(null);
|
||||
};
|
||||
}
|
||||
|
||||
/** Render each program block as clickable, opening modal on click */
|
||||
const renderProgram = (program, channelStart) => {
|
||||
// On program click, open the details modal
|
||||
function handleProgramClick(program, event) {
|
||||
// Optionally scroll that element into view or do something else
|
||||
event.currentTarget.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
||||
setSelectedProgram(program);
|
||||
}
|
||||
|
||||
// Close the modal
|
||||
function handleCloseModal() {
|
||||
setSelectedProgram(null);
|
||||
}
|
||||
|
||||
// Renders each program block
|
||||
function renderProgram(program, channelStart) {
|
||||
const programKey = `${program.tvg_id}-${program.start_time}`;
|
||||
const programStart = dayjs(program.start_time);
|
||||
const programEnd = dayjs(program.end_time);
|
||||
|
||||
const startOffsetMinutes = programStart.diff(channelStart, 'minute');
|
||||
const durationMinutes = programEnd.diff(programStart, 'minute');
|
||||
|
||||
const leftPx = (startOffsetMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH;
|
||||
const widthPx = (durationMinutes / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH;
|
||||
|
||||
// Highlight if currently live
|
||||
const isLive = now.isAfter(programStart) && now.isBefore(programEnd);
|
||||
|
||||
return (
|
||||
|
|
@ -223,7 +241,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
|
@ -375,7 +393,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Now-position line */}
|
||||
{/* Now line */}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{nowPosition >= 0 && (
|
||||
<Box
|
||||
|
|
@ -391,7 +409,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* Channel rows with program blocks */}
|
||||
{/* Channel rows */}
|
||||
{guideChannels.map((channel) => {
|
||||
const channelPrograms = programs.filter(
|
||||
(p) => p.tvg_id === channel.tvg_id
|
||||
|
|
@ -407,9 +425,7 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
}}
|
||||
>
|
||||
<Box sx={{ flex: 1, position: 'relative' }}>
|
||||
{channelPrograms.map((program) =>
|
||||
renderProgram(program, start)
|
||||
)}
|
||||
{channelPrograms.map((prog) => renderProgram(prog, start))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
@ -447,13 +463,24 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
</DialogTitle>
|
||||
<DialogContent sx={{ color: '#a0aec0' }}>
|
||||
<Typography variant="caption" display="block">
|
||||
{dayjs(selectedProgram.start_time).format('h:mma')} - {dayjs(selectedProgram.end_time).format('h:mma')}
|
||||
{dayjs(selectedProgram.start_time).format('h:mma')} -{' '}
|
||||
{dayjs(selectedProgram.end_time).format('h:mma')}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ mt: 2, color: '#fff' }}>
|
||||
{selectedProgram.description || 'No description available.'}
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{/* Only show the Watch button if currently live */}
|
||||
{now.isAfter(dayjs(selectedProgram.start_time)) &&
|
||||
now.isBefore(dayjs(selectedProgram.end_time)) && (
|
||||
<Button
|
||||
onClick={() => handleWatchStream(selectedProgram)}
|
||||
sx={{ color: '#38b2ac' }}
|
||||
>
|
||||
Watch Now
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleCloseModal} sx={{ color: '#38b2ac' }}>
|
||||
Close
|
||||
</Button>
|
||||
|
|
@ -463,6 +490,4 @@ const TVChannelGuide = ({ startDate, endDate }) => {
|
|||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TVChannelGuide;
|
||||
}
|
||||
|
|
|
|||
22
frontend/src/store/useVideoStore.js
Normal file
22
frontend/src/store/useVideoStore.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// frontend/src/store/useVideoStore.js
|
||||
import { create } from 'zustand';
|
||||
|
||||
/**
|
||||
* Global store to track whether a floating video is visible and which URL is playing.
|
||||
*/
|
||||
const useVideoStore = create((set) => ({
|
||||
isVisible: false,
|
||||
streamUrl: null,
|
||||
|
||||
showVideo: (url) => set({
|
||||
isVisible: true,
|
||||
streamUrl: url,
|
||||
}),
|
||||
|
||||
hideVideo: () => set({
|
||||
isVisible: false,
|
||||
streamUrl: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
export default useVideoStore;
|
||||
Loading…
Add table
Add a link
Reference in a new issue