diff --git a/apps/epg/api_views.py b/apps/epg/api_views.py index 547be152..58ab0334 100644 --- a/apps/epg/api_views.py +++ b/apps/epg/api_views.py @@ -53,7 +53,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet): try: return [perm() for perm in permission_classes_by_action[self.action]] except KeyError: - if self.action in ('sd_lineups', 'sd_lineups_search'): + if self.action in ('sd_lineups', 'sd_lineups_search', 'sd_countries'): if self.request.method == 'GET': return [IsStandardUser()] return [IsAdmin()] @@ -390,6 +390,31 @@ class EPGSourceViewSet(viewsets.ModelViewSet): status=status.HTTP_502_BAD_GATEWAY ) + @action(detail=True, methods=["get"], url_path="sd-countries") + def sd_countries(self, request, pk=None): + """Proxy /available/countries from the SD API to avoid browser CORS restrictions.""" + import requests as http_requests + from apps.epg.tasks import SD_BASE_URL + + source = self.get_object() + if source.source_type != 'schedules_direct': + return Response( + {"error": "This action is only available for Schedules Direct sources."}, + status=status.HTTP_400_BAD_REQUEST, + ) + try: + resp = http_requests.get( + f"{SD_BASE_URL}/available/countries", + timeout=15, + ) + resp.raise_for_status() + return Response(resp.json()) + except http_requests.exceptions.RequestException as e: + return Response( + {"error": f"Failed to fetch countries: {str(e)}"}, + status=status.HTTP_502_BAD_GATEWAY, + ) + @action(detail=True, methods=["post"], url_path="sd-lineups/search") def sd_lineups_search(self, request, pk=None): """ diff --git a/frontend/src/api.js b/frontend/src/api.js index 1c826465..445e7e82 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -3889,6 +3889,14 @@ export default class API { } } + static async getSDCountries(sourceId) { + try { + return await request(`${host}/api/epg/sources/${sourceId}/sd-countries/`); + } catch (e) { + return null; + } + } + static async getSDLineups(sourceId) { try { const response = await request( diff --git a/frontend/src/components/forms/EPG.jsx b/frontend/src/components/forms/EPG.jsx index f7e6ec92..ebfea46b 100644 --- a/frontend/src/components/forms/EPG.jsx +++ b/frontend/src/components/forms/EPG.jsx @@ -325,18 +325,16 @@ const SDLineupManager = ({ sourceId }) => { useEffect(() => { if (sourceId) { fetchActiveLineups(); - // Fetch country list from SD API per their recommendation to not hardcode - fetch('https://json.schedulesdirect.org/20141201/available/countries') - .then((r) => r.json()) - .then((data) => { - const all = Object.values(data).flat(); - const mapped = all - .filter((c) => c.shortName && c.fullName) - .map((c) => ({ value: c.shortName, label: c.fullName })) - .sort((a, b) => a.label.localeCompare(b.label)); - if (mapped.length > 0) setCountries(mapped); - }) - .catch(() => {}); // fallback list remains if fetch fails + // Fetch country list via backend proxy to avoid browser CORS restrictions + API.getSDCountries(sourceId).then((data) => { + if (!data) return; + const all = Object.values(data).flat(); + const mapped = all + .filter((c) => c.shortName && c.fullName) + .map((c) => ({ value: c.shortName, label: c.fullName })) + .sort((a, b) => a.label.localeCompare(b.label)); + if (mapped.length > 0) setCountries(mapped); + }); } }, [sourceId, fetchActiveLineups]); diff --git a/frontend/src/components/forms/__tests__/EPG.test.jsx b/frontend/src/components/forms/__tests__/EPG.test.jsx index 1e64a563..66ead589 100644 --- a/frontend/src/components/forms/__tests__/EPG.test.jsx +++ b/frontend/src/components/forms/__tests__/EPG.test.jsx @@ -8,6 +8,7 @@ global.fetch = vi.fn().mockResolvedValue({ vi.mock('../../../api.js', () => ({ default: { + getSDCountries: vi.fn().mockResolvedValue({}), getSDLineups: vi.fn().mockResolvedValue([]), addSDLineup: vi.fn().mockResolvedValue({ success: true }), deleteSDLineup: vi.fn().mockResolvedValue({ success: true }),