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

This commit is contained in:
dekzter 2025-03-05 11:08:04 -05:00
parent ca676206a4
commit 412b799d7b
10 changed files with 174 additions and 66 deletions

View file

@ -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)),
]

View file

@ -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",
})

View file

@ -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 = () => {
<ThemeProvider theme={theme}>
<CssBaseline />
<Router>
<Drawer
variant="permanent"
<Sidebar
open={open}
sx={{
width: open ? drawerWidth : miniDrawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: open ? drawerWidth : miniDrawerWidth,
transition: 'width 0.3s',
overflowX: 'hidden',
},
}}
>
<List sx={{ backgroundColor: '#495057', color: 'white' }}>
<ListItem disablePadding>
<ListItemButton
onClick={toggleDrawer}
size="small"
sx={{
pt: 0,
pb: 0,
}}
>
<img src={logo} width="33x" alt="logo" />
{open && (
<ListItemText primary="Dispatcharr" sx={{ paddingLeft: 3 }} />
)}
</ListItemButton>
</ListItem>
</List>
<Divider />
<Sidebar open />
</Drawer>
miniDrawerWidth={miniDrawerWidth}
drawerWidth={drawerWidth}
toggleDrawer={toggleDrawer}
/>
<Box
sx={{
display: 'flex',

View file

@ -16,7 +16,7 @@ export default class API {
* A static method so we can do: await API.getAuthToken()
*/
static async getAuthToken() {
return await useAuthStore.getState().getToken();
return await useAuthStore.getState().getToken();
}
static async login(username, password) {
@ -500,7 +500,7 @@ export default class API {
return retval;
}
// Notice there's a duplicated "refreshPlaylist" method above;
// Notice there's a duplicated "refreshPlaylist" method above;
// you might want to rename or remove one if it's not needed.
static async addEPG(values) {
@ -706,6 +706,18 @@ export default class API {
return retval;
}
static async getEnvironmentSettings() {
const response = await fetch(`${host}/api/core/settings/env/`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
const retval = await response.json();
return retval;
}
static async updateSetting(values) {
const { id, ...payload } = values;
const response = await fetch(`${host}/api/core/settings/${id}/`, {

View file

@ -6,6 +6,10 @@ import {
ListItemButton,
ListItemText,
ListItemIcon,
Box,
Divider,
Drawer,
TextField,
} from '@mui/material';
import {
Tv as TvIcon,
@ -14,7 +18,11 @@ import {
LiveTv as LiveTvIcon,
PlaylistPlay as PlaylistPlayIcon,
Settings as SettingsIcon,
Logout as LogoutIcon,
} from '@mui/icons-material';
import logo from '../images/logo.png';
import useAuthStore from '../store/auth';
import useSettingsStore from '../store/settings';
const items = [
{ text: 'Channels', icon: <TvIcon />, route: '/channels' },
@ -29,24 +37,90 @@ const items = [
{ text: 'Settings', icon: <SettingsIcon />, route: '/settings' },
];
const Sidebar = ({ open }) => {
const Sidebar = ({ open, miniDrawerWidth, drawerWidth, toggleDrawer }) => {
const location = useLocation();
const { isAuthenticated } = useAuthStore();
const {
environment: { public_ip },
} = useSettingsStore();
return (
<List>
{items.map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton
component={Link}
to={item.route}
selected={location.pathname == item.route}
>
<ListItemIcon>{item.icon}</ListItemIcon>
{open && <ListItemText primary={item.text} />}
</ListItemButton>
</ListItem>
))}
</List>
<Drawer
variant="permanent"
open={open}
sx={{
width: open ? drawerWidth : miniDrawerWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: open ? drawerWidth : miniDrawerWidth,
transition: 'width 0.3s',
overflowX: 'hidden',
'& .MuiDrawer-paper': {
display: 'flex',
flexDirection: 'column',
height: '100vh',
},
},
}}
>
<Box sx={{ flexGrow: 1 }}>
<List sx={{ backgroundColor: '#495057', color: 'white' }}>
<ListItem disablePadding>
<ListItemButton
onClick={toggleDrawer}
size="small"
sx={{
pt: 0,
pb: 0,
}}
>
<img src={logo} width="33x" alt="logo" />
{open && (
<ListItemText primary="Dispatcharr" sx={{ paddingLeft: 3 }} />
)}
</ListItemButton>
</ListItem>
</List>
<Divider />
<List>
{items.map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton
component={Link}
to={item.route}
selected={location.pathname == item.route}
>
<ListItemIcon>{item.icon}</ListItemIcon>
{open && <ListItemText primary={item.text} />}
</ListItemButton>
</ListItem>
))}
</List>
</Box>
{isAuthenticated && (
<Box sx={{ p: 2, borderTop: '1px solid #ccc' }}>
<List>
<ListItem disablePadding>
<ListItemButton>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItemButton>
</ListItem>
</List>
<TextField
size="small"
fullWidth
label="Public IP"
value={public_ip || ''}
disabled
/>
</Box>
)}
</Drawer>
);
};

View file

@ -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');

View file

@ -75,7 +75,9 @@ const EPGsTable = () => {
};
const deleteEPG = async (id) => {
setIsLoading(true);
await API.deleteEPG(id);
setIsLoading(false);
};
const refreshEPG = async (id) => {

View file

@ -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 = () => {

View file

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

View file

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