mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-27 12:04:12 +00:00
commit
23b77ab0cc
16 changed files with 245 additions and 151 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ class BulkDeleteStreamsAPIView(APIView):
|
|||
# ─────────────────────────────────────────────────────────
|
||||
# 5) Bulk Delete Channels
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class BulkDeleteChannelsViewSet(viewsets.ViewSet):
|
||||
class BulkDeleteChannelsAPIView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,34 +36,6 @@ class CoreSettingsAdmin(admin.ModelAdmin):
|
|||
just list and allow editing of any instance.
|
||||
"""
|
||||
list_display = (
|
||||
"default_user_agent",
|
||||
"default_stream_profile",
|
||||
"stream_command_timeout",
|
||||
"enable_stream_logging",
|
||||
"useragent_cache_timeout",
|
||||
"streamprofile_cache_timeout",
|
||||
"streamlink_path",
|
||||
"vlc_path",
|
||||
)
|
||||
fieldsets = (
|
||||
(None, {
|
||||
"fields": (
|
||||
"default_user_agent",
|
||||
"default_stream_profile",
|
||||
"stream_command_timeout",
|
||||
"enable_stream_logging",
|
||||
)
|
||||
}),
|
||||
("Caching", {
|
||||
"fields": (
|
||||
"useragent_cache_timeout",
|
||||
"streamprofile_cache_timeout",
|
||||
)
|
||||
}),
|
||||
("Paths", {
|
||||
"fields": (
|
||||
"streamlink_path",
|
||||
"vlc_path",
|
||||
)
|
||||
}),
|
||||
"key",
|
||||
"value",
|
||||
)
|
||||
|
|
|
|||
44
core/fixtures/initial_data.json
Normal file
44
core/fixtures/initial_data.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
[
|
||||
{
|
||||
"model": "core.useragent",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"user_agent_name": "TiviMate",
|
||||
"user_agent": "TiviMate/5.1.6 (Android 12)",
|
||||
"description": "",
|
||||
"is_active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.useragent",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"user_agent_name": "VLC",
|
||||
"user_agent": "VLC/3.0.21 LibVLC 3.0.21",
|
||||
"description": "",
|
||||
"is_active": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.streamprofile",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"profile_name": "ffmpeg",
|
||||
"command": "ffmpeg",
|
||||
"parameters": "-i {streamUrl} -c:v copy -c:a copy -f mpegts pipe:1",
|
||||
"is_active": true,
|
||||
"user_agent": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.streamprofile",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"profile_name": "streamlink",
|
||||
"command": "streamlink",
|
||||
"parameters": "{streamUrl} best --stdout",
|
||||
"is_active": true,
|
||||
"user_agent": "1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -49,46 +49,16 @@ class StreamProfile(models.Model):
|
|||
|
||||
|
||||
class CoreSettings(models.Model):
|
||||
default_user_agent = models.CharField(
|
||||
max_length=512,
|
||||
default="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/112.0.0.0 Safari/537.36",
|
||||
help_text="The default User-Agent string to use if none is provided."
|
||||
)
|
||||
default_stream_profile = models.CharField(
|
||||
key = models.CharField(
|
||||
max_length=255,
|
||||
default="default_profile",
|
||||
help_text="Name or identifier for the default stream profile."
|
||||
unique=True,
|
||||
)
|
||||
stream_command_timeout = models.PositiveIntegerField(
|
||||
default=300,
|
||||
help_text="Timeout in seconds for running stream commands."
|
||||
)
|
||||
enable_stream_logging = models.BooleanField(
|
||||
default=True,
|
||||
help_text="Toggle verbose logging for stream commands."
|
||||
)
|
||||
useragent_cache_timeout = models.PositiveIntegerField(
|
||||
default=300,
|
||||
help_text="Cache timeout in seconds for user agent data."
|
||||
)
|
||||
streamprofile_cache_timeout = models.PositiveIntegerField(
|
||||
default=300,
|
||||
help_text="Cache timeout in seconds for stream profile data."
|
||||
)
|
||||
streamlink_path = models.CharField(
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
default="/usr/bin/streamlink",
|
||||
help_text="Override path for the streamlink command."
|
||||
)
|
||||
vlc_path = models.CharField(
|
||||
value = models.CharField(
|
||||
max_length=255,
|
||||
default="/usr/bin/vlc",
|
||||
help_text="Override path for the VLC command."
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Core Settings"
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Core Setting"
|
||||
verbose_name_plural = "Core Settings"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from django.shortcuts import render
|
|||
|
||||
from apps.channels.models import Channel, Stream
|
||||
from apps.m3u.models import M3UAccountProfile
|
||||
from core.models import StreamProfile
|
||||
from core.models import StreamProfile, CoreSettings
|
||||
|
||||
# Import the persistent lock (the “real” lock)
|
||||
from dispatcharr.persistent_lock import PersistentLock
|
||||
|
|
@ -89,8 +89,9 @@ def stream_view(request, stream_id):
|
|||
# Get the stream profile set on the channel.
|
||||
stream_profile = channel.stream_profile
|
||||
if not stream_profile:
|
||||
logger.error("No stream profile set for channel ID=%s", channel.id)
|
||||
return HttpResponseServerError("No stream profile set for this channel.")
|
||||
logger.error("No stream profile set for channel ID=%s, using default", channel.id)
|
||||
stream_profile = StreamProfile.objects.get(id=CoreSettings.objects.get(key="default-stream-profile").value)
|
||||
|
||||
logger.debug("Stream profile used: %s", stream_profile.profile_name)
|
||||
|
||||
# Determine the user agent to use.
|
||||
|
|
|
|||
|
|
@ -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 && \
|
||||
|
|
|
|||
|
|
@ -21,4 +21,3 @@ services:
|
|||
- DJANGO_SUPERUSER_EMAIL=admin@dispatcharr.local
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
|
||||
|
|
|
|||
72
fixtures.json
Normal file
72
fixtures.json
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
[
|
||||
{
|
||||
"model": "core.useragent",
|
||||
"fields": {
|
||||
"user_agent_name": "TiviMate",
|
||||
"user_agent": "TiviMate/5.16 (Android 12)",
|
||||
"description": "",
|
||||
"is_active": true,
|
||||
"created_at": "2025-02-28T20:35:14.668Z",
|
||||
"updated_at": "2025-02-28T20:35:14.668Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.useragent",
|
||||
"fields": {
|
||||
"user_agent_name": "VLC",
|
||||
"user_agent": "VLC/3.0.21 LibVLC/3.0.21",
|
||||
"description": "",
|
||||
"is_active": true,
|
||||
"created_at": "2025-02-28T20:35:14.668Z",
|
||||
"updated_at": "2025-02-28T20:35:14.668Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.useragent",
|
||||
"fields": {
|
||||
"user_agent_name": "Chrome",
|
||||
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.3",
|
||||
"description": "",
|
||||
"is_active": true,
|
||||
"created_at": "2025-02-28T20:35:14.668Z",
|
||||
"updated_at": "2025-02-28T20:35:14.668Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.streamprofile",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"profile_name": "ffmpeg",
|
||||
"command": "ffmpeg",
|
||||
"parameters": "-i {streamUrl} -c:a copy -c:v copy -f mpegts pipe:1",
|
||||
"is_active": true,
|
||||
"user_agent": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.streamprofile",
|
||||
"fields": {
|
||||
"profile_name": "streamlink",
|
||||
"command": "streamlink",
|
||||
"parameters": "{streamUrl} best --stdout",
|
||||
"is_active": true,
|
||||
"user_agent": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.coresettings",
|
||||
"fields": {
|
||||
"key": "default-user-agent",
|
||||
"name": "Default User-Agent",
|
||||
"value": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "core.coresettings",
|
||||
"fields": {
|
||||
"key": "default-stream-profile",
|
||||
"name": "Default Stream Profile",
|
||||
"value": "1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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;
|
||||
|
|
@ -235,6 +235,27 @@ static async assignChannelNumbers(channelIds) {
|
|||
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: {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import ChannelForm from '../forms/Channel';
|
|||
import { TableHelper } from '../../helpers';
|
||||
import utils from '../../utils';
|
||||
import logo from '../../images/logo.png';
|
||||
import useVideoStore from '../../store/useVideoStore'; // NEW import
|
||||
|
||||
const ChannelsTable = () => {
|
||||
const [channel, setChannel] = useState(null);
|
||||
|
|
@ -42,6 +43,7 @@ const ChannelsTable = () => {
|
|||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
|
||||
const { channels, isLoading: channelsLoading } = useChannelsStore();
|
||||
const { showVideo } = useVideoStore.getState(); // or useVideoStore()
|
||||
|
||||
// Configure columns
|
||||
const columns = useMemo(
|
||||
|
|
@ -100,6 +102,10 @@ const ChannelsTable = () => {
|
|||
await API.deleteChannel(id);
|
||||
};
|
||||
|
||||
function handleWatchStream(channelNumber) {
|
||||
showVideo(`/output/stream/${channelNumber}/`);
|
||||
}
|
||||
|
||||
// (Optional) bulk delete, but your endpoint is @TODO
|
||||
const deleteChannels = async () => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -110,6 +116,7 @@ const ChannelsTable = () => {
|
|||
4,
|
||||
selected.map((chan) => () => deleteChannel(chan.original.id))
|
||||
);
|
||||
// await API.deleteChannels(selected.map((sel) => sel.id));
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
|
|
@ -176,15 +183,21 @@ const ChannelsTable = () => {
|
|||
// Example copy URLs
|
||||
const copyM3UUrl = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy(`${window.location.protocol}//${window.location.host}/output/m3u`);
|
||||
setTextToCopy(
|
||||
`${window.location.protocol}//${window.location.host}/output/m3u`
|
||||
);
|
||||
};
|
||||
const copyEPGUrl = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy(`${window.location.protocol}//${window.location.host}/output/epg`);
|
||||
setTextToCopy(
|
||||
`${window.location.protocol}//${window.location.host}/output/epg`
|
||||
);
|
||||
};
|
||||
const copyHDHRUrl = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
setTextToCopy(`${window.location.protocol}//${window.location.host}/output/hdhr`);
|
||||
setTextToCopy(
|
||||
`${window.location.protocol}//${window.location.host}/output/hdhr`
|
||||
);
|
||||
};
|
||||
|
||||
// Configure the MaterialReactTable
|
||||
|
|
@ -228,11 +241,14 @@ const ChannelsTable = () => {
|
|||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{/* If you had a channel preview:
|
||||
<IconButton size="small" color="error" onClick={() => previewChannel(row.original.id)} sx={{ p: 0 }}>
|
||||
<LiveTvIcon fontSize="small" />
|
||||
</IconButton>
|
||||
*/}
|
||||
<IconButton
|
||||
size="small"
|
||||
color="info"
|
||||
onClick={() => handleWatchStream(row.original.channel_number)}
|
||||
sx={{ p: 0 }}
|
||||
>
|
||||
<LiveTvIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
muiTableContainerProps: {
|
||||
|
|
|
|||
|
|
@ -65,33 +65,15 @@ const StreamsTable = () => {
|
|||
// Get all selected streams from the table
|
||||
const selected = table
|
||||
.getRowModel()
|
||||
.rows.filter((row) => row.getIsSelected())
|
||||
.map((row) => row.original);
|
||||
if (!selected.length) return;
|
||||
.rows.filter((row) => row.getIsSelected());
|
||||
|
||||
// Build payload: an array of objects with stream_id and channel_name
|
||||
const payload = selected.map((stream) => ({
|
||||
stream_id: stream.id,
|
||||
channel_name: stream.name || `Channel from ${stream.id}`,
|
||||
}));
|
||||
|
||||
// Retrieve the auth token (make sure getAuthToken is exported from your API file)
|
||||
const token = await getAuthToken();
|
||||
|
||||
// Send a POST request to your bulk endpoint
|
||||
const response = await fetch(`/api/channels/channels/from-stream/bulk/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const result = await response.json();
|
||||
console.log("Bulk create result:", result);
|
||||
|
||||
// Refresh the channels store so new channels appear in your UI.
|
||||
await useChannelsStore.getState().fetchChannels();
|
||||
await API.createChannelsFromStreams(
|
||||
selected.map((sel) => ({
|
||||
stream_id: sel.original.id,
|
||||
channel_name: sel.original.name,
|
||||
}))
|
||||
);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const editStream = async (stream = null) => {
|
||||
|
|
@ -219,7 +201,11 @@ const StreamsTable = () => {
|
|||
return (
|
||||
<Box>
|
||||
<MaterialReactTable table={table} />
|
||||
<StreamForm stream={stream} isOpen={modalOpen} onClose={closeStreamForm} />
|
||||
<StreamForm
|
||||
stream={stream}
|
||||
isOpen={modalOpen}
|
||||
onClose={closeStreamForm}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
),
|
||||
})),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue