merged in dev

This commit is contained in:
dekzter 2025-05-02 09:33:09 -04:00
commit 06879ed8ef
11 changed files with 171 additions and 46 deletions

View file

@ -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)

View file

@ -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.'),
),
]

View file

@ -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

View file

@ -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': {

View file

@ -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):

View file

@ -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/*

View file

@ -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

View file

@ -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

View file

@ -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' && (
<Box>
<Group justify="space-between">
<Box>Create EPG</Box>
<Switch
id="create_epg"
name="create_epg"
key={form.key('create_epg')}
{...form.getInputProps('create_epg', { type: 'checkbox' })}
/>
</Group>
{!m3uAccount && (
<Group justify="space-between">
<Box>Create EPG</Box>
<Switch
id="create_epg"
name="create_epg"
key={form.key('create_epg')}
{...form.getInputProps('create_epg', {
type: 'checkbox',
})}
/>
</Group>
)}
<TextInput
id="username"
@ -306,6 +312,14 @@ const M3U = ({
key={form.key('refresh_interval')}
/>
<NumberInput
min={1}
max={365}
label="Stale Stream Retention (days)"
description="Streams not seen for this many days will be removed"
{...form.getInputProps('stale_stream_days')}
/>
<Checkbox
label="Is Active"
{...form.getInputProps('is_active', { type: 'checkbox' })}

View file

@ -89,8 +89,9 @@ const StreamRowActions = ({
}, []);
const onPreview = useCallback(() => {
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 (
<>

View file

@ -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