mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
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:
parent
ca676206a4
commit
412b799d7b
10 changed files with 174 additions and 66 deletions
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}/`, {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -75,7 +75,9 @@ const EPGsTable = () => {
|
|||
};
|
||||
|
||||
const deleteEPG = async (id) => {
|
||||
setIsLoading(true);
|
||||
await API.deleteEPG(id);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const refreshEPG = async (id) => {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue