mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-29 04:50:20 +00:00
Fix cyclic error with logos loading.
This commit is contained in:
parent
73065ed319
commit
d2b852c9a2
6 changed files with 144 additions and 102 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-25 22:26
|
||||
# Generated by Django 5.2.4 on 2025-08-28 14:46
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
|
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
|||
('custom_properties', models.JSONField(blank=True, help_text='Additional metadata and properties for the movie', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dispatcharr_channels.logo')),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='movie', to='dispatcharr_channels.logo')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Movie',
|
||||
|
|
@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
|||
('custom_properties', models.JSONField(blank=True, help_text='Additional metadata and properties for the series', null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dispatcharr_channels.logo')),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='series', to='dispatcharr_channels.logo')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Series',
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
# Generated by Django 5.2.4 on 2025-08-26 16:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dispatcharr_channels', '0024_alter_channelgroupm3uaccount_channel_group'),
|
||||
('vod', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='movie',
|
||||
name='logo',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='movie', to='dispatcharr_channels.logo'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='series',
|
||||
name='logo',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='series', to='dispatcharr_channels.logo'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,66 +1,122 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Skeleton } from '@mantine/core';
|
||||
import useLogosStore from '../store/logos';
|
||||
import logo from '../images/logo.png'; // Default logo
|
||||
|
||||
// Global request queue to batch logo requests
|
||||
const logoRequestQueue = new Set();
|
||||
let logoRequestTimer = null;
|
||||
|
||||
const LazyLogo = ({
|
||||
logoId,
|
||||
alt = 'logo',
|
||||
style = { maxHeight: 18, maxWidth: 55 },
|
||||
fallbackSrc = logo,
|
||||
...props
|
||||
logoId,
|
||||
alt = 'logo',
|
||||
style = { maxHeight: 18, maxWidth: 55 },
|
||||
fallbackSrc = logo,
|
||||
...props
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const fetchAttempted = useRef(new Set()); // Track which IDs we've already tried to fetch
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// Determine the logo source
|
||||
const logoData = logoId && logos[logoId];
|
||||
const logoSrc = logoData?.cache_url || (logoId ? `/api/channels/logos/${logoId}/cache/` : fallbackSrc);
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds);
|
||||
|
||||
useEffect(() => {
|
||||
// If we have a logoId but no logo data, try to fetch it
|
||||
if (logoId && !logoData && !isLoading && !hasError) {
|
||||
setIsLoading(true);
|
||||
fetchLogosByIds([logoId])
|
||||
.then(() => {
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(`Failed to load logo ${logoId}:`, error);
|
||||
setIsLoading(false);
|
||||
setHasError(true);
|
||||
});
|
||||
// Determine the logo source
|
||||
const logoData = logoId && logos[logoId];
|
||||
const logoSrc = logoData?.cache_url || fallbackSrc; // Only use cache URL if we have logo data
|
||||
|
||||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// If we have a logoId but no logo data, add it to the batch request queue
|
||||
if (
|
||||
logoId &&
|
||||
!logoData &&
|
||||
!isLoading &&
|
||||
!hasError &&
|
||||
!fetchAttempted.current.has(logoId) &&
|
||||
isMountedRef.current
|
||||
) {
|
||||
setIsLoading(true);
|
||||
fetchAttempted.current.add(logoId); // Mark this ID as attempted
|
||||
logoRequestQueue.add(logoId);
|
||||
|
||||
// Clear existing timer and set new one to batch requests
|
||||
if (logoRequestTimer) {
|
||||
clearTimeout(logoRequestTimer);
|
||||
}
|
||||
|
||||
logoRequestTimer = setTimeout(async () => {
|
||||
if (logoRequestQueue.size > 0) {
|
||||
const idsToFetch = Array.from(logoRequestQueue);
|
||||
logoRequestQueue.clear();
|
||||
|
||||
try {
|
||||
await fetchLogosByIds(idsToFetch);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load logos:`, error);
|
||||
// Mark failed IDs so they can be retried
|
||||
idsToFetch.forEach((id) => {
|
||||
if (fetchAttempted.current.has(id)) {
|
||||
fetchAttempted.current.delete(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [logoId, logoData, fetchLogosByIds, isLoading, hasError]);
|
||||
|
||||
// Show skeleton while loading
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Skeleton
|
||||
height={style.maxHeight || 18}
|
||||
width={style.maxWidth || 55}
|
||||
style={{ ...style, borderRadius: 4 }}
|
||||
/>
|
||||
);
|
||||
// Update loading state for all components
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, 100); // Batch requests for 100ms
|
||||
}
|
||||
|
||||
// Show image (will use fallback if logo fails to load)
|
||||
// If we now have the logo data, stop loading
|
||||
if (logoData && isLoading && isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [logoId, fetchLogosByIds, logoData]); // Include logoData to detect when it becomes available
|
||||
|
||||
// Reset error state when logoId changes
|
||||
useEffect(() => {
|
||||
if (logoId) {
|
||||
setHasError(false);
|
||||
}
|
||||
}, [logoId]);
|
||||
|
||||
// Show skeleton while loading
|
||||
if (isLoading && !logoData) {
|
||||
return (
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt={alt}
|
||||
style={style}
|
||||
onError={(e) => {
|
||||
if (!hasError) {
|
||||
setHasError(true);
|
||||
e.target.src = fallbackSrc;
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<Skeleton
|
||||
height={style.maxHeight || 18}
|
||||
width={style.maxWidth || 55}
|
||||
style={{ ...style, borderRadius: 4 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Show image (will use fallback if logo fails to load)
|
||||
return (
|
||||
<img
|
||||
src={logoSrc}
|
||||
alt={alt}
|
||||
style={style}
|
||||
onError={(e) => {
|
||||
if (!hasError) {
|
||||
setHasError(true);
|
||||
e.target.src = fallbackSrc;
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LazyLogo;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
|
|
@ -70,7 +70,6 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
const [selectedEPG, setSelectedEPG] = useState('');
|
||||
const [tvgFilter, setTvgFilter] = useState('');
|
||||
const [logoFilter, setLogoFilter] = useState('');
|
||||
const [logoOptions, setLogoOptions] = useState([]);
|
||||
|
||||
const [groupPopoverOpened, setGroupPopoverOpened] = useState(false);
|
||||
const [groupFilter, setGroupFilter] = useState('');
|
||||
|
|
@ -241,9 +240,10 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => {
|
|||
}
|
||||
}, [channel, tvgsById, channelGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogoOptions([{ id: '0', name: 'Default' }].concat(Object.values(logos)));
|
||||
}, [logos]);
|
||||
// Memoize logo options to prevent infinite re-renders during background loading
|
||||
const logoOptions = useMemo(() => {
|
||||
return [{ id: '0', name: 'Default' }].concat(Object.values(logos));
|
||||
}, [logos]); // Only depend on logos object
|
||||
|
||||
const renderLogoOption = ({ option, checked }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import useChannelsStore from '../../store/channels';
|
||||
|
|
@ -63,7 +63,6 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
const [selectedEPG, setSelectedEPG] = useState('');
|
||||
const [tvgFilter, setTvgFilter] = useState('');
|
||||
const [logoFilter, setLogoFilter] = useState('');
|
||||
const [logoOptions, setLogoOptions] = useState([]);
|
||||
|
||||
const [groupPopoverOpened, setGroupPopoverOpened] = useState(false);
|
||||
const [groupFilter, setGroupFilter] = useState('');
|
||||
|
|
@ -232,9 +231,10 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => {
|
|||
}
|
||||
}, [channel, tvgsById, channelGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogoOptions([{ id: '0', name: 'Default' }].concat(Object.values(logos)));
|
||||
}, [logos]);
|
||||
// Memoize logo options to prevent infinite re-renders during background loading
|
||||
const logoOptions = useMemo(() => {
|
||||
return [{ id: '0', name: 'Default' }].concat(Object.values(logos));
|
||||
}, [logos]); // Only depend on logos object
|
||||
|
||||
const renderLogoOption = ({ option, checked }) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import useLogosStore from '../store/logos';
|
||||
|
||||
/**
|
||||
|
|
@ -46,16 +46,14 @@ export const useChannelLogoSelection = () => {
|
|||
|
||||
const channelLogos = useLogosStore((s) => s.channelLogos);
|
||||
const hasLoadedChannelLogos = useLogosStore((s) => s.hasLoadedChannelLogos);
|
||||
const backgroundLoading = useLogosStore((s) => s.backgroundLoading); // Use global loading state
|
||||
const backgroundLoading = useLogosStore((s) => s.backgroundLoading);
|
||||
const fetchChannelAssignableLogos = useLogosStore(
|
||||
(s) => s.fetchChannelAssignableLogos
|
||||
);
|
||||
|
||||
// Check if we have channel-assignable logos loaded
|
||||
const hasLogos = Object.keys(channelLogos).length > 0;
|
||||
|
||||
const ensureLogosLoaded = useCallback(async () => {
|
||||
// Use global loading state instead of local state
|
||||
if (backgroundLoading || (hasLoadedChannelLogos && isInitialized)) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -67,20 +65,15 @@ export const useChannelLogoSelection = () => {
|
|||
console.error('Failed to load channel-assignable logos:', error);
|
||||
}
|
||||
}, [
|
||||
backgroundLoading, // Use global loading state
|
||||
backgroundLoading,
|
||||
hasLoadedChannelLogos,
|
||||
isInitialized,
|
||||
fetchChannelAssignableLogos,
|
||||
]);
|
||||
|
||||
// Auto-load logos when hook is first used
|
||||
useEffect(() => {
|
||||
ensureLogosLoaded();
|
||||
}, [ensureLogosLoaded]);
|
||||
|
||||
return {
|
||||
logos: channelLogos, // Return channelLogos instead of all logos
|
||||
isLoading: backgroundLoading, // Use global loading state
|
||||
logos: channelLogos,
|
||||
isLoading: backgroundLoading,
|
||||
ensureLogosLoaded,
|
||||
hasLogos,
|
||||
};
|
||||
|
|
@ -91,22 +84,40 @@ export const useChannelLogoSelection = () => {
|
|||
*/
|
||||
export const useLogosById = (logoIds = []) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loadedIds, setLoadedIds] = useState(new Set());
|
||||
|
||||
const logos = useLogosStore((s) => s.logos);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds); // Find missing logos
|
||||
const missingIds = logoIds.filter((id) => id && !logos[id]);
|
||||
const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds);
|
||||
|
||||
// Memoize missing IDs calculation to prevent infinite loops
|
||||
const missingIds = useMemo(() => {
|
||||
return logoIds.filter((id) => id && !logos[id] && !loadedIds.has(id));
|
||||
}, [logoIds, logos, loadedIds]);
|
||||
|
||||
// Stringify logoIds to prevent array reference issues
|
||||
const logoIdsString = logoIds.join(',');
|
||||
|
||||
useEffect(() => {
|
||||
if (missingIds.length > 0 && !isLoading) {
|
||||
setIsLoading(true);
|
||||
|
||||
// Track that we're loading these IDs to prevent re-requests
|
||||
setLoadedIds((prev) => new Set([...prev, ...missingIds]));
|
||||
|
||||
fetchLogosByIds(missingIds)
|
||||
.then(() => setIsLoading(false))
|
||||
.catch((error) => {
|
||||
console.error('Failed to load logos by IDs:', error);
|
||||
// Remove failed IDs from loaded set so they can be retried
|
||||
setLoadedIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
missingIds.forEach((id) => newSet.delete(id));
|
||||
return newSet;
|
||||
});
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [missingIds, isLoading, fetchLogosByIds]);
|
||||
}, [logoIdsString, missingIds, isLoading, fetchLogosByIds]);
|
||||
|
||||
return {
|
||||
logos,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue