diff --git a/apps/epg/migrations/0008_epgsource_created_at_epgsource_updated_at.py b/apps/epg/migrations/0008_epgsource_created_at_epgsource_updated_at.py new file mode 100644 index 00000000..1dcfeed0 --- /dev/null +++ b/apps/epg/migrations/0008_epgsource_created_at_epgsource_updated_at.py @@ -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'), + ), + ] diff --git a/apps/epg/migrations/0009_alter_epgsource_created_at_and_more.py b/apps/epg/migrations/0009_alter_epgsource_created_at_and_more.py new file mode 100644 index 00000000..cb8088eb --- /dev/null +++ b/apps/epg/migrations/0009_alter_epgsource_created_at_and_more.py @@ -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'), + ), + ] diff --git a/apps/epg/models.py b/apps/epg/models.py index 3f9b018d..09986bfe 100644 --- a/apps/epg/models.py +++ b/apps/epg/models.py @@ -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 diff --git a/apps/epg/serializers.py b/apps/epg/serializers.py index e4a2a4b3..e4ff932e 100644 --- a/apps/epg/serializers.py +++ b/apps/epg/serializers.py @@ -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)) diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 3b84df6d..33e981a6 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -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): diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index e7dbfcea..d3948145 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -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) diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index e2de2af3..82dd7864 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -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.") diff --git a/docker/Dockerfile b/docker/Dockerfile index 9a576eeb..e3f8a165 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 diff --git a/frontend/src/components/forms/ChannelGroup.jsx b/frontend/src/components/forms/ChannelGroup.jsx index 93741ef1..2429d1d9 100644 --- a/frontend/src/components/forms/ChannelGroup.jsx +++ b/frontend/src/components/forms/ChannelGroup.jsx @@ -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 ( -
+ @@ -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 diff --git a/frontend/src/components/forms/Recording.jsx b/frontend/src/components/forms/Recording.jsx index db19e4a1..a4aaf266 100644 --- a/frontend/src/components/forms/Recording.jsx +++ b/frontend/src/components/forms/Recording.jsx @@ -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 }) => { /> - diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 599e7c4f..d2520cf7 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -892,7 +892,7 @@ const ChannelsTable = ({}) => { - + diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx index b3ad03f7..12d542ae 100644 --- a/frontend/src/components/tables/EPGsTable.jsx +++ b/frontend/src/components/tables/EPGsTable.jsx @@ -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, + }, ], [] ); diff --git a/frontend/src/components/tables/M3UsTable.jsx b/frontend/src/components/tables/M3UsTable.jsx index f079c98a..95ba9e93 100644 --- a/frontend/src/components/tables/M3UsTable.jsx +++ b/frontend/src/components/tables/M3UsTable.jsx @@ -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 = () => { ), }, + { + header: 'Updated', + accessorFn: (row) => dayjs(row.updated_at).format('MMMM D, YYYY h:mma'), + enableSorting: false, + }, ], [] ); diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index 9e431d17..4cfecab0 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -503,7 +503,7 @@ const StreamsTable = ({}) => { <> addStreamToChannel(row.original.id)} @@ -522,7 +522,7 @@ const StreamsTable = ({}) => { createChannelFromStream(row.original)} @@ -533,7 +533,7 @@ const StreamsTable = ({}) => { - +