From 412b799d7b47a2f3fc308b23a9b1bb9d34bbde02 Mon Sep 17 00:00:00 2001 From: dekzter Date: Wed, 5 Mar 2025 11:08:04 -0500 Subject: [PATCH] new environment settings endpoint, added added in support for conditionally building video URL since we don't want dev env to proxy the stream through the react server --- core/api_urls.py | 3 +- core/api_views.py | 25 +++++ frontend/src/App.js | 50 ++------- frontend/src/api.js | 16 ++- frontend/src/components/Sidebar.js | 104 +++++++++++++++--- .../src/components/tables/ChannelsTable.js | 16 ++- frontend/src/components/tables/EPGsTable.js | 2 + .../src/components/tables/StreamsTable.js | 7 ++ frontend/src/pages/Guide.js | 13 ++- frontend/src/store/settings.js | 4 + 10 files changed, 174 insertions(+), 66 deletions(-) diff --git a/core/api_urls.py b/core/api_urls.py index 6e240927..724a3311 100644 --- a/core/api_urls.py +++ b/core/api_urls.py @@ -2,7 +2,7 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .api_views import UserAgentViewSet, StreamProfileViewSet, CoreSettingsViewSet +from .api_views import UserAgentViewSet, StreamProfileViewSet, CoreSettingsViewSet, environment router = DefaultRouter() router.register(r'useragents', UserAgentViewSet, basename='useragent') @@ -10,5 +10,6 @@ router.register(r'streamprofiles', StreamProfileViewSet, basename='streamprofile router.register(r'settings', CoreSettingsViewSet, basename='coresettings') urlpatterns = [ + path('settings/env/', environment, name='token_refresh'), path('', include(router.urls)), ] diff --git a/core/api_views.py b/core/api_views.py index 917afe12..842d650f 100644 --- a/core/api_views.py +++ b/core/api_views.py @@ -6,6 +6,10 @@ from django.shortcuts import get_object_or_404 from .models import UserAgent, StreamProfile, CoreSettings from .serializers import UserAgentSerializer, StreamProfileSerializer, CoreSettingsSerializer from rest_framework.permissions import IsAuthenticated +from rest_framework.decorators import api_view, permission_classes +from drf_yasg.utils import swagger_auto_schema +import requests +import os class UserAgentViewSet(viewsets.ModelViewSet): """ @@ -28,3 +32,24 @@ class CoreSettingsViewSet(viewsets.ModelViewSet): """ queryset = CoreSettings.objects.all() serializer_class = CoreSettingsSerializer + +@swagger_auto_schema( + method='get', + operation_description="Endpoint for environment details", + responses={200: "Environment variables"} +) +@api_view(['GET']) +@permission_classes([IsAuthenticated]) +def environment(request): + public_ip = None + try: + response = requests.get("https://api64.ipify.org?format=json") + public_ip = response.json().get("ip") + except requests.RequestException as e: + return f"Error: {e}" + + return Response({ + 'authenticated': True, + 'public_ip': public_ip, + 'env_mode': "dev" if os.getenv('DISPATCHARR_ENV', None) == "dev" else "prod", + }) diff --git a/frontend/src/App.js b/frontend/src/App.js index 9dc0fedd..a16c72d1 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -12,23 +12,13 @@ import Login from './pages/Login'; import Channels from './pages/Channels'; import M3U from './pages/M3U'; import { ThemeProvider } from '@mui/material/styles'; -import { - Box, - CssBaseline, - Drawer, - List, - ListItem, - ListItemButton, - ListItemText, - Divider, -} from '@mui/material'; +import { Box, CssBaseline } from '@mui/material'; import theme from './theme'; import EPG from './pages/EPG'; import Guide from './pages/Guide'; import Settings from './pages/Settings'; import StreamProfiles from './pages/StreamProfiles'; import useAuthStore from './store/auth'; -import logo from './images/logo.png'; import Alert from './components/Alert'; import FloatingVideo from './components/FloatingVideo'; import SuperuserForm from './components/forms/SuperuserForm'; @@ -90,39 +80,13 @@ const App = () => { - - - - - logo - {open && ( - - )} - - - - - - + miniDrawerWidth={miniDrawerWidth} + drawerWidth={drawerWidth} + toggleDrawer={toggleDrawer} + /> + , route: '/channels' }, @@ -29,24 +37,90 @@ const items = [ { text: 'Settings', icon: , route: '/settings' }, ]; -const Sidebar = ({ open }) => { +const Sidebar = ({ open, miniDrawerWidth, drawerWidth, toggleDrawer }) => { const location = useLocation(); + const { isAuthenticated } = useAuthStore(); + const { + environment: { public_ip }, + } = useSettingsStore(); return ( - - {items.map((item) => ( - - - {item.icon} - {open && } - - - ))} - + + + + + + logo + {open && ( + + )} + + + + + + + {items.map((item) => ( + + + {item.icon} + {open && } + + + ))} + + + + {isAuthenticated && ( + + + + + + + + + + + + + + )} + ); }; diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js index 2572085e..3fcfca7b 100644 --- a/frontend/src/components/tables/ChannelsTable.js +++ b/frontend/src/components/tables/ChannelsTable.js @@ -32,6 +32,7 @@ import { TableHelper } from '../../helpers'; import utils from '../../utils'; import logo from '../../images/logo.png'; import useVideoStore from '../../store/useVideoStore'; +import useSettingsStore from '../../store/settings'; const ChannelsTable = () => { const [channel, setChannel] = useState(null); @@ -42,9 +43,12 @@ const ChannelsTable = () => { const [textToCopy, setTextToCopy] = useState(''); const [snackbarMessage, setSnackbarMessage] = useState(''); const [snackbarOpen, setSnackbarOpen] = useState(false); + const { showVideo } = useVideoStore.getState(); // or useVideoStore() const { channels, isLoading: channelsLoading } = useChannelsStore(); - const { showVideo } = useVideoStore.getState(); // or useVideoStore() + const { + environment: { env_mode }, + } = useSettingsStore(); // Configure columns const columns = useMemo( @@ -100,11 +104,17 @@ const ChannelsTable = () => { }; const deleteChannel = async (id) => { + setIsLoading(true); await API.deleteChannel(id); + setIsLoading(false); }; function handleWatchStream(channelNumber) { - showVideo(`/output/stream/${channelNumber}/`); + let vidUrl = `/output/stream/${channelNumber}/`; + if (env_mode == 'dev') { + vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; + } + showVideo(vidUrl); } // (Optional) bulk delete, but your endpoint is @TODO @@ -131,7 +141,9 @@ const ChannelsTable = () => { const rowOrder = table.getRowModel().rows.map((row) => row.original.id); // Call our custom API endpoint + setIsLoading(true); const result = await API.assignChannelNumbers(rowOrder); + setIsLoading(false); // We might get { message: "Channels have been auto-assigned!" } setSnackbarMessage(result.message || 'Channels assigned'); diff --git a/frontend/src/components/tables/EPGsTable.js b/frontend/src/components/tables/EPGsTable.js index a23e89f9..aadc516f 100644 --- a/frontend/src/components/tables/EPGsTable.js +++ b/frontend/src/components/tables/EPGsTable.js @@ -75,7 +75,9 @@ const EPGsTable = () => { }; const deleteEPG = async (id) => { + setIsLoading(true); await API.deleteEPG(id); + setIsLoading(false); }; const refreshEPG = async (id) => { diff --git a/frontend/src/components/tables/StreamsTable.js b/frontend/src/components/tables/StreamsTable.js index a3591fcf..ca8e0c60 100644 --- a/frontend/src/components/tables/StreamsTable.js +++ b/frontend/src/components/tables/StreamsTable.js @@ -53,11 +53,13 @@ const StreamsTable = () => { // Fallback: Individual creation (optional) const createChannelFromStream = async (stream) => { + setIsLoading(true); await API.createChannelFromStream({ channel_name: stream.name, channel_number: null, stream_id: stream.id, }); + setIsLoading(false); }; // Bulk creation: create channels from selected streams in one API call @@ -67,6 +69,7 @@ const StreamsTable = () => { .getRowModel() .rows.filter((row) => row.getIsSelected()); + setIsLoading(true); await API.createChannelsFromStreams( selected.map((sel) => ({ stream_id: sel.original.id, @@ -82,14 +85,18 @@ const StreamsTable = () => { }; const deleteStream = async (id) => { + setIsLoading(true); await API.deleteStream(id); + setIsLoading(false); }; const deleteStreams = async () => { + setIsLoading(true); const selected = table .getRowModel() .rows.filter((row) => row.getIsSelected()); await API.deleteStreams(selected.map((stream) => stream.original.id)); + setIsLoading(false); }; const closeStreamForm = () => { diff --git a/frontend/src/pages/Guide.js b/frontend/src/pages/Guide.js index e17b92fe..bfc49788 100644 --- a/frontend/src/pages/Guide.js +++ b/frontend/src/pages/Guide.js @@ -20,6 +20,7 @@ import useChannelsStore from '../store/channels'; import logo from '../images/logo.png'; import useVideoStore from '../store/useVideoStore'; // NEW import import useAlertStore from '../store/alerts'; +import useSettingsStore from '../store/settings'; /** Layout constants */ const CHANNEL_WIDTH = 120; // Width of the channel/logo column @@ -46,6 +47,9 @@ export default function TVChannelGuide({ startDate, endDate }) { const [selectedProgram, setSelectedProgram] = useState(null); const [loading, setLoading] = useState(true); const { showAlert } = useAlertStore(); + const { + environment: { env_mode }, + } = useSettingsStore(); const guideRef = useRef(null); @@ -172,9 +176,12 @@ export default function TVChannelGuide({ startDate, endDate }) { return; } // Build a playable stream URL for that channel - const url = - window.location.origin + '/output/stream/' + matched.channel_number; - showVideo(url); + let vidUrl = `/output/stream/${matched.channel_number}/`; + if (env_mode == 'dev') { + vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; + } + + showVideo(vidUrl); // Optionally close the modal setSelectedProgram(null); diff --git a/frontend/src/store/settings.js b/frontend/src/store/settings.js index 9c13be2f..b35dbb7c 100644 --- a/frontend/src/store/settings.js +++ b/frontend/src/store/settings.js @@ -3,6 +3,7 @@ import api from '../api'; const useSettingsStore = create((set) => ({ settings: {}, + environment: {}, isLoading: false, error: null, @@ -10,12 +11,15 @@ const useSettingsStore = create((set) => ({ set({ isLoading: true, error: null }); try { const settings = await api.getSettings(); + const env = await api.getEnvironmentSettings(); + console.log(env); set({ settings: settings.reduce((acc, setting) => { acc[setting.key] = setting; return acc; }, {}), isLoading: false, + environment: env, }); } catch (error) { console.error('Failed to fetch settings:', error);