diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py deleted file mode 100644 index e38e8750..00000000 --- a/apps/accounts/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:33 - -import django.contrib.auth.models -import django.contrib.auth.validators -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('channels', '__first__'), - ] - - operations = [ - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('avatar_config', models.JSONField(blank=True, default=dict, null=True)), - ('channel_groups', models.ManyToManyField(blank=True, related_name='users', to='channels.channelgroup')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - ] diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py new file mode 100644 index 00000000..c276f6c4 --- /dev/null +++ b/apps/accounts/urls.py @@ -0,0 +1,11 @@ +from django.urls import path +from django.contrib.auth import views as auth_views + +app_name = 'accounts' + +urlpatterns = [ + # Login view using Django's built-in authentication + path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'), + # Logout view using Django's built-in authentication + path('logout/', auth_views.LogoutView.as_view(next_page='accounts:login'), name='logout'), +] diff --git a/apps/channels/migrations/0001_initial.py b/apps/channels/migrations/0001_initial.py deleted file mode 100644 index fa790d6a..00000000 --- a/apps/channels/migrations/0001_initial.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:33 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('m3u', '__first__'), - ] - - operations = [ - migrations.CreateModel( - name='ChannelGroup', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ], - ), - migrations.CreateModel( - name='Stream', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='Default Stream', max_length=255)), - ('url', models.URLField()), - ('custom_url', models.URLField(blank=True, max_length=2000, null=True)), - ('logo_url', models.URLField(blank=True, max_length=2000, null=True)), - ('tvg_id', models.CharField(blank=True, max_length=255, null=True)), - ('local_file', models.FileField(blank=True, null=True, upload_to='uploads/')), - ('current_viewers', models.PositiveIntegerField(default=0)), - ('is_transcoded', models.BooleanField(default=False)), - ('ffmpeg_preset', models.CharField(blank=True, max_length=50, null=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('group_name', models.CharField(blank=True, max_length=255, null=True)), - ('m3u_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='streams', to='m3u.m3uaccount')), - ], - options={ - 'verbose_name': 'Stream', - 'verbose_name_plural': 'Streams', - 'ordering': ['-updated_at'], - }, - ), - migrations.CreateModel( - name='Channel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('channel_number', models.IntegerField()), - ('channel_name', models.CharField(max_length=255)), - ('logo_url', models.URLField(blank=True, max_length=2000, null=True)), - ('logo_file', models.ImageField(blank=True, null=True, upload_to='logos/')), - ('tvg_id', models.CharField(blank=True, max_length=255, null=True)), - ('tvg_name', models.CharField(blank=True, max_length=255, null=True)), - ('is_active', models.BooleanField(default=True)), - ('is_looping', models.BooleanField(default=False, help_text='If True, loops local file(s).')), - ('shuffle_mode', models.BooleanField(default=False, help_text='If True, randomize streams for failover.')), - ('channel_group', models.ForeignKey(blank=True, help_text='Channel group this channel belongs to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='channels', to='channels.channelgroup')), - ('streams', models.ManyToManyField(blank=True, related_name='channels', to='channels.stream')), - ], - ), - ] diff --git a/apps/channels/models.py b/apps/channels/models.py index dc4c38d7..fdeba659 100644 --- a/apps/channels/models.py +++ b/apps/channels/models.py @@ -24,10 +24,15 @@ class Stream(models.Model): local_file = models.FileField(upload_to='uploads/', blank=True, null=True) current_viewers = models.PositiveIntegerField(default=0) is_transcoded = models.BooleanField(default=False) - ffmpeg_preset = models.CharField(max_length=50, blank=True, null=True) updated_at = models.DateTimeField(auto_now=True) group_name = models.CharField(max_length=255, blank=True, null=True) - + stream_profile = models.ForeignKey( + StreamProfile, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='streams' + ) class Meta: # If you use m3u_account, you might do unique_together = ('name','custom_url','m3u_account') verbose_name = "Stream" diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index 62e00082..9e53abe7 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -1,10 +1,17 @@ from rest_framework import serializers from .models import Stream, Channel, ChannelGroup +from core.models import StreamProfile # # Stream # class StreamSerializer(serializers.ModelSerializer): + stream_profile_id = serializers.PrimaryKeyRelatedField( + queryset=StreamProfile.objects.all(), + source='stream_profile', + allow_null=True, + required=False + ) class Meta: model = Stream fields = [ @@ -18,9 +25,9 @@ class StreamSerializer(serializers.ModelSerializer): 'local_file', 'current_viewers', 'is_transcoded', - 'ffmpeg_preset', 'updated_at', 'group_name', + 'stream_profile_id', ] @@ -61,5 +68,6 @@ class ChannelSerializer(serializers.ModelSerializer): 'channel_group_id', 'tvg_id', 'tvg_name', - 'streams' + 'streams', + 'stream_profile_id', ] diff --git a/apps/channels/views.py b/apps/channels/views.py index b834d712..2292a128 100644 --- a/apps/channels/views.py +++ b/apps/channels/views.py @@ -38,7 +38,4 @@ class StreamDashboardView(View): @login_required def channels_dashboard_view(request): - """ - Example “dashboard” style view for Channels - """ - return render(request, 'channels/channels.html') + return render(request, 'channels/channels.html') \ No newline at end of file diff --git a/apps/dashboard/migrations/0001_initial.py b/apps/dashboard/migrations/0001_initial.py deleted file mode 100644 index c1e3065f..00000000 --- a/apps/dashboard/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Settings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('server_name', models.CharField(default='Dispatcharr', max_length=255)), - ('time_zone', models.CharField(default='UTC', max_length=50)), - ('default_logo_url', models.URLField(blank=True, null=True)), - ('max_concurrent_streams', models.PositiveIntegerField(default=10)), - ('auto_backup_frequency', models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly')], default='weekly', max_length=50)), - ('enable_debug_logs', models.BooleanField(default=False)), - ('schedules_direct_username', models.CharField(blank=True, max_length=255, null=True)), - ('schedules_direct_password', models.CharField(blank=True, max_length=255, null=True)), - ('schedules_direct_update_frequency', models.CharField(choices=[('12h', 'Every 12 Hours'), ('daily', 'Daily')], default='daily', max_length=50)), - ('schedules_direct_api_key', models.CharField(blank=True, max_length=255, null=True)), - ('transcoding_bitrate', models.PositiveIntegerField(default=2000)), - ('transcoding_audio_codec', models.CharField(choices=[('aac', 'AAC'), ('mp3', 'MP3')], default='aac', max_length=50)), - ('transcoding_resolution', models.CharField(choices=[('720p', '720p'), ('1080p', '1080p')], default='1080p', max_length=50)), - ('failover_behavior', models.CharField(choices=[('sequential', 'Sequential'), ('random', 'Random')], default='sequential', max_length=50)), - ('stream_health_check_frequency', models.PositiveIntegerField(default=5)), - ('email_notifications', models.BooleanField(default=False)), - ('webhook_url', models.URLField(blank=True, null=True)), - ('cpu_alert_threshold', models.PositiveIntegerField(default=90)), - ('memory_alert_threshold', models.PositiveIntegerField(default=90)), - ('hdhr_integration', models.BooleanField(default=True)), - ('custom_api_endpoints', models.JSONField(blank=True, null=True)), - ('backup_path', models.CharField(default='backups/', max_length=255)), - ('backup_frequency', models.CharField(choices=[('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly')], default='weekly', max_length=50)), - ('ffmpeg_path', models.CharField(default='/usr/bin/ffmpeg', max_length=255)), - ('custom_transcoding_flags', models.TextField(blank=True, null=True)), - ('celery_worker_concurrency', models.PositiveIntegerField(default=4)), - ], - ), - ] diff --git a/apps/epg/migrations/0001_initial.py b/apps/epg/migrations/0001_initial.py deleted file mode 100644 index 2fa9973c..00000000 --- a/apps/epg/migrations/0001_initial.py +++ /dev/null @@ -1,38 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:33 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('channels', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='EPGSource', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255, unique=True)), - ('source_type', models.CharField(choices=[('xmltv', 'XMLTV URL'), ('schedules_direct', 'Schedules Direct API')], max_length=20)), - ('url', models.URLField(blank=True, null=True)), - ('api_key', models.CharField(blank=True, max_length=255, null=True)), - ('is_active', models.BooleanField(default=True)), - ], - ), - migrations.CreateModel( - name='Program', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=255)), - ('description', models.TextField(blank=True, null=True)), - ('start_time', models.DateTimeField()), - ('end_time', models.DateTimeField()), - ('channel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='programs', to='channels.channel')), - ], - ), - ] diff --git a/apps/hdhr/migrations/0001_initial.py b/apps/hdhr/migrations/0001_initial.py deleted file mode 100644 index 74153d0a..00000000 --- a/apps/hdhr/migrations/0001_initial.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='HDHRDevice', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('friendly_name', models.CharField(default='Dispatcharr HDHomeRun', max_length=100)), - ('device_id', models.CharField(max_length=32, unique=True)), - ('tuner_count', models.PositiveIntegerField(default=3)), - ], - ), - ] diff --git a/apps/hdhr/ssdp.py b/apps/hdhr/ssdp.py index 012004f2..660d9c2f 100644 --- a/apps/hdhr/ssdp.py +++ b/apps/hdhr/ssdp.py @@ -1,23 +1,29 @@ +import os import socket import threading import time +from django.conf import settings # SSDP Multicast Address and Port SSDP_MULTICAST = "239.255.255.250" SSDP_PORT = 1900 -# Server Information DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaServer:1" -SERVER_IP = "10.0.0.107" # Replace with your server's IP address SERVER_PORT = 8000 -def ssdp_response(addr): - """Send an SSDP response to a specific address.""" +def get_host_ip(): + try: + # This relies on "host.docker.internal" being mapped to the host’s gateway IP. + return socket.gethostbyname("host.docker.internal") + except Exception: + return "127.0.0.1" + +def ssdp_response(addr, host_ip): response = ( f"HTTP/1.1 200 OK\r\n" f"CACHE-CONTROL: max-age=1800\r\n" f"EXT:\r\n" - f"LOCATION: http://{SERVER_IP}:{SERVER_PORT}/hdhr/device.xml\r\n" + f"LOCATION: http://{host_ip}:{SERVER_PORT}/hdhr/device.xml\r\n" f"SERVER: Dispatcharr/1.0 UPnP/1.0 HDHomeRun/1.0\r\n" f"ST: {DEVICE_TYPE}\r\n" f"USN: uuid:device1-1::{DEVICE_TYPE}\r\n" @@ -27,25 +33,22 @@ def ssdp_response(addr): sock.sendto(response.encode("utf-8"), addr) sock.close() -def ssdp_listener(): - """Listen for SSDP M-SEARCH requests and respond.""" +def ssdp_listener(host_ip): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((SSDP_MULTICAST, SSDP_PORT)) - while True: data, addr = sock.recvfrom(1024) if b"M-SEARCH" in data and DEVICE_TYPE.encode("utf-8") in data: print(f"Received M-SEARCH from {addr}") - ssdp_response(addr) + ssdp_response(addr, host_ip) -def ssdp_broadcaster(): - """Broadcast SSDP NOTIFY messages periodically.""" +def ssdp_broadcaster(host_ip): notify = ( f"NOTIFY * HTTP/1.1\r\n" f"HOST: {SSDP_MULTICAST}:{SSDP_PORT}\r\n" f"CACHE-CONTROL: max-age=1800\r\n" - f"LOCATION: http://{SERVER_IP}:{SERVER_PORT}/hdhr/device.xml\r\n" + f"LOCATION: http://{host_ip}:{SERVER_PORT}/hdhr/device.xml\r\n" f"SERVER: Dispatcharr/1.0 UPnP/1.0 HDHomeRun/1.0\r\n" f"NT: {DEVICE_TYPE}\r\n" f"NTS: ssdp:alive\r\n" @@ -54,19 +57,12 @@ def ssdp_broadcaster(): ) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - while True: sock.sendto(notify.encode("utf-8"), (SSDP_MULTICAST, SSDP_PORT)) time.sleep(30) -from django.conf import settings - def start_ssdp(): - """Start SSDP services.""" - global SERVER_IP - # Dynamically get the IP address of the server - SERVER_IP = settings.SERVER_IP or "127.0.0.1" # Default to localhost if not set - threading.Thread(target=ssdp_listener, daemon=True).start() - threading.Thread(target=ssdp_broadcaster, daemon=True).start() - print(f"SSDP services started on {SERVER_IP}.") - + host_ip = get_host_ip() + threading.Thread(target=ssdp_listener, args=(host_ip,), daemon=True).start() + threading.Thread(target=ssdp_broadcaster, args=(host_ip,), daemon=True).start() + print(f"SSDP services started on {host_ip}.") diff --git a/apps/m3u/migrations/0001_initial.py b/apps/m3u/migrations/0001_initial.py deleted file mode 100644 index b6c96e95..00000000 --- a/apps/m3u/migrations/0001_initial.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 4.2.2 on 2025-02-18 16:34 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='M3UAccount', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='Unique name for this M3U account', max_length=255, unique=True)), - ('server_url', models.URLField(blank=True, help_text='The base URL of the M3U server (optional if a file is uploaded)', null=True)), - ('uploaded_file', models.FileField(blank=True, null=True, upload_to='m3u_uploads/')), - ('max_streams', models.PositiveIntegerField(default=0, help_text='Maximum number of concurrent streams (0 for unlimited)')), - ('is_active', models.BooleanField(default=True, help_text='Set to false to deactivate this M3U account')), - ('created_at', models.DateTimeField(auto_now_add=True, help_text='Time when this account was created')), - ('updated_at', models.DateTimeField(auto_now=True, help_text='Time when this account was last updated')), - ], - ), - migrations.CreateModel( - name='ServerGroup', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text='Unique name for this server group.', max_length=100, unique=True)), - ], - ), - migrations.CreateModel( - name='M3UFilter', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('filter_type', models.CharField(choices=[('group', 'Group Title'), ('name', 'Stream Name')], default='group', help_text='Filter based on either group title or stream name.', max_length=50)), - ('regex_pattern', models.CharField(help_text='A regex pattern to match streams or groups.', max_length=200)), - ('exclude', models.BooleanField(default=True, help_text='If True, matching items are excluded; if False, only matches are included.')), - ('m3u_account', models.ForeignKey(help_text='The M3U account this filter is applied to.', on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='m3u.m3uaccount')), - ], - ), - migrations.AddField( - model_name='m3uaccount', - name='server_group', - field=models.ForeignKey(blank=True, help_text='The server group this M3U account belongs to', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='m3u.servergroup'), - ), - ] diff --git a/apps/m3u/migrations/0004_alter_m3uaccount_user_agent_delete_useragent.py b/apps/m3u/migrations/0004_alter_m3uaccount_user_agent_delete_useragent.py deleted file mode 100644 index 4dbf71b2..00000000 --- a/apps/m3u/migrations/0004_alter_m3uaccount_user_agent_delete_useragent.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.6 on 2025-02-21 14:58 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('core', '0001_initial'), - ('m3u', '0003_m3uaccount_user_agent'), - ] - - operations = [ - migrations.AlterField( - model_name='m3uaccount', - name='user_agent', - field=models.ForeignKey(blank=True, help_text='The User-Agent associated with this M3U account.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m3u_accounts', to='core.useragent'), - ), - migrations.DeleteModel( - name='UserAgent', - ), - ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/dispatcharr/urls.py b/dispatcharr/urls.py index 85c3fdf4..b12f91cd 100644 --- a/dispatcharr/urls.py +++ b/dispatcharr/urls.py @@ -28,7 +28,7 @@ urlpatterns = [ path('api/', include(('apps.api.urls', 'api'), namespace='api')), path('admin/', admin.site.urls), - #path('accounts/', include(('apps.accounts.urls', 'accounts'), namespace='accounts')), + path('accounts/', include(('apps.accounts.urls', 'accounts'), namespace='accounts')), #path('streams/', include(('apps.streams.urls', 'streams'), namespace='streams')), #path('hdhr/', include(('apps.hdhr.urls', 'hdhr'), namespace='hdhr')), path('m3u/', include(('apps.m3u.urls', 'm3u'), namespace='m3u')), diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 91dcd81a..b644ea3e 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,6 +11,8 @@ services: - redis volumes: - ../:/app + extra_hosts: + - "host.docker.internal:host-gateway" environment: - POSTGRES_HOST=db - POSTGRES_DB=dispatcharr @@ -29,6 +31,8 @@ services: - redis volumes: - ../:/app + extra_hosts: + - "host.docker.internal:host-gateway" environment: - POSTGRES_HOST=localhost - POSTGRES_DB=dispatcharr @@ -58,16 +62,5 @@ services: image: redis:latest container_name: dispatcharr_redis - - # You can add an Nginx or Traefik service here for SSL - # nginx: - # image: nginx:alpine - # container_name: dispatcharr_nginx - # ports: - # - "80:80" - # - "443:443" - # depends_on: - # - web - volumes: postgres_data: diff --git a/templates/base.html b/templates/base.html index 4d5557ca..e4fd3fc3 100755 --- a/templates/base.html +++ b/templates/base.html @@ -39,10 +39,20 @@