From 6180b4ffef6aaf522e39cd6683ca8b53632471d7 Mon Sep 17 00:00:00 2001 From: SergeantPanda Date: Mon, 28 Jul 2025 21:40:29 -0500 Subject: [PATCH] Save stream stats to database. --- ...am_stats_stream_stream_stats_updated_at.py | 23 ++++++++ apps/channels/models.py | 13 +++++ apps/channels/serializers.py | 2 + .../ts_proxy/services/channel_service.py | 58 +++++++++++++++++-- apps/proxy/ts_proxy/stream_manager.py | 14 ++++- 5 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 apps/channels/migrations/0023_stream_stream_stats_stream_stream_stats_updated_at.py diff --git a/apps/channels/migrations/0023_stream_stream_stats_stream_stream_stats_updated_at.py b/apps/channels/migrations/0023_stream_stream_stats_stream_stream_stats_updated_at.py new file mode 100644 index 00000000..1b0fdbe8 --- /dev/null +++ b/apps/channels/migrations/0023_stream_stream_stats_stream_stream_stats_updated_at.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.6 on 2025-07-29 02:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dispatcharr_channels', '0022_channel_auto_created_channel_auto_created_by_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='stream', + name='stream_stats', + field=models.JSONField(blank=True, help_text='JSON object containing stream statistics like video codec, resolution, etc.', null=True), + ), + migrations.AddField( + model_name='stream', + name='stream_stats_updated_at', + field=models.DateTimeField(blank=True, db_index=True, help_text='When stream statistics were last updated', null=True), + ), + ] diff --git a/apps/channels/models.py b/apps/channels/models.py index f53a9875..d6c3faef 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -95,6 +95,19 @@ class Stream(models.Model): ) last_seen = models.DateTimeField(db_index=True, default=datetime.now) custom_properties = models.TextField(null=True, blank=True) + + # Stream statistics fields + stream_stats = models.JSONField( + null=True, + blank=True, + help_text="JSON object containing stream statistics like video codec, resolution, etc." + ) + stream_stats_updated_at = models.DateTimeField( + null=True, + blank=True, + help_text="When stream statistics were last updated", + db_index=True + ) class Meta: # If you use m3u_account, you might do unique_together = ('name','url','m3u_account') diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 9273b265..32fd4a74 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -104,6 +104,8 @@ class StreamSerializer(serializers.ModelSerializer): "is_custom", "channel_group", "stream_hash", + "stream_stats", + "stream_stats_updated_at", ] def get_fields(self): diff --git a/apps/proxy/ts_proxy/services/channel_service.py b/apps/proxy/ts_proxy/services/channel_service.py index 026aa883..932479ea 100644 --- a/apps/proxy/ts_proxy/services/channel_service.py +++ b/apps/proxy/ts_proxy/services/channel_service.py @@ -417,8 +417,8 @@ class ChannelService: return False, None, None, {"error": f"Exception: {str(e)}"} @staticmethod - def parse_and_store_stream_info(channel_id, stream_info_line, stream_type="video"): - """Parse FFmpeg stream info line and store in Redis metadata""" + def parse_and_store_stream_info(channel_id, stream_info_line, stream_type="video", stream_id=None): + """Parse FFmpeg stream info line and store in Redis metadata and database""" try: if stream_type == "input": # Example lines: @@ -432,6 +432,9 @@ class ChannelService: # Store in Redis if we have valid data if input_format: ChannelService._update_stream_info_in_redis(channel_id, None, None, None, None, None, None, None, None, None, None, None, input_format) + # Save to database if stream_id is provided + if stream_id: + ChannelService._update_stream_stats_in_db(stream_id, stream_type=input_format) logger.debug(f"Input format info - Format: {input_format} for channel {channel_id}") @@ -480,6 +483,16 @@ class ChannelService: # Store in Redis if we have valid data if any(x is not None for x in [video_codec, resolution, source_fps, pixel_format, video_bitrate]): ChannelService._update_stream_info_in_redis(channel_id, video_codec, resolution, width, height, source_fps, pixel_format, video_bitrate, None, None, None, None, None) + # Save to database if stream_id is provided + if stream_id: + ChannelService._update_stream_stats_in_db( + stream_id, + video_codec=video_codec, + resolution=resolution, + source_fps=source_fps, + pixel_format=pixel_format, + video_bitrate=video_bitrate + ) logger.info(f"Video stream info - Codec: {video_codec}, Resolution: {resolution}, " f"Source FPS: {source_fps}, Pixel Format: {pixel_format}, " @@ -511,9 +524,15 @@ class ChannelService: # Store in Redis if we have valid data if any(x is not None for x in [audio_codec, sample_rate, channels, audio_bitrate]): ChannelService._update_stream_info_in_redis(channel_id, None, None, None, None, None, None, None, audio_codec, sample_rate, channels, audio_bitrate, None) - - logger.info(f"Audio stream info - Codec: {audio_codec}, Sample Rate: {sample_rate} Hz, " - f"Channels: {channels}, Audio Bitrate: {audio_bitrate} kb/s") + # Save to database if stream_id is provided + if stream_id: + ChannelService._update_stream_stats_in_db( + stream_id, + audio_codec=audio_codec, + sample_rate=sample_rate, + audio_channels=channels, + audio_bitrate=audio_bitrate + ) except Exception as e: logger.debug(f"Error parsing FFmpeg {stream_type} stream info: {e}") @@ -575,6 +594,35 @@ class ChannelService: logger.error(f"Error updating stream info in Redis: {e}") return False + @staticmethod + def _update_stream_stats_in_db(stream_id, **stats): + """Update stream stats in database""" + try: + from apps.channels.models import Stream + from django.utils import timezone + + stream = Stream.objects.get(id=stream_id) + + # Get existing stats or create new dict + current_stats = stream.stream_stats or {} + + # Update with new stats + for key, value in stats.items(): + if value is not None: + current_stats[key] = value + + # Save updated stats and timestamp + stream.stream_stats = current_stats + stream.stream_stats_updated_at = timezone.now() + stream.save(update_fields=['stream_stats', 'stream_stats_updated_at']) + + logger.debug(f"Updated stream stats in database for stream {stream_id}: {stats}") + return True + + except Exception as e: + logger.error(f"Error updating stream stats in database for stream {stream_id}: {e}") + return False + # Helper methods for Redis operations @staticmethod diff --git a/apps/proxy/ts_proxy/stream_manager.py b/apps/proxy/ts_proxy/stream_manager.py index f7c538c2..e80d4527 100644 --- a/apps/proxy/ts_proxy/stream_manager.py +++ b/apps/proxy/ts_proxy/stream_manager.py @@ -587,9 +587,9 @@ class StreamManager: from .services.channel_service import ChannelService if "video:" in content_lower: - ChannelService.parse_and_store_stream_info(self.channel_id, content, "video") + ChannelService.parse_and_store_stream_info(self.channel_id, content, "video", self.current_stream_id) elif "audio:" in content_lower: - ChannelService.parse_and_store_stream_info(self.channel_id, content, "audio") + ChannelService.parse_and_store_stream_info(self.channel_id, content, "audio", self.current_stream_id) # Determine log level based on content if any(keyword in content_lower for keyword in ['error', 'failed', 'cannot', 'invalid', 'corrupt']): @@ -605,7 +605,7 @@ class StreamManager: if content.startswith('Input #0'): # If it's input 0, parse stream info from .services.channel_service import ChannelService - ChannelService.parse_and_store_stream_info(self.channel_id, content, "input") + ChannelService.parse_and_store_stream_info(self.channel_id, content, "input", self.current_stream_id) else: # Everything else at debug level logger.debug(f"FFmpeg stderr for channel {self.channel_id}: {content}") @@ -649,6 +649,14 @@ class StreamManager: if any(x is not None for x in [ffmpeg_speed, ffmpeg_fps, actual_fps, ffmpeg_output_bitrate]): self._update_ffmpeg_stats_in_redis(ffmpeg_speed, ffmpeg_fps, actual_fps, ffmpeg_output_bitrate) + # Also save ffmpeg_output_bitrate to database if we have stream_id + if ffmpeg_output_bitrate is not None and self.current_stream_id: + from .services.channel_service import ChannelService + ChannelService._update_stream_stats_in_db( + self.current_stream_id, + ffmpeg_output_bitrate=ffmpeg_output_bitrate + ) + # Fix the f-string formatting actual_fps_str = f"{actual_fps:.1f}" if actual_fps is not None else "N/A" ffmpeg_output_bitrate_str = f"{ffmpeg_output_bitrate:.1f}" if ffmpeg_output_bitrate is not None else "N/A"