mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 10:45:27 +00:00
updated timestamp and extension checks for m3 uand epg
This commit is contained in:
parent
20c8ff2179
commit
e507c6f23c
14 changed files with 106 additions and 53 deletions
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 5.1.6 on 2025-04-07 16:29
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epg', '0007_populate_periodic_tasks'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='epgsource',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Time when this source was created'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='epgsource',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Time when this source was last updated'),
|
||||
),
|
||||
]
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.1.6 on 2025-04-07 16:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epg', '0008_epgsource_created_at_epgsource_updated_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='epgsource',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, help_text='Time when this source was created'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='epgsource',
|
||||
name='updated_at',
|
||||
field=models.DateTimeField(auto_now=True, help_text='Time when this source was last updated'),
|
||||
),
|
||||
]
|
||||
|
|
@ -17,6 +17,14 @@ class EPGSource(models.Model):
|
|||
refresh_task = models.ForeignKey(
|
||||
PeriodicTask, on_delete=models.SET_NULL, null=True, blank=True
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
help_text="Time when this source was created"
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
help_text="Time when this source was last updated"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ from apps.channels.models import Channel
|
|||
|
||||
class EPGSourceSerializer(serializers.ModelSerializer):
|
||||
epg_data_ids = serializers.SerializerMethodField()
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
class Meta:
|
||||
model = EPGSource
|
||||
fields = ['id', 'name', 'source_type', 'url', 'api_key', 'is_active', 'epg_data_ids', 'refresh_interval']
|
||||
fields = ['id', 'name', 'source_type', 'url', 'api_key', 'is_active', 'epg_data_ids', 'refresh_interval', 'created_at', 'updated_at']
|
||||
|
||||
def get_epg_data_ids(self, obj):
|
||||
return list(obj.epgs.values_list('id', flat=True))
|
||||
|
|
|
|||
|
|
@ -50,6 +50,8 @@ def refresh_epg_data(source_id):
|
|||
elif source.source_type == 'schedules_direct':
|
||||
fetch_schedules_direct(source)
|
||||
|
||||
source.save(update_fields=['updated_at'])
|
||||
|
||||
release_task_lock('refresh_epg_data', source_id)
|
||||
|
||||
def fetch_xmltv(source):
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ class M3UAccountSerializer(serializers.ModelSerializer):
|
|||
required=True
|
||||
)
|
||||
profiles = M3UAccountProfileSerializer(many=True, read_only=True)
|
||||
read_only_fields = ['locked']
|
||||
read_only_fields = ['locked', 'created_at', 'updated_at']
|
||||
# channel_groups = serializers.SerializerMethodField()
|
||||
channel_groups = ChannelGroupM3UAccountSerializer(source='channel_group', many=True, required=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ def refresh_single_m3u_account(account_id):
|
|||
|
||||
# Calculate elapsed time
|
||||
elapsed_time = end_time - start_time
|
||||
account.save(update_fields=['updated_at'])
|
||||
|
||||
print(f"Function took {elapsed_time} seconds to execute.")
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ ENV PATH="/dispatcharrpy/bin:$PATH" \
|
|||
# Copy the virtual environment and application from the builder stage
|
||||
COPY --from=builder /dispatcharrpy /dispatcharrpy
|
||||
COPY --from=builder /app /app
|
||||
COPY --from=frontend-builder /app/frontend /app/frontend
|
||||
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
||||
|
||||
# Run collectstatic after frontend assets are copied
|
||||
RUN cd /app && python manage.py collectstatic --noinput
|
||||
|
|
|
|||
|
|
@ -1,40 +1,31 @@
|
|||
// Modal.js
|
||||
import React, { useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import React from 'react';
|
||||
import API from '../../api';
|
||||
import { Flex, TextInput, Button, Modal } from '@mantine/core';
|
||||
import { isNotEmpty, useForm } from '@mantine/form';
|
||||
|
||||
const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
|
||||
const formik = useFormik({
|
||||
const form = useForm({
|
||||
mode: 'uncontrolled',
|
||||
initialValues: {
|
||||
name: '',
|
||||
name: channelGroup ? channelGroup.name : '',
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
name: Yup.string().required('Name is required'),
|
||||
}),
|
||||
onSubmit: async (values, { setSubmitting, resetForm }) => {
|
||||
if (channelGroup?.id) {
|
||||
await API.updateChannelGroup({ id: channelGroup.id, ...values });
|
||||
} else {
|
||||
await API.addChannelGroup(values);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
validate: {
|
||||
name: isNotEmpty('Specify a name'),
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const onSubmit = async () => {
|
||||
const values = form.getValues();
|
||||
if (channelGroup) {
|
||||
formik.setValues({
|
||||
name: channelGroup.name,
|
||||
});
|
||||
await API.updateChannelGroup({ id: channelGroup.id, ...values });
|
||||
} else {
|
||||
formik.resetForm();
|
||||
await API.addChannelGroup(values);
|
||||
}
|
||||
}, [channelGroup]);
|
||||
|
||||
return form.reset();
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return <></>;
|
||||
|
|
@ -42,14 +33,13 @@ const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
|
|||
|
||||
return (
|
||||
<Modal opened={isOpen} onClose={onClose} title="Channel Group">
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<TextInput
|
||||
id="name"
|
||||
name="name"
|
||||
label="Name"
|
||||
value={formik.values.name}
|
||||
onChange={formik.handleChange}
|
||||
error={formik.touched.name}
|
||||
{...form.getInputProps('name')}
|
||||
key={form.key('name')}
|
||||
/>
|
||||
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
|
|
@ -57,7 +47,7 @@ const ChannelGroup = ({ channelGroup = null, isOpen, onClose }) => {
|
|||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={formik.isSubmitting}
|
||||
disabled={form.submitting}
|
||||
size="small"
|
||||
>
|
||||
Submit
|
||||
|
|
|
|||
|
|
@ -1,22 +1,7 @@
|
|||
// Modal.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import React from 'react';
|
||||
import API from '../../api';
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
import {
|
||||
LoadingOverlay,
|
||||
TextInput,
|
||||
Button,
|
||||
Checkbox,
|
||||
Modal,
|
||||
Flex,
|
||||
NativeSelect,
|
||||
NumberInput,
|
||||
Space,
|
||||
Select,
|
||||
Alert,
|
||||
} from '@mantine/core';
|
||||
import { Button, Modal, Flex, Select, Alert } from '@mantine/core';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { CircleAlert } from 'lucide-react';
|
||||
|
|
@ -61,6 +46,8 @@ const DVR = ({ recording = null, channel = null, isOpen, onClose }) => {
|
|||
...values,
|
||||
channel: channel_id,
|
||||
});
|
||||
|
||||
form.reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
@ -110,7 +97,12 @@ const DVR = ({ recording = null, channel = null, isOpen, onClose }) => {
|
|||
/>
|
||||
|
||||
<Flex mih={50} gap="xs" justify="flex-end" align="flex-end">
|
||||
<Button type="submit" variant="contained" size="small">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={form.submitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -892,7 +892,7 @@ const ChannelsTable = ({}) => {
|
|||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="transparent" size="sm">
|
||||
<CircleEllipsis size="18" />
|
||||
<EllipsisVertical size="18" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
import { notifications } from '@mantine/notifications';
|
||||
import { IconSquarePlus } from '@tabler/icons-react';
|
||||
import { RefreshCcw, SquareMinus, SquarePen } from 'lucide-react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const EPGsTable = () => {
|
||||
const [epg, setEPG] = useState(null);
|
||||
|
|
@ -44,6 +45,11 @@ const EPGsTable = () => {
|
|||
accessorKey: 'max_streams',
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
header: 'Updated',
|
||||
accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'),
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from '@mantine/core';
|
||||
import { SquareMinus, SquarePen, RefreshCcw, Check, X } from 'lucide-react';
|
||||
import { IconSquarePlus } from '@tabler/icons-react'; // Import custom icons
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const M3UTable = () => {
|
||||
const [playlist, setPlaylist] = useState(null);
|
||||
|
|
@ -70,6 +71,11 @@ const M3UTable = () => {
|
|||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Updated',
|
||||
accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'),
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
|
|||
|
|
@ -503,7 +503,7 @@ const StreamsTable = ({}) => {
|
|||
<>
|
||||
<Tooltip label="Add to Channel">
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
size="xs"
|
||||
color={theme.tailwind.blue[6]}
|
||||
variant="transparent"
|
||||
onClick={() => addStreamToChannel(row.original.id)}
|
||||
|
|
@ -522,7 +522,7 @@ const StreamsTable = ({}) => {
|
|||
|
||||
<Tooltip label="Create New Channel">
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
size="xs"
|
||||
color={theme.tailwind.green[5]}
|
||||
variant="transparent"
|
||||
onClick={() => createChannelFromStream(row.original)}
|
||||
|
|
@ -533,7 +533,7 @@ const StreamsTable = ({}) => {
|
|||
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="transparent" size="sm">
|
||||
<ActionIcon variant="transparent" size="xs">
|
||||
<EllipsisVertical size="18" />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue