mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
fix(epg): add force refresh option for EPG data import and update related API methods
This commit is contained in:
parent
ae56407405
commit
5d2bc2606c
4 changed files with 89 additions and 36 deletions
|
|
@ -999,6 +999,7 @@ class EPGImportAPIView(APIView):
|
|||
def post(self, request, format=None):
|
||||
logger.info("EPGImportAPIView: Received request to import EPG data.")
|
||||
epg_id = request.data.get("id", None)
|
||||
force = bool(request.data.get("force", False))
|
||||
|
||||
# Check if this is a dummy EPG source
|
||||
try:
|
||||
|
|
@ -1013,7 +1014,7 @@ class EPGImportAPIView(APIView):
|
|||
except EPGSource.DoesNotExist:
|
||||
pass # Let the task handle the missing source
|
||||
|
||||
refresh_epg_data.delay(epg_id) # Trigger Celery task
|
||||
refresh_epg_data.delay(epg_id, force=force) # Trigger Celery task
|
||||
logger.info("EPGImportAPIView: Task dispatched to refresh EPG data.")
|
||||
return Response(
|
||||
{"success": True, "message": "EPG data refresh initiated."},
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ def refresh_all_epg_data():
|
|||
|
||||
|
||||
@shared_task(time_limit=14400)
|
||||
def refresh_epg_data(source_id):
|
||||
def refresh_epg_data(source_id, force=False):
|
||||
if not acquire_task_lock('refresh_epg_data', source_id):
|
||||
logger.debug(f"EPG refresh for {source_id} already running")
|
||||
return
|
||||
|
|
@ -378,7 +378,7 @@ def refresh_epg_data(source_id):
|
|||
parse_programs_for_source(source)
|
||||
|
||||
elif source.source_type == 'schedules_direct':
|
||||
fetch_schedules_direct(source)
|
||||
fetch_schedules_direct(source, force=force)
|
||||
|
||||
source.save(update_fields=['updated_at'])
|
||||
# After successful EPG refresh, evaluate DVR series rules to schedule new episodes
|
||||
|
|
@ -1973,7 +1973,7 @@ def fetch_schedules_direct_stations(self, source_id):
|
|||
fetch_schedules_direct(source, stations_only=True)
|
||||
|
||||
|
||||
def fetch_schedules_direct(source, stations_only=False):
|
||||
def fetch_schedules_direct(source, stations_only=False, force=False):
|
||||
"""
|
||||
Fetch EPG data from the Schedules Direct JSON API and persist it to the
|
||||
EPGData / ProgramData models.
|
||||
|
|
@ -2032,7 +2032,7 @@ def fetch_schedules_direct(source, stations_only=False):
|
|||
# updated_at, which would otherwise incorrectly trigger this guard). Always
|
||||
# allow the first full refresh through so guide data is immediately available.
|
||||
# -------------------------------------------------------------------------
|
||||
if not stations_only and source.updated_at:
|
||||
if not stations_only and not force and source.updated_at:
|
||||
from apps.epg.models import SDScheduleMD5 as _SDScheduleMD5
|
||||
has_prior_full_refresh = _SDScheduleMD5.objects.filter(epg_source=source).exists()
|
||||
if has_prior_full_refresh:
|
||||
|
|
@ -2053,6 +2053,8 @@ def fetch_schedules_direct(source, stations_only=False):
|
|||
return
|
||||
else:
|
||||
logger.info(f"SD source {source.id}: No prior full refresh detected — skipping 2-hour guard for first full fetch.")
|
||||
elif force and not stations_only:
|
||||
logger.info(f"SD source {source.id}: Force flag set — bypassing 2-hour refresh guard.")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Build SD-specific headers
|
||||
|
|
|
|||
|
|
@ -1632,11 +1632,11 @@ export default class API {
|
|||
}
|
||||
}
|
||||
|
||||
static async refreshEPG(id) {
|
||||
static async refreshEPG(id, force = false) {
|
||||
try {
|
||||
const response = await request(`${host}/api/epg/import/`, {
|
||||
method: 'POST',
|
||||
body: { id },
|
||||
body: { id, force },
|
||||
});
|
||||
|
||||
return response;
|
||||
|
|
@ -3820,7 +3820,9 @@ export default class API {
|
|||
|
||||
static async getSDLineups(sourceId) {
|
||||
try {
|
||||
const response = await request(`${host}/api/epg/sources/${sourceId}/sd-lineups/`);
|
||||
const response = await request(
|
||||
`${host}/api/epg/sources/${sourceId}/sd-lineups/`
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to retrieve Schedules Direct lineups', e);
|
||||
|
|
@ -3829,10 +3831,13 @@ export default class API {
|
|||
|
||||
static async addSDLineup(sourceId, lineup) {
|
||||
try {
|
||||
const response = await request(`${host}/api/epg/sources/${sourceId}/sd-lineups/`, {
|
||||
method: 'POST',
|
||||
body: { lineup },
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/epg/sources/${sourceId}/sd-lineups/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { lineup },
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification(`Failed to add lineup ${lineup}`, e);
|
||||
|
|
@ -3841,10 +3846,13 @@ export default class API {
|
|||
|
||||
static async deleteSDLineup(sourceId, lineup) {
|
||||
try {
|
||||
const response = await request(`${host}/api/epg/sources/${sourceId}/sd-lineups/`, {
|
||||
method: 'DELETE',
|
||||
body: { lineup },
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/epg/sources/${sourceId}/sd-lineups/`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
body: { lineup },
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification(`Failed to remove lineup ${lineup}`, e);
|
||||
|
|
@ -3853,10 +3861,13 @@ export default class API {
|
|||
|
||||
static async searchSDLineups(sourceId, country, postalcode) {
|
||||
try {
|
||||
const response = await request(`${host}/api/epg/sources/${sourceId}/sd-lineups/search/`, {
|
||||
method: 'POST',
|
||||
body: { country, postalcode },
|
||||
});
|
||||
const response = await request(
|
||||
`${host}/api/epg/sources/${sourceId}/sd-lineups/search/`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: { country, postalcode },
|
||||
}
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
errorNotification('Failed to search Schedules Direct lineups', e);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ const RowActions = ({ tableSize, row, editEPG, deleteEPG, refreshEPG }) => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
const EPGStatusCell = ({ epg }) => {
|
||||
// Direct Zustand subscription scoped to this source only.
|
||||
// This component re-renders whenever its source's progress changes,
|
||||
|
|
@ -138,9 +137,15 @@ const EPGStatusCell = ({ epg }) => {
|
|||
let additionalInfo = '';
|
||||
if (progress.message) {
|
||||
additionalInfo = progress.message;
|
||||
} else if (progress.processed !== undefined && progress.channels !== undefined) {
|
||||
} else if (
|
||||
progress.processed !== undefined &&
|
||||
progress.channels !== undefined
|
||||
) {
|
||||
additionalInfo = `${progress.processed.toLocaleString()} programs for ${progress.channels} channels`;
|
||||
} else if (progress.processed !== undefined && progress.total !== undefined) {
|
||||
} else if (
|
||||
progress.processed !== undefined &&
|
||||
progress.total !== undefined
|
||||
) {
|
||||
additionalInfo = `${progress.processed.toLocaleString()} / ${progress.total.toLocaleString()}`;
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +191,8 @@ const EPGStatusCell = ({ epg }) => {
|
|||
|
||||
// Show success message
|
||||
if (epg.status === 'success') {
|
||||
const successMessage = epg.last_message || 'EPG data refreshed successfully';
|
||||
const successMessage =
|
||||
epg.last_message || 'EPG data refreshed successfully';
|
||||
return (
|
||||
<Tooltip label={successMessage} multiline width={300}>
|
||||
<Text
|
||||
|
|
@ -205,12 +211,7 @@ const EPGStatusCell = ({ epg }) => {
|
|||
if (epg.status === 'idle' && epg.last_message) {
|
||||
return (
|
||||
<Tooltip label={epg.last_message} multiline width={300}>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs"
|
||||
lineClamp={2}
|
||||
style={{ lineHeight: 1.3 }}
|
||||
>
|
||||
<Text c="dimmed" size="xs" lineClamp={2} style={{ lineHeight: 1.3 }}>
|
||||
{epg.last_message}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
|
|
@ -230,6 +231,8 @@ const EPGsTable = () => {
|
|||
const [epgToDelete, setEpgToDelete] = useState(null);
|
||||
const [data, setData] = useState([]);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [confirmSDRefreshOpen, setConfirmSDRefreshOpen] = useState(false);
|
||||
const [sdRefreshTarget, setSDRefreshTarget] = useState(null);
|
||||
|
||||
const epgs = useEPGsStore((s) => s.epgs);
|
||||
|
||||
|
|
@ -274,9 +277,9 @@ const EPGsTable = () => {
|
|||
size: 130,
|
||||
cell: ({ cell }) => {
|
||||
const typeMap = {
|
||||
'xmltv': 'XMLTV',
|
||||
'schedules_direct': 'Schedules Direct',
|
||||
'dummy': 'Custom Dummy',
|
||||
xmltv: 'XMLTV',
|
||||
schedules_direct: 'Schedules Direct',
|
||||
dummy: 'Custom Dummy',
|
||||
};
|
||||
return typeMap[cell.getValue()] || cell.getValue();
|
||||
},
|
||||
|
|
@ -439,13 +442,27 @@ const EPGsTable = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const refreshEPG = async (id) => {
|
||||
await API.refreshEPG(id);
|
||||
const refreshEPG = async (id, force = false) => {
|
||||
await API.refreshEPG(id, force);
|
||||
notifications.show({
|
||||
title: 'EPG refresh initiated',
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefreshEPG = (id) => {
|
||||
const epgObj = epgs[id];
|
||||
if (
|
||||
epgObj?.source_type === 'schedules_direct' &&
|
||||
epgObj?.updated_at &&
|
||||
Date.now() - new Date(epgObj.updated_at).getTime() < 2 * 60 * 60 * 1000
|
||||
) {
|
||||
setSDRefreshTarget(id);
|
||||
setConfirmSDRefreshOpen(true);
|
||||
return;
|
||||
}
|
||||
refreshEPG(id);
|
||||
};
|
||||
|
||||
const closeEPGForm = () => {
|
||||
setEPG(null);
|
||||
setEPGModalOpen(false);
|
||||
|
|
@ -478,7 +495,7 @@ const EPGsTable = () => {
|
|||
row={row}
|
||||
editEPG={editEPG}
|
||||
deleteEPG={deleteEPG}
|
||||
refreshEPG={refreshEPG}
|
||||
refreshEPG={handleRefreshEPG}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -686,6 +703,28 @@ const EPGsTable = () => {
|
|||
onClose={closeDummyEPGForm}
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={confirmSDRefreshOpen}
|
||||
onClose={() => setConfirmSDRefreshOpen(false)}
|
||||
onConfirm={() => {
|
||||
setConfirmSDRefreshOpen(false);
|
||||
refreshEPG(sdRefreshTarget, true);
|
||||
}}
|
||||
title="Refresh Schedules Direct Early?"
|
||||
message={
|
||||
<div>
|
||||
<p>This source was refreshed less than 2 hours ago.</p>
|
||||
<p>
|
||||
Schedules Direct rate-limits requests per account. Refreshing too
|
||||
frequently may cause your account to be temporarily blocked.
|
||||
</p>
|
||||
<p>Are you sure you want to force a refresh now?</p>
|
||||
</div>
|
||||
}
|
||||
confirmLabel="Refresh Anyway"
|
||||
cancelLabel="Cancel"
|
||||
/>
|
||||
|
||||
<ConfirmationDialog
|
||||
opened={confirmDeleteOpen}
|
||||
onClose={() => setConfirmDeleteOpen(false)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue