diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index ea55e3e6..75772509 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -9,6 +9,8 @@ from django.shortcuts import get_object_or_404
from .models import Stream, Channel, ChannelGroup
from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer
+from .tasks import match_epg_channels
+
# ─────────────────────────────────────────────────────────
# 1) Stream API (CRUD)
@@ -30,6 +32,7 @@ class StreamViewSet(viewsets.ModelViewSet):
qs = qs.filter(channels__isnull=True)
return qs
+
# ─────────────────────────────────────────────────────────
# 2) Channel Group Management (CRUD)
# ─────────────────────────────────────────────────────────
@@ -38,6 +41,7 @@ class ChannelGroupViewSet(viewsets.ModelViewSet):
serializer_class = ChannelGroupSerializer
permission_classes = [IsAuthenticated]
+
# ─────────────────────────────────────────────────────────
# 3) Channel Management (CRUD)
# ─────────────────────────────────────────────────────────
@@ -178,6 +182,7 @@ class ChannelViewSet(viewsets.ModelViewSet):
# Gather current used numbers once.
used_numbers = set(Channel.objects.all().values_list('channel_number', flat=True))
next_number = 1
+
def get_auto_number():
nonlocal next_number
while next_number in used_numbers:
@@ -236,6 +241,20 @@ class ChannelViewSet(viewsets.ModelViewSet):
return Response(response_data, status=status.HTTP_201_CREATED)
+ # ─────────────────────────────────────────────────────────
+ # 6) EPG Fuzzy Matching
+ # ─────────────────────────────────────────────────────────
+ @swagger_auto_schema(
+ method='post',
+ operation_description="Kick off a Celery task that tries to fuzzy-match channels with EPG data.",
+ responses={202: "EPG matching task initiated"}
+ )
+ @action(detail=False, methods=['post'], url_path='match-epg')
+ def match_epg(self, request):
+ match_epg_channels.delay()
+ return Response({"message": "EPG matching task initiated."}, status=status.HTTP_202_ACCEPTED)
+
+
# ─────────────────────────────────────────────────────────
# 4) Bulk Delete Streams
# ─────────────────────────────────────────────────────────
@@ -262,6 +281,7 @@ class BulkDeleteStreamsAPIView(APIView):
Stream.objects.filter(id__in=stream_ids).delete()
return Response({"message": "Streams deleted successfully!"}, status=status.HTTP_204_NO_CONTENT)
+
# ─────────────────────────────────────────────────────────
# 5) Bulk Delete Channels
# ─────────────────────────────────────────────────────────
diff --git a/apps/channels/tasks.py b/apps/channels/tasks.py
new file mode 100644
index 00000000..c4bf8177
--- /dev/null
+++ b/apps/channels/tasks.py
@@ -0,0 +1,207 @@
+# apps/channels/tasks.py
+
+import logging
+import re
+
+from celery import shared_task
+from rapidfuzz import fuzz
+from sentence_transformers import SentenceTransformer, util
+from django.db import transaction
+
+from apps.channels.models import Channel
+from apps.epg.models import EPGData
+from core.models import CoreSettings # to retrieve "preferred-region" setting
+
+logger = logging.getLogger(__name__)
+
+# Load the model once at module level
+SENTENCE_MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
+st_model = SentenceTransformer(SENTENCE_MODEL_NAME)
+
+# Threshold constants
+BEST_FUZZY_THRESHOLD = 70
+LOWER_FUZZY_THRESHOLD = 40
+EMBED_SIM_THRESHOLD = 0.65
+
+# Common extraneous words
+COMMON_EXTRANEOUS_WORDS = [
+ "tv", "channel", "network", "television",
+ "east", "west", "hd", "uhd", "us", "usa", "not", "24/7",
+ "1080p", "720p", "540p", "480p",
+ "arabic", "latino", "film", "movie", "movies"
+]
+
+def normalize_channel_name(name: str) -> str:
+ """
+ A more aggressive normalization that:
+ - Lowercases
+ - Removes bracketed/parenthesized text
+ - Removes punctuation
+ - Strips extraneous words
+ - Collapses extra spaces
+ """
+ if not name:
+ return ""
+
+ # Lowercase
+ norm = name.lower()
+
+ # Remove bracketed text
+ norm = re.sub(r"\[.*?\]", "", norm)
+ norm = re.sub(r"\(.*?\)", "", norm)
+
+ # Remove punctuation except word chars/spaces
+ norm = re.sub(r"[^\w\s]", "", norm)
+
+ # Remove extraneous tokens
+ tokens = norm.split()
+ tokens = [t for t in tokens if t not in COMMON_EXTRANEOUS_WORDS]
+
+ # Rejoin
+ norm = " ".join(tokens).strip()
+ return norm
+
+@shared_task
+def match_epg_channels():
+ """
+ Goes through all Channels and tries to find a matching EPGData row by:
+ 1) If channel.tvg_id is valid in EPGData, skip
+ 2) If channel has a tvg_id but not found in EPGData, attempt direct EPGData lookup
+ 3) Otherwise do name-based fuzzy ratio pass:
+ - add region-based bonus if region code is found in the EPG row
+ - if fuzzy >= BEST_FUZZY_THRESHOLD => accept
+ - if fuzzy in [LOWER_FUZZY_THRESHOLD..BEST_FUZZY_THRESHOLD) => do embedding check
+ - else skip
+ 4) Log summary
+ """
+ logger.info("Starting EPG matching logic...")
+
+ # Try to get user's preferred region from CoreSettings
+ try:
+ region_obj = CoreSettings.objects.get(key="preferred-region")
+ region_code = region_obj.value.strip().lower() # e.g. "us"
+ except CoreSettings.DoesNotExist:
+ region_code = None
+
+ # 1) Gather EPG rows
+ all_epg = list(EPGData.objects.all())
+ epg_rows = []
+ for e in all_epg:
+ epg_rows.append({
+ "epg_id": e.id,
+ "tvg_id": e.tvg_id or "", # e.g. "Fox News.us"
+ "raw_name": e.channel_name,
+ "norm_name": normalize_channel_name(e.channel_name),
+ })
+
+ # 2) Pre-encode embeddings if possible
+ epg_embeddings = None
+ if any(row["norm_name"] for row in epg_rows):
+ epg_embeddings = st_model.encode(
+ [row["norm_name"] for row in epg_rows],
+ convert_to_tensor=True
+ )
+
+ matched_channels = []
+
+ with transaction.atomic():
+ for chan in Channel.objects.all():
+ # A) Skip if channel.tvg_id is valid
+ if chan.tvg_id and EPGData.objects.filter(tvg_id=chan.tvg_id).exists():
+ continue
+
+ # B) If channel has a tvg_id but not in EPG, do direct lookup
+ if chan.tvg_id:
+ epg_match = EPGData.objects.filter(tvg_id=chan.tvg_id).first()
+ if epg_match:
+ logger.info(
+ f"Channel {chan.id} '{chan.channel_name}' => found EPG by tvg_id={chan.tvg_id}"
+ )
+ continue
+
+ # C) No valid tvg_id => name-based matching
+ fallback_name = chan.tvg_name.strip() if chan.tvg_name else chan.channel_name
+ norm_chan = normalize_channel_name(fallback_name)
+ if not norm_chan:
+ logger.info(
+ f"Channel {chan.id} '{chan.channel_name}' => empty after normalization, skipping"
+ )
+ continue
+
+ best_score = 0
+ best_epg = None
+
+ for row in epg_rows:
+ if not row["norm_name"]:
+ continue
+ # Base fuzzy ratio
+ base_score = fuzz.ratio(norm_chan, row["norm_name"])
+
+ # If we have a region_code, add a small bonus if the epg row has that region
+ # e.g. tvg_id or raw_name might contain ".us" or "us"
+ bonus = 0
+ if region_code:
+ # example: if region_code is "us" and row["tvg_id"] ends with ".us"
+ # or row["raw_name"] has "us" in it, etc.
+ # We'll do a naive check:
+ combined_text = row["tvg_id"].lower() + " " + row["raw_name"].lower()
+ if region_code in combined_text:
+ bonus = 15 # pick a small bonus
+
+ score = base_score + bonus
+
+ if score > best_score:
+ best_score = score
+ best_epg = row
+
+ if not best_epg:
+ logger.info(f"Channel {chan.id} '{fallback_name}' => no EPG match at all.")
+ continue
+
+ # E) Decide acceptance
+ if best_score >= BEST_FUZZY_THRESHOLD:
+ # Accept
+ chan.tvg_id = best_epg["tvg_id"]
+ chan.save()
+ matched_channels.append((chan.id, fallback_name, best_epg["tvg_id"]))
+ logger.info(
+ f"Channel {chan.id} '{fallback_name}' => matched tvg_id={best_epg['tvg_id']} (score={best_score})"
+ )
+ elif best_score >= LOWER_FUZZY_THRESHOLD and epg_embeddings is not None:
+ # borderline => do embedding
+ chan_embedding = st_model.encode(norm_chan, convert_to_tensor=True)
+ sim_scores = util.cos_sim(chan_embedding, epg_embeddings)[0]
+ top_index = int(sim_scores.argmax())
+ top_value = float(sim_scores[top_index])
+
+ if top_value >= EMBED_SIM_THRESHOLD:
+ matched_epg = epg_rows[top_index]
+ chan.tvg_id = matched_epg["tvg_id"]
+ chan.save()
+ matched_channels.append((chan.id, fallback_name, matched_epg["tvg_id"]))
+ logger.info(
+ f"Channel {chan.id} '{fallback_name}' => matched EPG tvg_id={matched_epg['tvg_id']} "
+ f"(fuzzy={best_score}, cos-sim={top_value:.2f})"
+ )
+ else:
+ logger.info(
+ f"Channel {chan.id} '{fallback_name}' => fuzzy={best_score}, "
+ f"cos-sim={top_value:.2f} < {EMBED_SIM_THRESHOLD}, skipping"
+ )
+ else:
+ # no match
+ logger.info(
+ f"Channel {chan.id} '{fallback_name}' => fuzzy={best_score} < {LOWER_FUZZY_THRESHOLD}, skipping"
+ )
+
+ # Final summary
+ total_matched = len(matched_channels)
+ if total_matched:
+ logger.info(f"Match Summary: {total_matched} channel(s) matched.")
+ for (cid, cname, tvg) in matched_channels:
+ logger.info(f" - Channel ID={cid}, Name='{cname}' => tvg_id='{tvg}'")
+ else:
+ logger.info("No new channels were matched.")
+
+ logger.info("Finished EPG matching logic.")
+ return f"Done. Matched {total_matched} channel(s)."
diff --git a/frontend/src/api.js b/frontend/src/api.js
index f840d1ab..0ed976f0 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -1,3 +1,4 @@
+// src/api.js (updated)
import useAuthStore from './store/auth';
import useChannelsStore from './store/channels';
import useUserAgentsStore from './store/userAgents';
@@ -7,18 +8,17 @@ import useStreamsStore from './store/streams';
import useStreamProfilesStore from './store/streamProfiles';
import useSettingsStore from './store/settings';
-// const axios = Axios.create({
-// withCredentials: true,
-// });
-
+// If needed, you can set a base host or keep it empty if relative requests
const host = '';
-export const getAuthToken = async () => {
- const token = await useAuthStore.getState().getToken(); // Assuming token is stored in Zustand store
- return token;
-};
-
export default class API {
+ /**
+ * A static method so we can do: await API.getAuthToken()
+ */
+ static async getAuthToken() {
+ return await useAuthStore.getState().getToken();
+ }
+
static async login(username, password) {
const response = await fetch(`${host}/api/accounts/token/`, {
method: 'POST',
@@ -31,11 +31,11 @@ export default class API {
return await response.json();
}
- static async refreshToken(refreshToken) {
+ static async refreshToken(refresh) {
const response = await fetch(`${host}/api/accounts/token/refresh/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ refresh: refreshToken }),
+ body: JSON.stringify({ refresh }),
});
const retval = await response.json();
@@ -54,7 +54,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/`, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
@@ -66,7 +66,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/groups/`, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
@@ -78,7 +78,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/groups/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -97,7 +97,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/groups/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -114,6 +114,7 @@ export default class API {
static async addChannel(channel) {
let body = null;
if (channel.logo_file) {
+ // Must send FormData for file upload
body = new FormData();
for (const prop in channel) {
body.append(prop, channel[prop]);
@@ -127,7 +128,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
...(channel.logo_file
? {}
: {
@@ -149,7 +150,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -162,7 +163,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/bulk-delete/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ channel_ids }),
@@ -176,7 +177,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -195,26 +196,22 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/assign/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ channel_order: channelIds }),
});
- // The backend returns something like { "message": "Channels have been auto-assigned!" }
if (!response.ok) {
- // If you want to handle errors gracefully:
const text = await response.text();
throw new Error(`Assign channels failed: ${response.status} => ${text}`);
}
- // Usually it has a { message: "..."} or similar
const retval = await response.json();
- // If you want to automatically refresh the channel list in Zustand:
+ // Optionally refresh the channel list in Zustand
await useChannelsStore.getState().fetchChannels();
- // Return the entire JSON result (so the caller can see the "message")
return retval;
}
@@ -222,7 +219,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/channels/from-stream/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -242,7 +239,7 @@ export default class API {
{
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -261,7 +258,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/streams/`, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
@@ -273,7 +270,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/streams/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -292,7 +289,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/streams/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -310,7 +307,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/streams/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -322,7 +319,7 @@ export default class API {
const response = await fetch(`${host}/api/channels/streams/bulk-delete/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ stream_ids: ids }),
@@ -335,7 +332,7 @@ export default class API {
const response = await fetch(`${host}/api/core/useragents/`, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
@@ -347,7 +344,7 @@ export default class API {
const response = await fetch(`${host}/api/core/useragents/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -366,7 +363,7 @@ export default class API {
const response = await fetch(`${host}/api/core/useragents/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -384,7 +381,7 @@ export default class API {
const response = await fetch(`${host}/api/core/useragents/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -395,7 +392,7 @@ export default class API {
static async getPlaylist(id) {
const response = await fetch(`${host}/api/m3u/accounts/${id}/`, {
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -407,7 +404,7 @@ export default class API {
static async getPlaylists() {
const response = await fetch(`${host}/api/m3u/accounts/`, {
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -420,7 +417,7 @@ export default class API {
const response = await fetch(`${host}/api/m3u/accounts/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -438,7 +435,7 @@ export default class API {
const response = await fetch(`${host}/api/m3u/refresh/${id}/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -451,7 +448,7 @@ export default class API {
const response = await fetch(`${host}/api/m3u/refresh/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -464,7 +461,7 @@ export default class API {
const response = await fetch(`${host}/api/m3u/accounts/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -477,7 +474,7 @@ export default class API {
const response = await fetch(`${host}/api/m3u/accounts/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -494,7 +491,7 @@ export default class API {
static async getEPGs() {
const response = await fetch(`${host}/api/epg/sources/`, {
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -503,18 +500,8 @@ export default class API {
return retval;
}
- static async refreshPlaylist(id) {
- const response = await fetch(`${host}/api/m3u/refresh/${id}/`, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
- 'Content-Type': 'application/json',
- },
- });
-
- const retval = await response.json();
- return retval;
- }
+ // Notice there's a duplicated "refreshPlaylist" method above;
+ // you might want to rename or remove one if it's not needed.
static async addEPG(values) {
let body = null;
@@ -532,7 +519,7 @@ export default class API {
const response = await fetch(`${host}/api/epg/sources/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
...(values.epg_file
? {}
: {
@@ -554,7 +541,7 @@ export default class API {
const response = await fetch(`${host}/api/epg/sources/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -566,7 +553,7 @@ export default class API {
const response = await fetch(`${host}/api/epg/import/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ id }),
@@ -579,7 +566,7 @@ export default class API {
static async getStreamProfiles() {
const response = await fetch(`${host}/api/core/streamprofiles/`, {
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -592,7 +579,7 @@ export default class API {
const response = await fetch(`${host}/api/core/streamprofiles/`, {
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -610,7 +597,7 @@ export default class API {
const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -628,7 +615,7 @@ export default class API {
const response = await fetch(`${host}/api/core/streamprofiles/${id}/`, {
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -639,7 +626,7 @@ export default class API {
static async getGrid() {
const response = await fetch(`${host}/api/epg/grid/`, {
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
});
@@ -654,7 +641,7 @@ export default class API {
{
method: 'POST',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(values),
@@ -663,7 +650,7 @@ export default class API {
const retval = await response.json();
if (retval.id) {
- // Fetch m3u account to update it with its new playlists
+ // Refresh the playlist
const playlist = await API.getPlaylist(accountId);
usePlaylistsStore
.getState()
@@ -679,7 +666,7 @@ export default class API {
{
method: 'DELETE',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
}
@@ -696,7 +683,7 @@ export default class API {
{
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
@@ -711,7 +698,7 @@ export default class API {
const response = await fetch(`${host}/api/core/settings/`, {
headers: {
'Content-Type': 'application/json',
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
},
});
@@ -724,7 +711,7 @@ export default class API {
const response = await fetch(`${host}/api/core/settings/${id}/`, {
method: 'PUT',
headers: {
- Authorization: `Bearer ${await getAuthToken()}`,
+ Authorization: `Bearer ${await API.getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js
index e271511c..b09d9566 100644
--- a/frontend/src/components/tables/ChannelsTable.js
+++ b/frontend/src/components/tables/ChannelsTable.js
@@ -24,13 +24,14 @@ import {
SwapVert as SwapVertIcon,
LiveTv as LiveTvIcon,
ContentCopy,
+ Tv as TvIcon, // <-- ADD THIS IMPORT
} from '@mui/icons-material';
import API from '../../api';
import ChannelForm from '../forms/Channel';
import { TableHelper } from '../../helpers';
import utils from '../../utils';
import logo from '../../images/logo.png';
-import useVideoStore from '../../store/useVideoStore'; // NEW import
+import useVideoStore from '../../store/useVideoStore';
const ChannelsTable = () => {
const [channel, setChannel] = useState(null);
@@ -116,6 +117,7 @@ const ChannelsTable = () => {
4,
selected.map((chan) => () => deleteChannel(chan.original.id))
);
+ // If you have a real bulk-delete endpoint, call it here:
// await API.deleteChannels(selected.map((sel) => sel.id));
setIsLoading(false);
};
@@ -144,6 +146,32 @@ const ChannelsTable = () => {
}
};
+ // ─────────────────────────────────────────────────────────
+ // The new "Match EPG" button logic
+ // ─────────────────────────────────────────────────────────
+ const matchEpg = async () => {
+ try {
+ // Hit our new endpoint that triggers the fuzzy matching Celery task
+ const resp = await fetch('/api/channels/channels/match-epg/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${await API.getAuthToken()}`,
+ },
+ });
+
+ if (resp.ok) {
+ setSnackbarMessage('EPG matching task started!');
+ } else {
+ const text = await resp.text();
+ setSnackbarMessage(`Failed to start EPG matching: ${text}`);
+ }
+ } catch (err) {
+ setSnackbarMessage(`Error: ${err.message}`);
+ }
+ setSnackbarOpen(true);
+ };
+
const closeChannelForm = () => {
setChannel(null);
setChannelModalOpen(false);
@@ -294,6 +322,18 @@ const ChannelsTable = () => {
+ {/* Our brand-new button for EPG matching */}
+
+
+
+
+
+