mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
refactor(EPGSourceViewSet, frontend): streamline SD countries fetching and remove proxy endpoint
- Introduced a new method `_fetch_sd_countries` in `EPGSourceViewSet` to fetch SD countries directly from the backend, eliminating the need for a separate proxy endpoint. - Updated the `sd_lineups` action to include the fetched countries in the response. - Removed the `getSDCountries` method from the API and adjusted the frontend to fetch countries alongside lineups, improving efficiency - Updated related components to handle the new data structure for countries.
This commit is contained in:
parent
887a177581
commit
ed364a26c9
4 changed files with 52 additions and 50 deletions
|
|
@ -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', 'sd_countries'):
|
||||
if self.action in ('sd_lineups', 'sd_lineups_search'):
|
||||
if self.request.method == 'GET':
|
||||
return [IsStandardUser()]
|
||||
return [IsAdmin()]
|
||||
|
|
@ -227,6 +227,24 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
f"Lockout set until {reset_at.isoformat()}."
|
||||
)
|
||||
|
||||
def _fetch_sd_countries(self):
|
||||
"""Fetch the SD country list (token not required; User-Agent is)."""
|
||||
import requests as http_requests
|
||||
from apps.epg.tasks import SD_BASE_URL
|
||||
from version import __version__ as dispatcharr_version
|
||||
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
f"{SD_BASE_URL}/available/countries",
|
||||
headers={'User-Agent': f'Dispatcharr/{dispatcharr_version}'},
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except http_requests.exceptions.RequestException as e:
|
||||
logger.warning(f"Failed to fetch SD countries: {e}")
|
||||
return None
|
||||
|
||||
@action(detail=True, methods=["get", "post", "delete"], url_path="sd-lineups")
|
||||
def sd_lineups(self, request, pk=None):
|
||||
"""
|
||||
|
|
@ -256,6 +274,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
}
|
||||
|
||||
if request.method == "GET":
|
||||
countries = self._fetch_sd_countries()
|
||||
try:
|
||||
resp = http_requests.get(
|
||||
f"{SD_BASE_URL}/lineups",
|
||||
|
|
@ -272,6 +291,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"changes_remaining": self._get_sd_changes_remaining(source),
|
||||
"changes_reset_at": self._get_sd_reset_at(source),
|
||||
"notice": "No lineups are currently configured on this Schedules Direct account. Use the search below to add one.",
|
||||
"countries": countries,
|
||||
})
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
|
@ -281,6 +301,7 @@ class EPGSourceViewSet(viewsets.ModelViewSet):
|
|||
"max_lineups": 4,
|
||||
"changes_remaining": self._get_sd_changes_remaining(source),
|
||||
"changes_reset_at": self._get_sd_reset_at(source),
|
||||
"countries": countries,
|
||||
})
|
||||
except http_requests.exceptions.RequestException as e:
|
||||
return Response(
|
||||
|
|
@ -390,31 +411,6 @@ 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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3889,14 +3889,6 @@ 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(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { showNotification } from '../../utils/notificationUtils.js';
|
|||
import API from '../../api.js';
|
||||
import useEPGsStore from '../../store/epgs';
|
||||
|
||||
// Countries are fetched dynamically from the SD API on component mount.
|
||||
// Countries are fetched with lineups from the backend on component mount.
|
||||
// Fallback list used if the API call fails.
|
||||
const SD_COUNTRIES_FALLBACK = [
|
||||
{ value: 'USA', label: 'United States' },
|
||||
|
|
@ -48,6 +48,16 @@ const SD_COUNTRIES_FALLBACK = [
|
|||
{ value: 'NZL', label: 'New Zealand' },
|
||||
];
|
||||
|
||||
const mapSDCountries = (data) => {
|
||||
if (!data) return null;
|
||||
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));
|
||||
return mapped.length > 0 ? mapped : null;
|
||||
};
|
||||
|
||||
// ESPN HD logo previews — packaged as static URLs so no API call is needed.
|
||||
// These are publicly accessible S3 URLs that don't require authentication.
|
||||
const SD_LOGO_PREVIEW_BASE =
|
||||
|
|
@ -309,6 +319,8 @@ const SDLineupManager = ({ sourceId }) => {
|
|||
if (data) {
|
||||
setActiveLineups(data.lineups || []);
|
||||
setLineupNotice(data.notice || null);
|
||||
const mappedCountries = mapSDCountries(data.countries);
|
||||
if (mappedCountries) setCountries(mappedCountries);
|
||||
// Always update changesRemaining from server — includes null (unknown) and 0 (locked)
|
||||
if (data.changes_remaining !== undefined) {
|
||||
setChangesRemaining(data.changes_remaining);
|
||||
|
|
@ -325,16 +337,6 @@ const SDLineupManager = ({ sourceId }) => {
|
|||
useEffect(() => {
|
||||
if (sourceId) {
|
||||
fetchActiveLineups();
|
||||
// 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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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 }),
|
||||
|
|
@ -216,12 +215,20 @@ vi.mock('@mantine/core', async () => ({
|
|||
{(data ?? []).map((opt) => {
|
||||
const val = typeof opt === 'string' ? opt : opt.value;
|
||||
const lbl = typeof opt === 'string' ? opt : opt.label;
|
||||
return <option key={val} value={val}>{lbl}</option>;
|
||||
return (
|
||||
<option key={val} value={val}>
|
||||
{lbl}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
),
|
||||
Loader: ({ size }) => <div data-testid="loader" data-size={size} />,
|
||||
Badge: ({ children, color }) => <span data-testid="badge" data-color={color}>{children}</span>,
|
||||
Badge: ({ children, color }) => (
|
||||
<span data-testid="badge" data-color={color}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
ScrollArea: ({ children }) => <div data-testid="scroll-area">{children}</div>,
|
||||
Table: ({ children }) => <table>{children}</table>,
|
||||
Tooltip: ({ children }) => <div>{children}</div>,
|
||||
|
|
@ -238,10 +245,15 @@ vi.mock('@mantine/core', async () => ({
|
|||
</label>
|
||||
),
|
||||
UnstyledButton: ({ children, onClick, ...props }) => (
|
||||
<button type="button" onClick={onClick} {...props}>{children}</button>
|
||||
<button type="button" onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
Alert: ({ children, title, color, icon }) => (
|
||||
<div data-testid="alert" data-color={color}><strong>{title}</strong>{children}</div>
|
||||
<div data-testid="alert" data-color={color}>
|
||||
<strong>{title}</strong>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
Stack: ({ children, gap }) => <div data-testid="stack">{children}</div>,
|
||||
Text: ({ children, ...props }) => <span>{children}</span>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue