From 8ca91bd897b2bf7429f757226dcb85c2266cf7ca Mon Sep 17 00:00:00 2001 From: dekzter Date: Wed, 19 Mar 2025 07:15:31 -0400 Subject: [PATCH 1/8] fixed match epg, centralized to api --- frontend/src/api.js | 13 ++++++++++++ .../src/components/tables/ChannelsTable.jsx | 20 ++++++------------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/frontend/src/api.js b/frontend/src/api.js index 8a3cd97e..60403bd0 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -856,4 +856,17 @@ export default class API { const retval = await response.json(); return retval; } + + static async matchEpg() { + const response = await fetch(`${host}/api/channels/channels/match-epg/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await API.getAuthToken()}`, + }, + }); + + const retval = await response.json(); + return retval; + } } diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index c1e2889e..e570463f 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -376,22 +376,14 @@ const ChannelsTable = ({}) => { 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()}`, - }, - }); + await API.matchEpg(); - if (resp.ok) { - showAlert('EPG matching task started!'); - } else { - const text = await resp.text(); - showAlert(`Failed to start EPG matching: ${text}`); - } + notifications.show({ + title: 'EPG matching task started!', + // style: { width: '200px', left: '200px' }, + }); } catch (err) { - showAlert(`Error: ${err.message}`); + notifications.show(`Error: ${err.message}`); } }; From d7927052c79e19edd0678efa5476225ec7ca078a Mon Sep 17 00:00:00 2001 From: dekzter Date: Wed, 19 Mar 2025 07:15:51 -0400 Subject: [PATCH 2/8] quick style fix for header cell background --- frontend/src/components/tables/StreamsTable.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index 337ea9da..6030ab63 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -107,7 +107,7 @@ const StreamsTable = ({}) => { header: 'Name', accessorKey: 'name', mantineTableHeadCellProps: { - style: { textAlign: 'center' }, // Center-align the header + style: { textAlign: 'center', backgroundColor: 'rgb(56, 58, 63)' }, // Center-align the header }, Header: ({ column }) => ( Date: Wed, 19 Mar 2025 07:16:12 -0400 Subject: [PATCH 3/8] default null for stream profiles --- frontend/src/components/forms/Channel.jsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index 89b6ac86..e59c0c2b 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -60,7 +60,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => { name: '', channel_number: '', channel_group_id: '', - stream_profile_id: '0', + stream_profile_id: null, tvg_id: '', tvg_name: '', }, @@ -104,7 +104,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => { name: channel.name, channel_number: channel.channel_number, channel_group_id: channel.channel_group?.id, - stream_profile_id: channel.stream_profile_id || '0', + stream_profile_id: channel.stream_profile_id, tvg_id: channel.tvg_id, tvg_name: channel.tvg_name, }); @@ -307,10 +307,12 @@ const Channel = ({ channel = null, isOpen, onClose }) => { ? formik.touched.stream_profile_id : '' } - data={streamProfiles.map((option) => ({ - value: `${option.id}`, - label: option.name, - }))} + data={[{ value: null, label: '(use default)' }].concat( + streamProfiles.map((option) => ({ + value: `${option.id}`, + label: option.name, + })) + )} /> Date: Wed, 19 Mar 2025 07:17:07 -0400 Subject: [PATCH 4/8] fixed video URL --- frontend/src/pages/Guide.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index 412aa45c..67dad742 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -167,7 +167,7 @@ export default function TVChannelGuide({ startDate, endDate }) { return; } // Build a playable stream URL for that channel - let vidUrl = `/proxy/ts/stream/${matched.uui}`; + let vidUrl = `/proxy/ts/stream/${matched.uuid}`; if (env_mode == 'dev') { vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; } From 86a0e5b7418edd6aa262c473813d97dda2c90e37 Mon Sep 17 00:00:00 2001 From: dekzter Date: Wed, 19 Mar 2025 07:17:22 -0400 Subject: [PATCH 5/8] Attempting to fix refresh --- frontend/src/store/auth.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx index 23c8596c..b1ff0c33 100644 --- a/frontend/src/store/auth.jsx +++ b/frontend/src/store/auth.jsx @@ -46,7 +46,7 @@ const useAuthStore = create((set, get) => ({ const tokenExpiration = localStorage.getItem('tokenExpiration'); let accessToken = null; if (isTokenExpired(tokenExpiration)) { - accessToken = await get().refreshToken(); + accessToken = await get().getRefreshToken(); } else { accessToken = localStorage.getItem('accessToken'); } @@ -77,7 +77,7 @@ const useAuthStore = create((set, get) => ({ }, // Action to refresh the token - refreshToken: async () => { + getRefreshToken: async () => { const refreshToken = localStorage.getItem('refreshToken'); if (!refreshToken) return; @@ -92,7 +92,7 @@ const useAuthStore = create((set, get) => ({ localStorage.setItem('accessToken', data.access); localStorage.setItem('tokenExpiration', decodeToken(data.access)); - return true; + return data.access; } } catch (error) { console.error('Token refresh failed:', error); From 0f84a38dd5df437ebf47da0305797a1bfd97872b Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Wed, 19 Mar 2025 09:25:49 -0500 Subject: [PATCH 6/8] Cleaned up gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 82dbe4dc..b6631ac0 100755 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ .DS_Store -__pycache__/ **/__pycache__/ **/.vscode/ *.pyc @@ -9,8 +8,6 @@ staticfiles/ docker/DockerfileAIO docker/Dockerfile DEV static/ -docker/DockerfileAIO -docker/Dockerfile DEV data/ .next -next-env.d.ts +next-env.d.ts \ No newline at end of file From 061ca4d46e1e9e77780ea129c00481604625abb5 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Wed, 19 Mar 2025 09:56:32 -0500 Subject: [PATCH 7/8] Check for HLS switch to FFmpeg if detected. --- apps/proxy/ts_proxy/stream_manager.py | 48 ++++++++++++++++++++------- apps/proxy/ts_proxy/utils.py | 37 +++++++++++++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/apps/proxy/ts_proxy/stream_manager.py b/apps/proxy/ts_proxy/stream_manager.py index 48d2b8a5..817ee915 100644 --- a/apps/proxy/ts_proxy/stream_manager.py +++ b/apps/proxy/ts_proxy/stream_manager.py @@ -12,6 +12,7 @@ from apps.channels.models import Channel, Stream from apps.m3u.models import M3UAccount, M3UAccountProfile from core.models import UserAgent, CoreSettings from .stream_buffer import StreamBuffer +from .utils import detect_stream_type logger = logging.getLogger("ts_proxy") @@ -81,6 +82,16 @@ class StreamManager: self.stop_requested = False try: + # Check stream type before connecting + stream_type = detect_stream_type(self.url) + if stream_type == 'hls': + logger.info(f"Detected HLS stream: {self.url}") + logger.info(f"HLS streams will be handled with FFmpeg for now - future version will support HLS natively") + # Enable transcoding for HLS streams + self.transcode = True + # We'll override the stream profile selection with ffmpeg in the transcoding section + self.force_ffmpeg = True + # Start health monitor thread health_thread = threading.Thread(target=self._monitor_health, daemon=True) health_thread.start() @@ -96,7 +107,20 @@ class StreamManager: # Generate transcode command logger.debug(f"Building transcode command for channel {self.channel_id}") channel = get_object_or_404(Channel, uuid=self.channel_id) - stream_profile = channel.get_stream_profile() + + # Use FFmpeg specifically for HLS streams + if hasattr(self, 'force_ffmpeg') and self.force_ffmpeg: + from core.models import StreamProfile + try: + stream_profile = StreamProfile.objects.get(name='ffmpeg', locked=True) + logger.info("Using FFmpeg stream profile for HLS content") + except StreamProfile.DoesNotExist: + # Fall back to channel's profile if FFmpeg not found + stream_profile = channel.get_stream_profile() + logger.warning("FFmpeg profile not found, using channel default profile") + else: + stream_profile = channel.get_stream_profile() + self.transcode_cmd = stream_profile.build_command(self.url, self.user_agent) # Start command process for transcoding logger.debug(f"Starting transcode process: {self.transcode_cmd}") @@ -225,10 +249,10 @@ class StreamManager: break timeout = min(2 ** self.retry_count, 30) - + # When a connection fails and reconnect is needed: - self.reconnecting = True - + self.reconnecting = True + # Cancel all existing buffer timers during reconnect for timer in list(self._buffer_check_timers): try: @@ -237,10 +261,10 @@ class StreamManager: except Exception as e: logger.error(f"Error canceling buffer timer: {e}") self._buffer_check_timers = [] - + logger.info(f"Reconnecting in {timeout} seconds... (attempt {self.retry_count})") time.sleep(timeout) - + self.reconnecting = False # Reset flag after sleep except Exception as e: @@ -272,7 +296,7 @@ class StreamManager: """Stop the stream manager and cancel all timers""" # Add at the beginning of your stop method self.stopping = True - + # Cancel all buffer check timers for timer in list(self._buffer_check_timers): try: @@ -280,9 +304,9 @@ class StreamManager: timer.cancel() except Exception as e: logger.error(f"Error canceling buffer check timer: {e}") - + self._buffer_check_timers.clear() - + # Rest of your existing stop method... # Set the flag first self.stop_requested = True @@ -525,10 +549,10 @@ class StreamManager: if getattr(self, 'stopping', False) or getattr(self, 'reconnecting', False): logger.debug(f"Buffer check aborted - channel {self.buffer.channel_id} is stopping or reconnecting") return - + # Clean up completed timers self._buffer_check_timers = [t for t in self._buffer_check_timers if t.is_alive()] - + if hasattr(self.buffer, 'index') and hasattr(self.buffer, 'channel_id'): current_buffer_index = self.buffer.index initial_chunks_needed = getattr(Config, 'INITIAL_BEHIND_CHUNKS', 10) @@ -541,7 +565,7 @@ class StreamManager: else: # Still waiting, log progress and schedule another check logger.debug(f"Buffer filling for channel {channel_id}: {current_buffer_index}/{initial_chunks_needed} chunks") - + # Schedule another check - NOW WITH TRACKING if not getattr(self, 'stopping', False): timer = threading.Timer(0.5, self._check_buffer_and_set_state) diff --git a/apps/proxy/ts_proxy/utils.py b/apps/proxy/ts_proxy/utils.py index e69de29b..46240aac 100644 --- a/apps/proxy/ts_proxy/utils.py +++ b/apps/proxy/ts_proxy/utils.py @@ -0,0 +1,37 @@ +import logging +import re +from urllib.parse import urlparse + +logger = logging.getLogger("ts_proxy") + +def detect_stream_type(url): + """ + Detect if stream URL is HLS or TS format. + + Args: + url (str): The stream URL to analyze + + Returns: + str: 'hls' or 'ts' depending on detected format + """ + if not url: + return 'unknown' + + url_lower = url.lower() + + # Look for common HLS indicators + if (url_lower.endswith('.m3u8') or + '.m3u8?' in url_lower or + '/playlist.m3u' in url_lower): + return 'hls' + + # Additional HLS patterns + parsed = urlparse(url) + path = parsed.path.lower() + if ('playlist' in path and ('.m3u' in path or '.m3u8' in path)) or \ + ('manifest' in path and ('.m3u' in path or '.m3u8' in path)) or \ + ('master' in path and ('.m3u' in path or '.m3u8' in path)): + return 'hls' + + # Default to TS + return 'ts' \ No newline at end of file From d878eda1a73d48cd314bc34b47eabc33e396fd90 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Wed, 19 Mar 2025 10:54:09 -0500 Subject: [PATCH 8/8] Only switch to default ffmpeg if proxy is selected. --- apps/proxy/ts_proxy/stream_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/proxy/ts_proxy/stream_manager.py b/apps/proxy/ts_proxy/stream_manager.py index 817ee915..ae9c104e 100644 --- a/apps/proxy/ts_proxy/stream_manager.py +++ b/apps/proxy/ts_proxy/stream_manager.py @@ -84,7 +84,7 @@ class StreamManager: try: # Check stream type before connecting stream_type = detect_stream_type(self.url) - if stream_type == 'hls': + if self.transcode == False and stream_type == 'hls': logger.info(f"Detected HLS stream: {self.url}") logger.info(f"HLS streams will be handled with FFmpeg for now - future version will support HLS natively") # Enable transcoding for HLS streams