diff --git a/apps/epg/tasks.py b/apps/epg/tasks.py index 74411bdb..cb985a51 100644 --- a/apps/epg/tasks.py +++ b/apps/epg/tasks.py @@ -13,6 +13,7 @@ from django.conf import settings from django.db import transaction from django.utils import timezone from apps.channels.models import Channel +from core.models import UserAgent, CoreSettings from asgiref.sync import async_to_sync from channels.layers import get_channel_layer @@ -67,7 +68,22 @@ def fetch_xmltv(source): logger.info(f"Fetching XMLTV data from source: {source.name}") try: - response = requests.get(source.url, timeout=30) + # Get default user agent from settings + default_user_agent_setting = CoreSettings.objects.filter(key='default-user-agent').first() + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0" # Fallback default + if default_user_agent_setting and default_user_agent_setting.value: + try: + user_agent_obj = UserAgent.objects.filter(id=int(default_user_agent_setting.value)).first() + if user_agent_obj and user_agent_obj.user_agent: + user_agent = user_agent_obj.user_agent + logger.debug(f"Using default user agent: {user_agent}") + except (ValueError, Exception) as e: + logger.warning(f"Error retrieving default user agent, using fallback: {e}") + headers = { + 'User-Agent': user_agent + } + + response = requests.get(source.url, headers=headers, timeout=30) response.raise_for_status() logger.debug("XMLTV data fetched successfully.") @@ -296,10 +312,24 @@ def parse_programs_for_source(epg_source, tvg_id=None): def fetch_schedules_direct(source): logger.info(f"Fetching Schedules Direct data from source: {source.name}") try: + # Get default user agent from settings + default_user_agent_setting = CoreSettings.objects.filter(key='default-user-agent').first() + user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0" # Fallback default + + if default_user_agent_setting and default_user_agent_setting.value: + try: + user_agent_obj = UserAgent.objects.filter(id=int(default_user_agent_setting.value)).first() + if user_agent_obj and user_agent_obj.user_agent: + user_agent = user_agent_obj.user_agent + logger.debug(f"Using default user agent: {user_agent}") + except (ValueError, Exception) as e: + logger.warning(f"Error retrieving default user agent, using fallback: {e}") + api_url = '' headers = { 'Content-Type': 'application/json', 'Authorization': f'Bearer {source.api_key}', + 'User-Agent': user_agent } logger.debug(f"Requesting subscriptions from Schedules Direct using URL: {api_url}") response = requests.get(api_url, headers=headers, timeout=30) diff --git a/apps/m3u/migrations/0008_m3uaccount_stale_stream_days.py b/apps/m3u/migrations/0008_m3uaccount_stale_stream_days.py new file mode 100644 index 00000000..69a1397d --- /dev/null +++ b/apps/m3u/migrations/0008_m3uaccount_stale_stream_days.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.6 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('m3u', '0007_remove_m3uaccount_uploaded_file_m3uaccount_file_path'), + ] + + operations = [ + migrations.AddField( + model_name='m3uaccount', + name='stale_stream_days', + field=models.PositiveIntegerField(default=7, help_text='Number of days after which a stream will be removed if not seen in the M3U source.'), + ), + ] diff --git a/apps/m3u/models.py b/apps/m3u/models.py index 99ead627..9901836b 100644 --- a/apps/m3u/models.py +++ b/apps/m3u/models.py @@ -81,6 +81,10 @@ class M3UAccount(models.Model): refresh_task = models.ForeignKey( PeriodicTask, on_delete=models.SET_NULL, null=True, blank=True ) + stale_stream_days = models.PositiveIntegerField( + default=7, + help_text="Number of days after which a stream will be removed if not seen in the M3U source." + ) def __str__(self): return self.name diff --git a/apps/m3u/serializers.py b/apps/m3u/serializers.py index dd9b0e7a..faafda2a 100644 --- a/apps/m3u/serializers.py +++ b/apps/m3u/serializers.py @@ -66,7 +66,7 @@ class M3UAccountSerializer(serializers.ModelSerializer): fields = [ 'id', 'name', 'server_url', 'file_path', 'server_group', 'max_streams', 'is_active', 'created_at', 'updated_at', 'filters', 'user_agent', 'profiles', 'locked', - 'channel_groups', 'refresh_interval', 'custom_properties', 'account_type', 'username', 'password' + 'channel_groups', 'refresh_interval', 'custom_properties', 'account_type', 'username', 'password', 'stale_stream_days', ] extra_kwargs = { 'password': { diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py index ce536234..caef9f13 100644 --- a/apps/m3u/tasks.py +++ b/apps/m3u/tasks.py @@ -36,10 +36,15 @@ def fetch_m3u_lines(account, use_cache=False): """Fetch M3U file lines efficiently.""" if account.server_url: if not use_cache or not os.path.exists(file_path): - user_agent = account.get_user_agent() - headers = {"User-Agent": user_agent.user_agent} - logger.info(f"Fetching from URL {account.server_url}") try: + # Try to get account-specific user agent first + user_agent_obj = account.get_user_agent() + user_agent = user_agent_obj.user_agent if user_agent_obj else "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + logger.debug(f"Using user agent: {user_agent} for M3U account: {account.name}") + headers = {"User-Agent": user_agent} + logger.info(f"Fetching from URL {account.server_url}") + response = requests.get(account.server_url, headers=headers, stream=True) response.raise_for_status() @@ -75,7 +80,7 @@ def fetch_m3u_lines(account, use_cache=False): send_m3u_update(account.id, "downloading", progress, speed=speed, elapsed_time=elapsed_time, time_remaining=time_remaining) send_m3u_update(account.id, "downloading", 100) - except requests.exceptions.RequestException as e: + except Exception as e: logger.error(f"Error fetching M3U from URL {account.server_url}: {e}") return [] @@ -345,8 +350,8 @@ def process_m3u_batch(account_id, batch, groups, hash_keys): Stream.objects.bulk_create(streams_to_create, ignore_conflicts=True) if streams_to_update: Stream.objects.bulk_update(streams_to_update, { key for key in stream_props.keys() if key not in ["m3u_account", "stream_hash"] and key not in hash_keys}) - # if len(existing_streams.keys()) > 0: - # Stream.objects.bulk_update(existing_streams.values(), ["last_seen"]) + if len(existing_streams.keys()) > 0: + Stream.objects.bulk_update(existing_streams.values(), ["last_seen"]) except Exception as e: logger.error(f"Bulk create failed: {str(e)}") @@ -365,18 +370,31 @@ def cleanup_streams(account_id): m3u_account__enabled=True, ).values_list('id', flat=True) logger.info(f"Found {len(existing_groups)} active groups") - streams = Stream.objects.filter(m3u_account=account) + # Calculate cutoff date for stale streams + stale_cutoff = timezone.now() - timezone.timedelta(days=account.stale_stream_days) + logger.info(f"Removing streams not seen since {stale_cutoff}") + + # Delete streams that are not in active groups streams_to_delete = Stream.objects.filter( m3u_account=account ).exclude( - channel_group__in=existing_groups # Exclude products having any of the excluded tags + channel_group__in=existing_groups ) - # Delete the filtered products - streams_to_delete.delete() + # Also delete streams that haven't been seen for longer than stale_stream_days + stale_streams = Stream.objects.filter( + m3u_account=account, + last_seen__lt=stale_cutoff + ) - logger.info(f"Cleanup complete") + deleted_count = streams_to_delete.count() + stale_count = stale_streams.count() + + streams_to_delete.delete() + stale_streams.delete() + + logger.info(f"Cleanup complete: {deleted_count} streams removed due to group filter, {stale_count} removed as stale") @shared_task def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False): diff --git a/docker/Dockerfile b/docker/Dockerfile index 26b54975..92705403 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -76,7 +76,15 @@ RUN apt-get update && \ streamlink \ wget \ gnupg2 \ - lsb-release && \ + lsb-release \ + libva-drm2 \ + libva-x11-2 \ + libva-dev \ + libva-wayland2 \ + vainfo \ + i965-va-driver \ + intel-media-va-driver \ + mesa-va-drivers && \ cp /app/docker/nginx.conf /etc/nginx/sites-enabled/default && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini index 326f4b5d..726730bf 100644 --- a/docker/uwsgi.ini +++ b/docker/uwsgi.ini @@ -24,11 +24,8 @@ vacuum = true die-on-term = true static-map = /static=/app/static -# Worker management (Optimize for I/O bound tasks) +# Worker management workers = 4 -threads = 4 -enable-threads = true -thread-stacksize=512 # Optimize for streaming http = 0.0.0.0:5656 @@ -39,8 +36,9 @@ http-timeout = 600 # Prevent disconnects from long streams lazy-apps = true # Improve memory efficiency # Async mode (use gevent for high concurrency) -gevent = 100 -async = 100 +gevent = 400 # Each unused greenlet costs ~2-4KB of memory +# Higher values have minimal performance impact when idle, but provide capacity for traffic spikes +# If memory usage becomes an issue, reduce this value # Performance tuning thunder-lock = true diff --git a/frontend/src/components/FloatingVideo.jsx b/frontend/src/components/FloatingVideo.jsx index 0be42029..9d8fb09e 100644 --- a/frontend/src/components/FloatingVideo.jsx +++ b/frontend/src/components/FloatingVideo.jsx @@ -18,23 +18,57 @@ export default function FloatingVideo() { return; } - // If the browser supports MSE for live playback, initialize mpegts.js - if (mpegts.getFeatureList().mseLivePlayback) { - const player = mpegts.createPlayer({ - type: 'mpegts', - url: streamUrl, - isLive: true, - // You can include other custom MPEGTS.js config fields here, e.g.: - // cors: true, - // withCredentials: false, - }); + // Check if we have an existing player and clean it up + if (playerRef.current) { + playerRef.current.destroy(); + playerRef.current = null; + } - player.attachMediaElement(videoRef.current); - player.load(); - player.play(); + // Debug log to help diagnose stream issues + console.log("Attempting to play stream:", streamUrl); - // Store player instance so we can clean up later - playerRef.current = player; + try { + // If the browser supports MSE for live playback, initialize mpegts.js + if (mpegts.getFeatureList().mseLivePlayback) { + const player = mpegts.createPlayer({ + type: 'mpegts', // MPEG-TS format + url: streamUrl, + isLive: true, + enableWorker: true, + enableStashBuffer: false, // Try disabling stash buffer for live streams + liveBufferLatencyChasing: true, + liveSync: true, + cors: true, // Enable CORS for cross-domain requests + }); + + player.attachMediaElement(videoRef.current); + + // Add error event handler + player.on(mpegts.Events.ERROR, (errorType, errorDetail) => { + console.error('Player error:', errorType, errorDetail); + // If it's a format issue, show a helpful message + if (errorDetail.includes('Unsupported media type')) { + const message = document.createElement('div'); + message.textContent = "Unsupported stream format. Please try a different stream."; + message.style.position = 'absolute'; + message.style.top = '50%'; + message.style.left = '50%'; + message.style.transform = 'translate(-50%, -50%)'; + message.style.color = 'white'; + message.style.textAlign = 'center'; + message.style.width = '100%'; + videoRef.current.parentNode.appendChild(message); + } + }); + + player.load(); + player.play(); + + // Store player instance so we can clean up later + playerRef.current = player; + } + } catch (error) { + console.error("Error initializing player:", error); } // Cleanup when component unmounts or streamUrl changes diff --git a/frontend/src/components/forms/M3U.jsx b/frontend/src/components/forms/M3U.jsx index a080b401..1d81279f 100644 --- a/frontend/src/components/forms/M3U.jsx +++ b/frontend/src/components/forms/M3U.jsx @@ -61,6 +61,7 @@ const M3U = ({ create_epg: false, username: '', password: '', + stale_stream_days: 7, }, validate: { @@ -84,6 +85,7 @@ const M3U = ({ account_type: m3uAccount.account_type, username: m3uAccount.username ?? '', password: '', + stale_stream_days: m3uAccount.stale_stream_days || 7, }); if (m3uAccount.account_type == 'XC') { @@ -237,15 +239,19 @@ const M3U = ({ {form.getValues().account_type == 'XC' && ( - - Create EPG - - + {!m3uAccount && ( + + Create EPG + + + )} + + { + console.log('Previewing stream:', row.original.name, 'ID:', row.original.id, 'Hash:', row.original.stream_hash); handleWatchStream(row.original.stream_hash); - }, []); + }, [row.original.id]); // Add proper dependency to ensure correct stream return ( <> diff --git a/version.py b/version.py index 25e27d60..171eaa80 100644 --- a/version.py +++ b/version.py @@ -1,5 +1,5 @@ """ Dispatcharr version information. """ -__version__ = '0.4.0' # Follow semantic versioning (MAJOR.MINOR.PATCH) +__version__ = '0.4.1' # Follow semantic versioning (MAJOR.MINOR.PATCH) __timestamp__ = None # Set during CI/CD build process