mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-22 18:28:00 +00:00
Initial commit
This commit is contained in:
commit
8edb743ebd
1078 changed files with 264296 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
34
.idea/DispatcharrSM.iml
generated
Normal file
34
.idea/DispatcharrSM.iml
generated
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="FacetManager">
|
||||
<facet type="django" name="Django">
|
||||
<configuration>
|
||||
<option name="rootFolder" value="$MODULE_DIR$" />
|
||||
<option name="settingsModule" value="dispatcharr/settings.py" />
|
||||
<option name="manageScript" value="$MODULE_DIR$/manage.py" />
|
||||
<option name="environment" value="<map/>" />
|
||||
<option name="doNotUseTestRunner" value="false" />
|
||||
<option name="trackFilePattern" value="migrations" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.9 (DispatcharrSM)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Django" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.9 (DispatcharrSM)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (DispatcharrSM)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/DispatcharrSM.iml" filepath="$PROJECT_DIR$/.idea/DispatcharrSM.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
77
FileTree.py
Executable file
77
FileTree.py
Executable file
|
|
@ -0,0 +1,77 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
|
||||
# Specify the names of the script file and output file to exclude them
|
||||
SCRIPT_NAME = "FileTree.py"
|
||||
OUTPUT_FILE = "FileTree.txt"
|
||||
EXCLUDED_FILES = {SCRIPT_NAME, OUTPUT_FILE, ".DS_Store", "__init__.py", "FileTree.old.txt"}
|
||||
EXCLUDED_DIRS = {"__pycache__", "migrations", "static", "staticfiles", "media", ".venv", ".idea"} # Exclude directories like __pycache__
|
||||
|
||||
def generate_file_tree(output_file):
|
||||
"""Generate a pretty file tree of the current directory and subdirectories."""
|
||||
with open(output_file, 'w') as f:
|
||||
for root, dirs, files in os.walk('.'): # Walk through the directory tree
|
||||
# Remove excluded directories from the traversal
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
|
||||
level = root.count(os.sep)
|
||||
indent = '│ ' * level
|
||||
f.write(f"{indent}├── {os.path.basename(root)}/\n")
|
||||
sub_indent = '│ ' * (level + 1)
|
||||
for i, file in enumerate(files):
|
||||
if file not in EXCLUDED_FILES:
|
||||
connector = '└── ' if i == len(files) - 1 else '├── '
|
||||
f.write(f"{sub_indent}{connector}{file}\n")
|
||||
|
||||
def append_file_contents(output_file):
|
||||
"""Append contents of each file in the current directory and subdirectories to the output file, excluding specified files."""
|
||||
# Determine the maximum width for the boxes
|
||||
max_width = 20 # Default minimum width
|
||||
file_paths = []
|
||||
for root, dirs, files in os.walk('.'): # Walk through the directory tree
|
||||
# Remove excluded directories from the traversal
|
||||
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
|
||||
for file_name in files:
|
||||
if file_name not in EXCLUDED_FILES:
|
||||
file_path = os.path.join(root, file_name)
|
||||
relative_path = os.path.relpath(file_path, start='.')
|
||||
directory = os.path.dirname(relative_path)
|
||||
base_name = os.path.basename(relative_path)
|
||||
file_paths.append((directory, base_name))
|
||||
max_width = max(max_width, len(directory) + 10, len(base_name) + 10)
|
||||
|
||||
max_width += 4 # Add padding for aesthetics
|
||||
|
||||
# Append file contents with uniform box size
|
||||
with open(output_file, 'a') as f:
|
||||
for directory, base_name in file_paths:
|
||||
# Add the formatted header for the file
|
||||
horizontal_line = f"┌{'─' * max_width}┐"
|
||||
directory_line = f"│ Directory: {directory:<{max_width - 12}}│"
|
||||
file_line = f"│ File: {base_name:<{max_width - 12}}│"
|
||||
bottom_line = f"└{'─' * max_width}┘"
|
||||
|
||||
f.write(f"\n{horizontal_line}\n")
|
||||
f.write(f"{directory_line}\n")
|
||||
f.write(f"{file_line}\n")
|
||||
f.write(f"{bottom_line}\n\n")
|
||||
|
||||
# Append the contents of the file
|
||||
file_path = os.path.join(directory, base_name)
|
||||
try:
|
||||
with open(file_path, 'r', errors='ignore') as file:
|
||||
f.write(file.read())
|
||||
except Exception as e:
|
||||
f.write(f"Error reading {file_path}: {e}\n")
|
||||
|
||||
# Add a visually distinct footer to signify the end of the file
|
||||
f.write(f"\n========= END OF FILE =========\n")
|
||||
f.write(f"File: {base_name}\n")
|
||||
f.write(f"===============================\n\n")
|
||||
|
||||
def main():
|
||||
generate_file_tree(OUTPUT_FILE)
|
||||
append_file_contents(OUTPUT_FILE)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
6348
FileTree.txt
Normal file
6348
FileTree.txt
Normal file
File diff suppressed because it is too large
Load diff
2
README.md
Normal file
2
README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Dispatcharr
|
||||
|
||||
BIN
apps/.DS_Store
vendored
Normal file
BIN
apps/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
apps/accounts/.DS_Store
vendored
Normal file
BIN
apps/accounts/.DS_Store
vendored
Normal file
Binary file not shown.
0
apps/accounts/__init__.py
Normal file
0
apps/accounts/__init__.py
Normal file
BIN
apps/accounts/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/admin.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/api_urls.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/api_urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/api_views.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/api_views.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/apps.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/forms.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/forms.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/models.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/serializers.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/serializers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/urls.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/accounts/__pycache__/views.cpython-310.pyc
Normal file
BIN
apps/accounts/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
16
apps/accounts/admin.py
Normal file
16
apps/accounts/admin.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin, GroupAdmin
|
||||
from django.contrib.auth.models import Group
|
||||
from .models import User
|
||||
|
||||
@admin.register(User)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'password', 'avatar_config', 'groups')}),
|
||||
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'user_permissions')}),
|
||||
('Important dates', {'fields': ('last_login', 'date_joined')}),
|
||||
)
|
||||
|
||||
# Unregister default Group admin and re-register it.
|
||||
admin.site.unregister(Group)
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
35
apps/accounts/api_urls.py
Normal file
35
apps/accounts/api_urls.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .api_views import (
|
||||
AuthViewSet, UserViewSet, GroupViewSet,
|
||||
list_permissions
|
||||
)
|
||||
|
||||
app_name = 'accounts'
|
||||
|
||||
# 🔹 Register ViewSets with a Router
|
||||
router = DefaultRouter()
|
||||
router.register(r'users', UserViewSet, basename='user')
|
||||
router.register(r'groups', GroupViewSet, basename='group')
|
||||
|
||||
# 🔹 Custom Authentication Endpoints
|
||||
auth_view = AuthViewSet.as_view({
|
||||
'post': 'login'
|
||||
})
|
||||
|
||||
logout_view = AuthViewSet.as_view({
|
||||
'post': 'logout'
|
||||
})
|
||||
|
||||
# 🔹 Define API URL patterns
|
||||
urlpatterns = [
|
||||
# Authentication
|
||||
path('auth/login/', auth_view, name='user-login'),
|
||||
path('auth/logout/', logout_view, name='user-logout'),
|
||||
|
||||
# Permissions API
|
||||
path('permissions/', list_permissions, name='list-permissions'),
|
||||
]
|
||||
|
||||
# 🔹 Include ViewSet routes
|
||||
urlpatterns += router.urls
|
||||
132
apps/accounts/api_views.py
Normal file
132
apps/accounts/api_views.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import viewsets
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from .models import User
|
||||
from .serializers import UserSerializer, GroupSerializer, PermissionSerializer
|
||||
|
||||
|
||||
# 🔹 1) Authentication APIs
|
||||
class AuthViewSet(viewsets.ViewSet):
|
||||
"""Handles user login and logout"""
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Authenticate and log in a user",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=['username', 'password'],
|
||||
properties={
|
||||
'username': openapi.Schema(type=openapi.TYPE_STRING),
|
||||
'password': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_PASSWORD)
|
||||
},
|
||||
),
|
||||
responses={200: "Login successful", 400: "Invalid credentials"},
|
||||
)
|
||||
def login(self, request):
|
||||
"""Logs in a user and returns user details"""
|
||||
username = request.data.get('username')
|
||||
password = request.data.get('password')
|
||||
user = authenticate(request, username=username, password=password)
|
||||
|
||||
if user:
|
||||
login(request, user)
|
||||
return Response({
|
||||
"message": "Login successful",
|
||||
"user": {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"groups": list(user.groups.values_list('name', flat=True))
|
||||
}
|
||||
})
|
||||
return Response({"error": "Invalid credentials"}, status=400)
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Log out the current user",
|
||||
responses={200: "Logout successful"}
|
||||
)
|
||||
def logout(self, request):
|
||||
"""Logs out the authenticated user"""
|
||||
logout(request)
|
||||
return Response({"message": "Logout successful"})
|
||||
|
||||
|
||||
# 🔹 2) User Management APIs
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for Users"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Retrieve a list of users",
|
||||
responses={200: UserSerializer(many=True)}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Retrieve a specific user by ID")
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Create a new user")
|
||||
def create(self, request, *args, **kwargs):
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Update a user")
|
||||
def update(self, request, *args, **kwargs):
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Delete a user")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
# 🔹 3) Group Management APIs
|
||||
class GroupViewSet(viewsets.ModelViewSet):
|
||||
"""Handles CRUD operations for Groups"""
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Retrieve a list of groups",
|
||||
responses={200: GroupSerializer(many=True)}
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Retrieve a specific group by ID")
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
return super().retrieve(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Create a new group")
|
||||
def create(self, request, *args, **kwargs):
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Update a group")
|
||||
def update(self, request, *args, **kwargs):
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(operation_description="Delete a group")
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
# 🔹 4) Permissions List API
|
||||
@swagger_auto_schema(
|
||||
method='get',
|
||||
operation_description="Retrieve a list of all permissions",
|
||||
responses={200: PermissionSerializer(many=True)}
|
||||
)
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def list_permissions(request):
|
||||
"""Returns a list of all available permissions"""
|
||||
permissions = Permission.objects.all()
|
||||
serializer = PermissionSerializer(permissions, many=True)
|
||||
return Response(serializer.data)
|
||||
6
apps/accounts/apps.py
Normal file
6
apps/accounts/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.accounts'
|
||||
verbose_name = "Accounts & Authentication"
|
||||
59
apps/accounts/forms.py
Normal file
59
apps/accounts/forms.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import Group as AuthGroup
|
||||
from apps.channels.models import ChannelGroup
|
||||
from .models import User
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class UserRegistrationForm(UserCreationForm):
|
||||
groups = forms.ModelMultipleChoiceField(
|
||||
queryset=AuthGroup.objects.all(),
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'groups', 'password1', 'password2', ]
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
if commit:
|
||||
user.save()
|
||||
self.save_m2m() # Save the many-to-many field data
|
||||
return user
|
||||
|
||||
|
||||
|
||||
class GroupForm(forms.ModelForm):
|
||||
permissions = forms.ModelMultipleChoiceField(
|
||||
queryset=Permission.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AuthGroup
|
||||
fields = ['name', 'permissions']
|
||||
|
||||
|
||||
class UserEditForm(forms.ModelForm):
|
||||
auth_groups = forms.ModelMultipleChoiceField(
|
||||
queryset=AuthGroup.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False,
|
||||
label="Auth Groups"
|
||||
)
|
||||
channel_groups = forms.ModelMultipleChoiceField(
|
||||
queryset=ChannelGroup.objects.all(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False,
|
||||
label="Channel Groups"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'email', 'auth_groups', 'channel_groups']
|
||||
47
apps/accounts/migrations/0001_initial.py
Normal file
47
apps/accounts/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
apps/accounts/migrations/__init__.py
Normal file
0
apps/accounts/migrations/__init__.py
Normal file
Binary file not shown.
BIN
apps/accounts/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/accounts/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
30
apps/accounts/models.py
Normal file
30
apps/accounts/models.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# apps/accounts/models.py
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser, Permission
|
||||
|
||||
class User(AbstractUser):
|
||||
"""
|
||||
Custom user model for Dispatcharr.
|
||||
Inherits from Django's AbstractUser to add additional fields if needed.
|
||||
"""
|
||||
avatar_config = models.JSONField(default=dict, blank=True, null=True)
|
||||
channel_groups = models.ManyToManyField(
|
||||
'channels.ChannelGroup', # Updated reference to renamed model
|
||||
blank=True,
|
||||
related_name="users"
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
def get_groups(self):
|
||||
"""
|
||||
Returns the groups (roles) the user belongs to.
|
||||
"""
|
||||
return self.groups.all()
|
||||
|
||||
def get_permissions(self):
|
||||
"""
|
||||
Returns the permissions assigned to the user and their groups.
|
||||
"""
|
||||
return self.user_permissions.all() | Permission.objects.filter(group__user=self)
|
||||
32
apps/accounts/serializers.py
Normal file
32
apps/accounts/serializers.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from .models import User
|
||||
|
||||
|
||||
# 🔹 Fix for Permission serialization
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Permission
|
||||
fields = ['id', 'name', 'codename']
|
||||
|
||||
|
||||
# 🔹 Fix for Group serialization
|
||||
class GroupSerializer(serializers.ModelSerializer):
|
||||
permissions = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Permission.objects.all()
|
||||
) # ✅ Fixes ManyToManyField `_meta` error
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
fields = ['id', 'name', 'permissions']
|
||||
|
||||
|
||||
# 🔹 Fix for User serialization
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
groups = serializers.SlugRelatedField(
|
||||
many=True, queryset=Group.objects.all(), slug_field="name"
|
||||
) # ✅ Fix ManyToMany `_meta` error
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email', 'groups']
|
||||
14
apps/accounts/signals.py
Normal file
14
apps/accounts/signals.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# apps/accounts/signals.py
|
||||
# Example: automatically create something on user creation
|
||||
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from .models import User
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def handle_new_user(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
# e.g. initialize default avatar config
|
||||
if not instance.avatar_config:
|
||||
instance.avatar_config = {"style": "circle"}
|
||||
instance.save()
|
||||
BIN
apps/api/.DS_Store
vendored
Normal file
BIN
apps/api/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
apps/api/__pycache__/urls.cpython-310.pyc
Normal file
BIN
apps/api/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
40
apps/api/urls.py
Normal file
40
apps/api/urls.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
from django.urls import path, include
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_yasg import openapi
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
# Configure Swagger Schema
|
||||
schema_view = get_schema_view(
|
||||
openapi.Info(
|
||||
title="Dispatcharr API",
|
||||
default_version='v1',
|
||||
description="API documentation for Dispatcharr",
|
||||
terms_of_service="https://www.google.com/policies/terms/",
|
||||
contact=openapi.Contact(email="support@dispatcharr.local"),
|
||||
license=openapi.License(name="Unlicense"),
|
||||
),
|
||||
public=True,
|
||||
permission_classes=(AllowAny,),
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('accounts/', include(('apps.accounts.api_urls', 'accounts'), namespace='accounts')),
|
||||
#path('backup/', include(('apps.backup.api_urls', 'backup'), namespace='backup')),
|
||||
path('channels/', include(('apps.channels.api_urls', 'channels'), namespace='channels')),
|
||||
path('epg/', include(('apps.epg.api_urls', 'epg'), namespace='epg')),
|
||||
path('hdhr/', include(('apps.hdhr.api_urls', 'hdhr'), namespace='hdhr')),
|
||||
path('m3u/', include(('apps.m3u.api_urls', 'm3u'), namespace='m3u')),
|
||||
# path('output/', include(('apps.output.api_urls', 'output'), namespace='output')),
|
||||
#path('player/', include(('apps.player.api_urls', 'player'), namespace='player')),
|
||||
#path('settings/', include(('apps.settings.api_urls', 'settings'), namespace='settings')),
|
||||
#path('streams/', include(('apps.streams.api_urls', 'streams'), namespace='streams')),
|
||||
|
||||
|
||||
|
||||
# Swagger Documentation api_urls
|
||||
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
path('swagger.json', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||
]
|
||||
BIN
apps/channels/.DS_Store
vendored
Normal file
BIN
apps/channels/.DS_Store
vendored
Normal file
Binary file not shown.
0
apps/channels/__init__.py
Normal file
0
apps/channels/__init__.py
Normal file
BIN
apps/channels/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/admin.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/api_urls.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/api_urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/api_views.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/api_views.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/apps.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/dashboard_urls.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/dashboard_urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/dashboard_views.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/dashboard_views.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/forms.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/forms.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/models.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/serializers.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/serializers.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/signals.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/signals.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/tasks.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/tasks.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/urls.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/utils.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/utils.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/channels/__pycache__/views.cpython-310.pyc
Normal file
BIN
apps/channels/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
27
apps/channels/admin.py
Normal file
27
apps/channels/admin.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
from django.contrib import admin
|
||||
from .models import Stream, Channel, ChannelGroup
|
||||
|
||||
@admin.register(Stream)
|
||||
class StreamAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'id', 'name', 'group_name', 'custom_url',
|
||||
'current_viewers', 'is_transcoded', 'updated_at',
|
||||
)
|
||||
list_filter = ('group_name', 'is_transcoded')
|
||||
search_fields = ('name', 'custom_url', 'group_name')
|
||||
ordering = ('-updated_at',)
|
||||
|
||||
@admin.register(Channel)
|
||||
class ChannelAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'channel_number', 'channel_name', 'channel_group',
|
||||
'is_active', 'is_looping', 'shuffle_mode', 'tvg_name'
|
||||
)
|
||||
list_filter = ('channel_group', 'is_active', 'is_looping', 'shuffle_mode')
|
||||
search_fields = ('channel_name', 'channel_group__name', 'tvg_name')
|
||||
ordering = ('channel_number',)
|
||||
|
||||
@admin.register(ChannelGroup)
|
||||
class ChannelGroupAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
||||
24
apps/channels/api_urls.py
Normal file
24
apps/channels/api_urls.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .api_views import (
|
||||
StreamViewSet,
|
||||
ChannelViewSet,
|
||||
ChannelGroupViewSet,
|
||||
BulkDeleteStreamsAPIView,
|
||||
BulkDeleteChannelsViewSet
|
||||
)
|
||||
|
||||
app_name = 'channels' # for DRF routing
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'streams', StreamViewSet, basename='stream')
|
||||
router.register(r'groups', ChannelGroupViewSet, basename='channel-group')
|
||||
router.register(r'channels', ChannelViewSet, basename='channel')
|
||||
router.register(r'bulk-delete-channels', BulkDeleteChannelsViewSet, basename='bulk-delete-channels')
|
||||
|
||||
urlpatterns = [
|
||||
# Bulk delete for streams is a single APIView, not a ViewSet
|
||||
path('streams/bulk-delete/', BulkDeleteStreamsAPIView.as_view(), name='bulk_delete_streams'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
173
apps/channels/api_views.py
Normal file
173
apps/channels/api_views.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.decorators import action
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from .models import Stream, Channel, ChannelGroup
|
||||
from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 1) Stream API (CRUD)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class StreamViewSet(viewsets.ModelViewSet):
|
||||
queryset = Stream.objects.all()
|
||||
serializer_class = StreamSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
|
||||
assigned = self.request.query_params.get('assigned')
|
||||
if assigned is not None:
|
||||
# Streams that belong to a given channel?
|
||||
qs = qs.filter(channels__id=assigned)
|
||||
|
||||
unassigned = self.request.query_params.get('unassigned')
|
||||
if unassigned == '1':
|
||||
# Streams that are not linked to any channel
|
||||
qs = qs.filter(channels__isnull=True)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 2) Channel Group Management (CRUD)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class ChannelGroupViewSet(viewsets.ModelViewSet):
|
||||
queryset = ChannelGroup.objects.all()
|
||||
serializer_class = ChannelGroupSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 3) Channel Management (CRUD)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class ChannelViewSet(viewsets.ModelViewSet):
|
||||
queryset = Channel.objects.all()
|
||||
serializer_class = ChannelSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
method='post',
|
||||
operation_description="Auto-assign channel_number in bulk by an ordered list of channel IDs.",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["channel_order"],
|
||||
properties={
|
||||
"channel_order": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
description="List of channel IDs in the new order"
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={200: "Channels have been auto-assigned!"}
|
||||
)
|
||||
@action(detail=False, methods=['post'], url_path='assign')
|
||||
def assign(self, request):
|
||||
channel_order = request.data.get('channel_order', [])
|
||||
for order, channel_id in enumerate(channel_order, start=1):
|
||||
Channel.objects.filter(id=channel_id).update(channel_number=order)
|
||||
return Response({"message": "Channels have been auto-assigned!"}, status=status.HTTP_200_OK)
|
||||
|
||||
@swagger_auto_schema(
|
||||
method='post',
|
||||
operation_description=(
|
||||
"Create a new channel from an existing stream.\n"
|
||||
"Request body must contain: 'stream_id', 'channel_number', 'channel_name'."
|
||||
),
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["stream_id", "channel_number", "channel_name"],
|
||||
properties={
|
||||
"stream_id": openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER, description="ID of the stream to link"
|
||||
),
|
||||
"channel_number": openapi.Schema(
|
||||
type=openapi.TYPE_INTEGER, description="Desired channel_number"
|
||||
),
|
||||
"channel_name": openapi.Schema(
|
||||
type=openapi.TYPE_STRING, description="Desired channel name"
|
||||
)
|
||||
}
|
||||
),
|
||||
responses={201: ChannelSerializer()}
|
||||
)
|
||||
@action(detail=False, methods=['post'], url_path='from-stream')
|
||||
def from_stream(self, request):
|
||||
stream_id = request.data.get('stream_id')
|
||||
if not stream_id:
|
||||
return Response({"error": "Missing stream_id"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
stream = get_object_or_404(Stream, pk=stream_id)
|
||||
|
||||
channel_data = {
|
||||
'channel_number': request.data.get('channel_number', 0),
|
||||
'channel_name': request.data.get('channel_name', f"Channel from {stream.name}"),
|
||||
}
|
||||
serializer = self.get_serializer(data=channel_data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
channel = serializer.save()
|
||||
|
||||
# Optionally attach the stream to that channel
|
||||
channel.streams.add(stream)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 4) Bulk Delete Streams
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class BulkDeleteStreamsAPIView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Bulk delete streams by ID",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["stream_ids"],
|
||||
properties={
|
||||
"stream_ids": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
description="Stream IDs to delete"
|
||||
)
|
||||
},
|
||||
),
|
||||
responses={204: "Streams deleted"}
|
||||
)
|
||||
def delete(self, request, *args, **kwargs):
|
||||
stream_ids = request.data.get('stream_ids', [])
|
||||
Stream.objects.filter(id__in=stream_ids).delete()
|
||||
return Response({"message": "Streams deleted successfully!"}, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# 5) Bulk Delete Channels
|
||||
# ─────────────────────────────────────────────────────────
|
||||
class BulkDeleteChannelsViewSet(viewsets.ViewSet):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
@swagger_auto_schema(
|
||||
operation_description="Bulk delete channels by ID",
|
||||
request_body=openapi.Schema(
|
||||
type=openapi.TYPE_OBJECT,
|
||||
required=["channel_ids"],
|
||||
properties={
|
||||
"channel_ids": openapi.Schema(
|
||||
type=openapi.TYPE_ARRAY,
|
||||
items=openapi.Items(type=openapi.TYPE_INTEGER),
|
||||
description="Channel IDs to delete"
|
||||
)
|
||||
},
|
||||
),
|
||||
responses={204: "Channels deleted"}
|
||||
)
|
||||
def destroy(self, request):
|
||||
channel_ids = request.data.get('channel_ids', [])
|
||||
Channel.objects.filter(id__in=channel_ids).delete()
|
||||
return Response({"message": "Channels deleted"}, status=status.HTTP_204_NO_CONTENT)
|
||||
6
apps/channels/apps.py
Normal file
6
apps/channels/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
class ChannelsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.channels'
|
||||
verbose_name = "Channel & Stream Management"
|
||||
52
apps/channels/forms.py
Normal file
52
apps/channels/forms.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from django import forms
|
||||
from .models import Stream, Channel, ChannelGroup
|
||||
|
||||
#
|
||||
# ChannelGroup Form
|
||||
#
|
||||
class ChannelGroupForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ChannelGroup
|
||||
fields = ['name']
|
||||
|
||||
|
||||
#
|
||||
# Channel Form
|
||||
#
|
||||
class ChannelForm(forms.ModelForm):
|
||||
channel_group = forms.ModelChoiceField(
|
||||
queryset=ChannelGroup.objects.all(),
|
||||
required=False,
|
||||
label="Channel Group",
|
||||
empty_label="--- No group ---"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Channel
|
||||
fields = [
|
||||
'channel_number',
|
||||
'channel_name',
|
||||
'channel_group',
|
||||
'is_active',
|
||||
'is_looping',
|
||||
'shuffle_mode',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Example: Stream Form (optional if you want a ModelForm for Streams)
|
||||
#
|
||||
class StreamForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Stream
|
||||
fields = [
|
||||
'name',
|
||||
'url',
|
||||
'custom_url',
|
||||
'logo_url',
|
||||
'tvg_id',
|
||||
'local_file',
|
||||
'is_transcoded',
|
||||
'ffmpeg_preset',
|
||||
'group_name',
|
||||
]
|
||||
BIN
apps/channels/management/.DS_Store
vendored
Normal file
BIN
apps/channels/management/.DS_Store
vendored
Normal file
Binary file not shown.
0
apps/channels/management/__init__.py
Normal file
0
apps/channels/management/__init__.py
Normal file
BIN
apps/channels/management/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/channels/management/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
0
apps/channels/management/commands/__init__.py
Normal file
0
apps/channels/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
24
apps/channels/management/commands/remove_duplicates.py
Normal file
24
apps/channels/management/commands/remove_duplicates.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from apps.channels.models import Stream, Channel, ChannelGroup
|
||||
from apps.m3u.models import M3UAccount
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Delete all Channels, Streams, M3Us from the database (example)."
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
# Delete all Streams
|
||||
stream_count = Stream.objects.count()
|
||||
Stream.objects.all().delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {stream_count} Streams."))
|
||||
|
||||
# Or delete Channels:
|
||||
channel_count = Channel.objects.count()
|
||||
Channel.objects.all().delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {channel_count} Channels."))
|
||||
|
||||
# If you have M3UAccount:
|
||||
m3u_count = M3UAccount.objects.count()
|
||||
M3UAccount.objects.all().delete()
|
||||
self.stdout.write(self.style.SUCCESS(f"Deleted {m3u_count} M3U accounts."))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully deleted the requested objects."))
|
||||
63
apps/channels/migrations/0001_initial.py
Normal file
63
apps/channels/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
apps/channels/migrations/__init__.py
Normal file
0
apps/channels/migrations/__init__.py
Normal file
Binary file not shown.
BIN
apps/channels/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/channels/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
101
apps/channels/models.py
Normal file
101
apps/channels/models.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
# If you have an M3UAccount model in apps.m3u, you can still import it:
|
||||
from apps.m3u.models import M3UAccount
|
||||
|
||||
class Stream(models.Model):
|
||||
"""
|
||||
Represents a single stream (e.g. from an M3U source or custom URL).
|
||||
"""
|
||||
name = models.CharField(max_length=255, default="Default Stream")
|
||||
url = models.URLField()
|
||||
custom_url = models.URLField(max_length=2000, blank=True, null=True)
|
||||
m3u_account = models.ForeignKey(
|
||||
M3UAccount,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="streams"
|
||||
)
|
||||
logo_url = models.URLField(max_length=2000, blank=True, null=True)
|
||||
tvg_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
# If you use m3u_account, you might do unique_together = ('name','custom_url','m3u_account')
|
||||
verbose_name = "Stream"
|
||||
verbose_name_plural = "Streams"
|
||||
ordering = ['-updated_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.name or self.custom_url or f"Stream ID {self.id}"
|
||||
|
||||
|
||||
class ChannelManager(models.Manager):
|
||||
def active(self):
|
||||
return self.filter(is_active=True)
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
channel_number = models.IntegerField()
|
||||
channel_name = models.CharField(max_length=255)
|
||||
logo_url = models.URLField(max_length=2000, blank=True, null=True)
|
||||
logo_file = models.ImageField(
|
||||
upload_to='logos/', # Will store in MEDIA_ROOT/logos
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
|
||||
# M2M to Stream now in the same file
|
||||
streams = models.ManyToManyField(
|
||||
Stream,
|
||||
blank=True,
|
||||
related_name='channels'
|
||||
)
|
||||
|
||||
channel_group = models.ForeignKey(
|
||||
'ChannelGroup',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='channels',
|
||||
help_text="Channel group this channel belongs to."
|
||||
)
|
||||
tvg_id = models.CharField(max_length=255, blank=True, null=True)
|
||||
tvg_name = models.CharField(max_length=255, blank=True, 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.")
|
||||
|
||||
objects = ChannelManager()
|
||||
|
||||
def clean(self):
|
||||
# Enforce unique channel_number within a given group
|
||||
existing = Channel.objects.filter(
|
||||
channel_number=self.channel_number,
|
||||
channel_group=self.channel_group
|
||||
).exclude(id=self.id)
|
||||
if existing.exists():
|
||||
raise ValidationError(
|
||||
f"Channel number {self.channel_number} already exists in group {self.channel_group}."
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.channel_number} - {self.channel_name}"
|
||||
|
||||
|
||||
class ChannelGroup(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
|
||||
def related_channels(self):
|
||||
# local import if needed to avoid cyc. Usually fine in a single file though
|
||||
return Channel.objects.filter(channel_group=self)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
68
apps/channels/serializers.py
Normal file
68
apps/channels/serializers.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from rest_framework import serializers
|
||||
from .models import Stream, Channel, ChannelGroup
|
||||
|
||||
#
|
||||
# Stream
|
||||
#
|
||||
class StreamSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Stream
|
||||
fields = [
|
||||
'id',
|
||||
'name',
|
||||
'url',
|
||||
'custom_url',
|
||||
'm3u_account', # Uncomment if using M3U fields
|
||||
'logo_url',
|
||||
'tvg_id',
|
||||
'local_file',
|
||||
'current_viewers',
|
||||
'is_transcoded',
|
||||
'ffmpeg_preset',
|
||||
'updated_at',
|
||||
'group_name',
|
||||
]
|
||||
|
||||
|
||||
#
|
||||
# Channel Group
|
||||
#
|
||||
class ChannelGroupSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ChannelGroup
|
||||
fields = ['id', 'name']
|
||||
|
||||
|
||||
#
|
||||
# Channel
|
||||
#
|
||||
class ChannelSerializer(serializers.ModelSerializer):
|
||||
# Show nested group data, or ID
|
||||
channel_group = ChannelGroupSerializer(read_only=True)
|
||||
channel_group_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=ChannelGroup.objects.all(),
|
||||
source="channel_group",
|
||||
write_only=True,
|
||||
required=False
|
||||
)
|
||||
|
||||
# Possibly show streams inline, or just by ID
|
||||
# streams = StreamSerializer(many=True, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Channel
|
||||
fields = [
|
||||
'id',
|
||||
'channel_number',
|
||||
'channel_name',
|
||||
'logo_url',
|
||||
'logo_file',
|
||||
'channel_group',
|
||||
'channel_group_id',
|
||||
'tvg_id',
|
||||
'tvg_name',
|
||||
'is_active',
|
||||
'is_looping',
|
||||
'shuffle_mode',
|
||||
'streams'
|
||||
]
|
||||
12
apps/channels/urls.py
Normal file
12
apps/channels/urls.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from django.urls import path
|
||||
from .views import StreamDashboardView, channels_dashboard_view
|
||||
|
||||
app_name = 'channels_dashboard'
|
||||
|
||||
urlpatterns = [
|
||||
# Example “dashboard” routes for streams
|
||||
path('streams/', StreamDashboardView.as_view(), name='stream_dashboard'),
|
||||
|
||||
# Example “dashboard” route for channels
|
||||
path('channels/', channels_dashboard_view, name='channels_dashboard'),
|
||||
]
|
||||
25
apps/channels/utils.py
Normal file
25
apps/channels/utils.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import threading
|
||||
|
||||
lock = threading.Lock()
|
||||
# Dictionary to track usage: {account_id: current_usage}
|
||||
active_streams_map = {}
|
||||
|
||||
def increment_stream_count(account):
|
||||
with lock:
|
||||
current_usage = active_streams_map.get(account.id, 0)
|
||||
current_usage += 1
|
||||
active_streams_map[account.id] = current_usage
|
||||
account.active_streams = current_usage
|
||||
account.save(update_fields=['active_streams'])
|
||||
|
||||
def decrement_stream_count(account):
|
||||
with lock:
|
||||
current_usage = active_streams_map.get(account.id, 0)
|
||||
if current_usage > 0:
|
||||
current_usage -= 1
|
||||
if current_usage == 0:
|
||||
del active_streams_map[account.id]
|
||||
else:
|
||||
active_streams_map[account.id] = current_usage
|
||||
account.active_streams = current_usage
|
||||
account.save(update_fields=['active_streams'])
|
||||
44
apps/channels/views.py
Normal file
44
apps/channels/views.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from django.views import View
|
||||
from django.http import JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.shortcuts import render
|
||||
|
||||
from .models import Stream
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@method_decorator(login_required, name='dispatch')
|
||||
class StreamDashboardView(View):
|
||||
"""
|
||||
Example “dashboard” style view for Streams
|
||||
"""
|
||||
def get(self, request, *args, **kwargs):
|
||||
streams = Stream.objects.values(
|
||||
'id', 'name', 'url', 'custom_url',
|
||||
'group_name', 'current_viewers'
|
||||
)
|
||||
return JsonResponse({'data': list(streams)}, safe=False)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
Creates a new Stream from JSON data
|
||||
"""
|
||||
import json
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
new_stream = Stream.objects.create(**data)
|
||||
return JsonResponse({
|
||||
'id': new_stream.id,
|
||||
'message': 'Stream created successfully!'
|
||||
}, status=201)
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=400)
|
||||
|
||||
|
||||
@login_required
|
||||
def channels_dashboard_view(request):
|
||||
"""
|
||||
Example “dashboard” style view for Channels
|
||||
"""
|
||||
return render(request, 'channels/channels.html')
|
||||
BIN
apps/dashboard/.DS_Store
vendored
Normal file
BIN
apps/dashboard/.DS_Store
vendored
Normal file
Binary file not shown.
0
apps/dashboard/__init__.py
Normal file
0
apps/dashboard/__init__.py
Normal file
BIN
apps/dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/dashboard/__pycache__/admin.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/dashboard/__pycache__/apps.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/apps.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/dashboard/__pycache__/models.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/models.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/dashboard/__pycache__/urls.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/urls.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/dashboard/__pycache__/views.cpython-310.pyc
Normal file
BIN
apps/dashboard/__pycache__/views.cpython-310.pyc
Normal file
Binary file not shown.
2
apps/dashboard/admin.py
Normal file
2
apps/dashboard/admin.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from django.contrib import admin
|
||||
|
||||
8
apps/dashboard/api_urls.py
Normal file
8
apps/dashboard/api_urls.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
from .views import dashboard_view, settings_view, live_dashboard_data
|
||||
|
||||
app_name = 'dashboard'
|
||||
|
||||
urlpatterns = [
|
||||
path('dashboard-data/', live_dashboard_data, name='dashboard_data'),
|
||||
]
|
||||
6
apps/dashboard/apps.py
Normal file
6
apps/dashboard/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.dashboard'
|
||||
46
apps/dashboard/migrations/0001_initial.py
Normal file
46
apps/dashboard/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# 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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
apps/dashboard/migrations/__init__.py
Normal file
0
apps/dashboard/migrations/__init__.py
Normal file
Binary file not shown.
BIN
apps/dashboard/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/dashboard/migrations/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
66
apps/dashboard/models.py
Normal file
66
apps/dashboard/models.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from django.db import models
|
||||
|
||||
class Settings(models.Model):
|
||||
# General Settings
|
||||
server_name = models.CharField(max_length=255, default="Dispatcharr")
|
||||
time_zone = models.CharField(max_length=50, default="UTC")
|
||||
default_logo_url = models.URLField(blank=True, null=True)
|
||||
max_concurrent_streams = models.PositiveIntegerField(default=10)
|
||||
auto_backup_frequency = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("daily", "Daily"), ("weekly", "Weekly"), ("monthly", "Monthly")],
|
||||
default="weekly"
|
||||
)
|
||||
enable_debug_logs = models.BooleanField(default=False)
|
||||
|
||||
# Schedules Direct Settings
|
||||
schedules_direct_username = models.CharField(max_length=255, blank=True, null=True)
|
||||
schedules_direct_password = models.CharField(max_length=255, blank=True, null=True)
|
||||
schedules_direct_update_frequency = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("12h", "Every 12 Hours"), ("daily", "Daily")],
|
||||
default="daily"
|
||||
)
|
||||
schedules_direct_api_key = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# Stream and Channel Settings
|
||||
transcoding_bitrate = models.PositiveIntegerField(default=2000) # in kbps
|
||||
transcoding_audio_codec = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("aac", "AAC"), ("mp3", "MP3")],
|
||||
default="aac"
|
||||
)
|
||||
transcoding_resolution = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("720p", "720p"), ("1080p", "1080p")],
|
||||
default="1080p"
|
||||
)
|
||||
failover_behavior = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("sequential", "Sequential"), ("random", "Random")],
|
||||
default="sequential"
|
||||
)
|
||||
stream_health_check_frequency = models.PositiveIntegerField(default=5) # in minutes
|
||||
|
||||
# Notifications
|
||||
email_notifications = models.BooleanField(default=False)
|
||||
webhook_url = models.URLField(blank=True, null=True)
|
||||
cpu_alert_threshold = models.PositiveIntegerField(default=90) # Percentage
|
||||
memory_alert_threshold = models.PositiveIntegerField(default=90) # Percentage
|
||||
|
||||
# API Settings
|
||||
hdhr_integration = models.BooleanField(default=True)
|
||||
custom_api_endpoints = models.JSONField(blank=True, null=True)
|
||||
|
||||
# Backup and Restore
|
||||
backup_path = models.CharField(max_length=255, default="backups/")
|
||||
backup_frequency = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("daily", "Daily"), ("weekly", "Weekly"), ("monthly", "Monthly")],
|
||||
default="weekly"
|
||||
)
|
||||
|
||||
# Advanced
|
||||
ffmpeg_path = models.CharField(max_length=255, default="/usr/bin/ffmpeg")
|
||||
custom_transcoding_flags = models.TextField(blank=True, null=True)
|
||||
celery_worker_concurrency = models.PositiveIntegerField(default=4)
|
||||
3
apps/dashboard/tests.py
Normal file
3
apps/dashboard/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
apps/dashboard/urls.py
Normal file
10
apps/dashboard/urls.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from django.urls import path
|
||||
from .views import dashboard_view, settings_view, live_dashboard_data
|
||||
|
||||
app_name = 'dashboard'
|
||||
|
||||
urlpatterns = [
|
||||
path('', dashboard_view, name='dashboard'),
|
||||
path('settings/', settings_view, name='settings'),
|
||||
path('api/dashboard-data/', live_dashboard_data, name='dashboard_data'),
|
||||
]
|
||||
80
apps/dashboard/views.py
Normal file
80
apps/dashboard/views.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from psutil import cpu_percent, virtual_memory, net_io_counters
|
||||
from apps.channels.models import Stream
|
||||
from django.http import JsonResponse # ADD THIS LINE
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard_view(request):
|
||||
# Fetch system metrics
|
||||
try:
|
||||
cpu_usage = cpu_percent(interval=1)
|
||||
ram = virtual_memory()
|
||||
ram_usage = f"{ram.used / (1024 ** 3):.1f} GB / {ram.total / (1024 ** 3):.1f} GB"
|
||||
network = net_io_counters()
|
||||
network_traffic = f"{network.bytes_sent / (1024 ** 2):.1f} MB"
|
||||
except Exception as e:
|
||||
cpu_usage = "N/A"
|
||||
ram_usage = "N/A"
|
||||
network_traffic = "N/A"
|
||||
print(f"Error fetching system metrics: {e}")
|
||||
|
||||
# Fetch active streams and related channels
|
||||
active_streams = Stream.objects.filter(current_viewers__gt=0).prefetch_related('channels')
|
||||
active_streams_list = [
|
||||
f"Stream {i + 1}: {stream.custom_url or 'Unknown'} ({stream.current_viewers} viewers)"
|
||||
for i, stream in enumerate(active_streams)
|
||||
]
|
||||
|
||||
# Pass data to the template
|
||||
context = {
|
||||
"cpu_usage": f"{cpu_usage}%",
|
||||
"ram_usage": ram_usage,
|
||||
"current_streams": active_streams.count(),
|
||||
"network_traffic": network_traffic,
|
||||
"active_streams": active_streams_list,
|
||||
}
|
||||
return render(request, "dashboard/dashboard.html", context)
|
||||
|
||||
@login_required
|
||||
def settings_view(request):
|
||||
# Placeholder for settings functionality
|
||||
return render(request, 'settings.html')
|
||||
|
||||
@login_required
|
||||
def live_dashboard_data(request):
|
||||
try:
|
||||
cpu_usage = cpu_percent(interval=1)
|
||||
ram = virtual_memory()
|
||||
network = net_io_counters()
|
||||
ram_usage = f"{ram.used / (1024 ** 3):.1f} GB / {ram.total / (1024 ** 3):.1f} GB"
|
||||
network_traffic = f"{network.bytes_sent / (1024 ** 2):.1f} MB"
|
||||
|
||||
# Mocked example data for the charts
|
||||
cpu_data = [45, 50, 60, 55, 70, 65]
|
||||
ram_data = [6.5, 7.0, 7.5, 8.0, 8.5, 9.0]
|
||||
network_data = [120, 125, 130, 128, 126, 124]
|
||||
|
||||
active_streams = Stream.objects.filter(current_viewers__gt=0)
|
||||
active_streams_list = [
|
||||
f"Stream {i + 1}: {stream.custom_url or 'Unknown'} ({stream.current_viewers} viewers)"
|
||||
for i, stream in enumerate(active_streams)
|
||||
]
|
||||
|
||||
data = {
|
||||
"cpu_usage": f"{cpu_usage}%",
|
||||
"ram_usage": ram_usage,
|
||||
"current_streams": active_streams.count(),
|
||||
"network_traffic": network_traffic,
|
||||
"active_streams": active_streams_list,
|
||||
"cpu_data": cpu_data,
|
||||
"ram_data": ram_data,
|
||||
"network_data": network_data,
|
||||
}
|
||||
except Exception as e:
|
||||
data = {
|
||||
"error": str(e)
|
||||
}
|
||||
return JsonResponse(data)
|
||||
|
||||
BIN
apps/epg/.DS_Store
vendored
Normal file
BIN
apps/epg/.DS_Store
vendored
Normal file
Binary file not shown.
0
apps/epg/__init__.py
Normal file
0
apps/epg/__init__.py
Normal file
BIN
apps/epg/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
apps/epg/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/epg/__pycache__/admin.cpython-310.pyc
Normal file
BIN
apps/epg/__pycache__/admin.cpython-310.pyc
Normal file
Binary file not shown.
BIN
apps/epg/__pycache__/api_urls.cpython-310.pyc
Normal file
BIN
apps/epg/__pycache__/api_urls.cpython-310.pyc
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue