merged in from main

This commit is contained in:
kappa118 2025-03-01 09:25:45 -05:00
parent b1dcf159a2
commit c82391f57b
12 changed files with 100 additions and 62 deletions

View file

@ -5,7 +5,7 @@ from .api_views import (
ChannelViewSet,
ChannelGroupViewSet,
BulkDeleteStreamsAPIView,
BulkDeleteChannelsViewSet
BulkDeleteChannelsAPIView
)
app_name = 'channels' # for DRF routing
@ -14,11 +14,11 @@ router = DefaultRouter()
router.register(r'streams', StreamViewSet, basename='stream')
router.register(r'groups', ChannelGroupViewSet, basename='channel-group')
router.register(r'channels', ChannelViewSet, basename='channel')
router.register(r'bulk-delete-channels', BulkDeleteChannelsViewSet, basename='bulk-delete-channels')
urlpatterns = [
# Bulk delete for streams is a single APIView, not a ViewSet
# Bulk delete is a single APIView, not a ViewSet
path('streams/bulk-delete/', BulkDeleteStreamsAPIView.as_view(), name='bulk_delete_streams'),
path('channels/bulk-delete/', BulkDeleteChannelsAPIView.as_view(), name='bulk_delete_channels'),
]
urlpatterns += router.urls

View file

@ -265,7 +265,7 @@ class BulkDeleteStreamsAPIView(APIView):
# ─────────────────────────────────────────────────────────
# 5) Bulk Delete Channels
# ─────────────────────────────────────────────────────────
class BulkDeleteChannelsViewSet(viewsets.ViewSet):
class BulkDeleteChannelsAPIView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(

View file

@ -59,7 +59,7 @@ class EPGGridAPIView(APIView):
start_time__gte=now, start_time__lte=twelve_hours_later
)
count = programs.count()
logger.debug(f"EPGGridAPIView: Found {count} program(s).")
logger.debug(f"EPG`Grid`APIView: Found {count} program(s).")
serializer = ProgramDataSerializer(programs, many=True)
return Response({'data': serializer.data}, status=status.HTTP_200_OK)

View file

@ -1,5 +1,7 @@
FROM python:3.10-slim
ENV API_PORT=5656
# Add PostgreSQL repository
RUN apt-get update && apt-get install -y wget gnupg2 && \
echo "deb http://apt.postgresql.org/pub/repos/apt/ bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \

View file

@ -21,4 +21,3 @@ services:
- DJANGO_SUPERUSER_EMAIL=admin@dispatcharr.local
volumes:
- ./data:/app/data

View file

@ -11475,7 +11475,6 @@
"mkdirp": "bin/cmd.js"
}
},
<<<<<<< HEAD
"node_modules/mpd-parser": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
@ -11491,8 +11490,6 @@
"mpd-to-m3u8-json": "bin/parse.js"
}
},
=======
>>>>>>> origin/main
"node_modules/mpegts.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mpegts.js/-/mpegts.js-1.8.0.tgz",

View file

@ -157,18 +157,18 @@ export default class API {
}
// @TODO: the bulk delete endpoint is currently broken
// static async deleteChannels(channel_ids) {
// const response = await fetch(`${host}/api/channels/bulk-delete-channels/0/`, {
// method: 'DELETE',
// headers: {
// Authorization: `Bearer ${await getAuthToken()}`,
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify({ channel_ids }),
// });
static async deleteChannels(channel_ids) {
const response = await fetch(`${host}/api/channels/channels/bulk-delete/`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ channel_ids }),
});
// useChannelsStore.getState().removeChannels(channel_ids)
// }
useChannelsStore.getState().removeChannels(channel_ids);
}
static async updateChannel(values) {
const { id, ...payload } = values;
@ -225,6 +225,27 @@ export default class API {
return retval;
}
static async createChannelsFromStreams(values) {
const response = await fetch(
`${host}/api/channels/channels/from-stream/bulk/`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${await getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
}
);
const retval = await response.json();
if (retval.created.length > 0) {
useChannelsStore.getState().addChannels(retval.created);
}
return retval;
}
static async getStreams() {
const response = await fetch(`${host}/api/channels/streams/`, {
headers: {

View file

@ -30,6 +30,7 @@ import { TableHelper } from '../../helpers';
import utils from '../../utils';
import { ContentCopy } from '@mui/icons-material';
import logo from '../../images/logo.png';
import useVideoStore from '../../store/useVideoStore'; // NEW import
const Example = () => {
const [channel, setChannel] = useState(null);
@ -43,6 +44,7 @@ const Example = () => {
const [snackbarOpen, setSnackbarOpen] = useState(false);
const { channels, isLoading: channelsLoading } = useChannelsStore();
const { showVideo } = useVideoStore.getState(); // or useVideoStore()
const columns = useMemo(
//column definitions...
@ -104,6 +106,10 @@ const Example = () => {
await API.deleteChannel(id);
};
function handleWatchStream(channelNumber) {
showVideo(`/output/stream/${channelNumber}/`);
}
// @TODO: the bulk delete endpoint is currently broken
const deleteChannels = async () => {
setIsLoading(true);
@ -116,6 +122,7 @@ const Example = () => {
return deleteChannel(chan.original.id);
})
);
// await API.deleteChannels(selected.map((sel) => sel.id));
setIsLoading(false);
};
@ -227,14 +234,14 @@ const Example = () => {
>
<DeleteIcon fontSize="small" />
</IconButton>
{/* <IconButton
<IconButton
size="small"
color="error"
onClick={() => previewChannel(row.original.id)}
color="info"
onClick={() => handleWatchStream(row.original.channel_number)}
sx={{ p: 0 }}
>
<LiveTvIcon fontSize="small" />
</IconButton> */}
</IconButton>
</Box>
),
muiTableContainerProps: {

View file

@ -71,11 +71,12 @@ const Example = () => {
const selected = table
.getRowModel()
.rows.filter((row) => row.getIsSelected());
await utils.Limiter(
4,
selected.map((stream) => () => {
return createChannelFromStream(stream.original);
})
await API.createChannelsFromStreams(
selected.map((sel) => ({
stream_id: sel.original.id,
channel_name: sel.original.name,
}))
);
setIsLoading(false);
};

View file

@ -19,10 +19,10 @@ 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 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);
// Modal size constants
@ -72,7 +72,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
};
fetchPrograms();
}, [channels, activeChannels]);
}, [channels]);
// Use start/end from props or default to "today at midnight" +24h
const defaultStart = dayjs(startDate || dayjs().startOf('day'));
@ -129,7 +129,8 @@ export default function TVChannelGuide({ startDate, endDate }) {
if (guideRef.current) {
const nowOffset = dayjs().diff(start, 'minute');
const scrollPosition =
(nowOffset / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH - MINUTE_BLOCK_WIDTH;
(nowOffset / MINUTE_INCREMENT) * MINUTE_BLOCK_WIDTH -
MINUTE_BLOCK_WIDTH;
guideRef.current.scrollLeft = Math.max(scrollPosition, 0);
}
}, [programs, start]);
@ -173,7 +174,10 @@ export default function TVChannelGuide({ startDate, endDate }) {
// 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' });
event.currentTarget.scrollIntoView({
behavior: 'smooth',
inline: 'center',
});
setSelectedProgram(program);
}
@ -383,7 +387,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
width: '1px',
height: '10px',
backgroundColor: '#718096',
marginRight: i < 3 ? (HOUR_WIDTH / 4 - 1) + 'px' : 0,
marginRight: i < 3 ? HOUR_WIDTH / 4 - 1 + 'px' : 0,
}}
/>
))}
@ -473,14 +477,14 @@ export default function TVChannelGuide({ startDate, endDate }) {
<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>
)}
now.isBefore(dayjs(selectedProgram.end_time)) && (
<Button
onClick={() => handleWatchStream(selectedProgram)}
sx={{ color: '#38b2ac' }}
>
Watch Now
</Button>
)}
<Button onClick={handleCloseModal} sx={{ color: '#38b2ac' }}>
Close
</Button>

View file

@ -1,5 +1,5 @@
import { create } from "zustand";
import api from "../api";
import { create } from 'zustand';
import api from '../api';
const useChannelsStore = create((set) => ({
channels: [],
@ -13,8 +13,8 @@ const useChannelsStore = create((set) => ({
const channels = await api.getChannels();
set({ channels: channels, isLoading: false });
} catch (error) {
console.error("Failed to fetch channels:", error);
set({ error: "Failed to load channels.", isLoading: false });
console.error('Failed to fetch channels:', error);
set({ error: 'Failed to load channels.', isLoading: false });
}
},
@ -24,8 +24,8 @@ const useChannelsStore = create((set) => ({
const channelGroups = await api.getChannelGroups();
set({ channelGroups: channelGroups, isLoading: false });
} catch (error) {
console.error("Failed to fetch channel groups:", error);
set({ error: "Failed to load channel groups.", isLoading: false });
console.error('Failed to fetch channel groups:', error);
set({ error: 'Failed to load channel groups.', isLoading: false });
}
},
@ -34,17 +34,22 @@ const useChannelsStore = create((set) => ({
channels: [...state.channels, newChannel],
})),
addChannels: (newChannels) =>
set((state) => ({
channels: state.channels.concat(newChannels),
})),
updateChannel: (userAgent) =>
set((state) => ({
channels: state.channels.map((chan) =>
chan.id === userAgent.id ? userAgent : chan,
chan.id === userAgent.id ? userAgent : chan
),
})),
removeChannels: (channelIds) =>
set((state) => ({
channels: state.channels.filter(
(channel) => !channelIds.includes(channel.id),
(channel) => !channelIds.includes(channel.id)
),
})),
@ -56,7 +61,7 @@ const useChannelsStore = create((set) => ({
updateChannelGroup: (channelGroup) =>
set((state) => ({
channelGroups: state.channelGroups.map((group) =>
group.id === channelGroup.id ? channelGroup : group,
group.id === channelGroup.id ? channelGroup : group
),
})),
}));

View file

@ -8,15 +8,17 @@ const useVideoStore = create((set) => ({
isVisible: false,
streamUrl: null,
showVideo: (url) => set({
isVisible: true,
streamUrl: url,
}),
showVideo: (url) =>
set({
isVisible: true,
streamUrl: url,
}),
hideVideo: () => set({
isVisible: false,
streamUrl: null,
}),
hideVideo: () =>
set({
isVisible: false,
streamUrl: null,
}),
}));
export default useVideoStore;