mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-18 00:55:50 +00:00
Merge branch 'dev' of https://github.com/Dispatcharr/Dispatcharr into dev
This commit is contained in:
commit
a7fa6ff55e
8 changed files with 67 additions and 25 deletions
|
|
@ -18,8 +18,8 @@ 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'logos', LogoViewSet, basename='logos')
|
||||
router.register(r'profiles', ChannelProfileViewSet, basename='profiles')
|
||||
router.register(r'logos', LogoViewSet, basename='logo')
|
||||
router.register(r'profiles', ChannelProfileViewSet, basename='profile')
|
||||
|
||||
urlpatterns = [
|
||||
# Bulk delete is a single APIView, not a ViewSet
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
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.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_yasg import openapi
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db import transaction
|
||||
import os, json
|
||||
import os, json, requests
|
||||
|
||||
from .models import Stream, Channel, ChannelGroup, Logo, ChannelProfile, ChannelProfileMembership
|
||||
from .serializers import StreamSerializer, ChannelSerializer, ChannelGroupSerializer, LogoSerializer, ChannelProfileMembershipSerializer, BulkChannelProfileMembershipSerializer, ChannelProfileSerializer
|
||||
|
|
@ -18,6 +18,8 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from apps.epg.models import EPGData
|
||||
from django.db.models import Q
|
||||
from django.http import StreamingHttpResponse, FileResponse, Http404
|
||||
|
||||
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
|
@ -490,6 +492,26 @@ class LogoViewSet(viewsets.ModelViewSet):
|
|||
|
||||
return Response({'id': logo.id, 'name': logo.name, 'url': logo.url}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=True, methods=['get'], permission_classes=[AllowAny])
|
||||
def cache(self, request, pk=None):
|
||||
"""Streams the logo file, whether it's local or remote."""
|
||||
logo = self.get_object()
|
||||
logo_url = logo.url
|
||||
|
||||
if logo_url.startswith("/data"): # Local file
|
||||
if not os.path.exists(logo_url):
|
||||
raise Http404("Image not found")
|
||||
return FileResponse(open(logo_url, "rb"), content_type="image/*")
|
||||
|
||||
else: # Remote image
|
||||
try:
|
||||
remote_response = requests.get(logo_url, stream=True)
|
||||
if remote_response.status_code == 200:
|
||||
return StreamingHttpResponse(remote_response.iter_content(chunk_size=8192), content_type="image/*")
|
||||
raise Http404("Remote image not found")
|
||||
except requests.RequestException:
|
||||
raise Http404("Error fetching remote image")
|
||||
|
||||
class ChannelProfileViewSet(viewsets.ModelViewSet):
|
||||
queryset = ChannelProfile.objects.all()
|
||||
serializer_class = ChannelProfileSerializer
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@ from .models import Stream, Channel, ChannelGroup, ChannelStream, ChannelGroupM3
|
|||
from apps.epg.serializers import EPGDataSerializer
|
||||
from core.models import StreamProfile
|
||||
from apps.epg.models import EPGData
|
||||
from django.urls import reverse
|
||||
|
||||
class LogoSerializer(serializers.ModelSerializer):
|
||||
cache_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Logo
|
||||
fields = ['id', 'name', 'url', 'cache_url']
|
||||
|
||||
def get_cache_url(self, obj):
|
||||
# return f"/api/channels/logos/{obj.id}/cache/"
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
return request.build_absolute_uri(reverse('api:channels:logo-cache', args=[obj.id])) # Use 'logo-cache'
|
||||
return reverse('api:channels:logo-cache', args=[obj.id])
|
||||
|
||||
#
|
||||
# Stream
|
||||
|
|
@ -126,7 +141,7 @@ class ChannelSerializer(serializers.ModelSerializer):
|
|||
queryset=Stream.objects.all(), many=True, write_only=True, required=False
|
||||
)
|
||||
|
||||
logo = serializers.SerializerMethodField()
|
||||
logo = LogoSerializer(read_only=True)
|
||||
logo_id = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Logo.objects.all(),
|
||||
source='logo',
|
||||
|
|
@ -215,8 +230,3 @@ class ChannelGroupM3UAccountSerializer(serializers.ModelSerializer):
|
|||
|
||||
# Optionally, if you only need the id of the ChannelGroup, you can customize it like this:
|
||||
# channel_group = serializers.PrimaryKeyRelatedField(queryset=ChannelGroup.objects.all())
|
||||
|
||||
class LogoSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Logo
|
||||
fields = ['id', 'name', 'url']
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
proxy_cache_path /app/logo_cache levels=1:2 keys_zone=logo_cache:10m
|
||||
inactive=24h use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 9191;
|
||||
|
||||
|
|
@ -26,6 +29,14 @@ server {
|
|||
root /data;
|
||||
}
|
||||
|
||||
location /api/logos/(?<logo_id>\d+)/cache/ {
|
||||
proxy_pass http://127.0.0.1:5656;
|
||||
proxy_cache logo_cache;
|
||||
proxy_cache_key "$scheme$request_uri"; # Cache per logo URL
|
||||
proxy_cache_valid 200 24h; # Cache for 24 hours
|
||||
proxy_cache_use_stale error timeout updating; # Serve stale if Django is slow
|
||||
}
|
||||
|
||||
# admin disabled when not in dev mode
|
||||
location /admin {
|
||||
return 301 /login;
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
if (files.length === 1) {
|
||||
console.log(files[0]);
|
||||
const retval = await API.uploadLogo(files[0]);
|
||||
setLogoPreview(retval.url);
|
||||
setLogoPreview(retval.cache_url);
|
||||
formik.setFieldValue('logo_id', retval.id);
|
||||
} else {
|
||||
setLogoPreview(null);
|
||||
|
|
@ -150,7 +150,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
const renderLogoOption = ({ option, checked }) => {
|
||||
return (
|
||||
<Center style={{ width: '100%' }}>
|
||||
<img src={logos[option.value].url} width="30" />
|
||||
<img src={logos[option.value].cache_url} width="30" />
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
|
@ -402,7 +402,7 @@ const Channel = ({ channel = null, isOpen, onClose }) => {
|
|||
<img
|
||||
src={
|
||||
logos[formik.values.logo_id]
|
||||
? logos[formik.values.logo_id].url
|
||||
? logos[formik.values.logo_id].cache_url
|
||||
: logo
|
||||
}
|
||||
height="40"
|
||||
|
|
|
|||
|
|
@ -327,6 +327,9 @@ const ChannelsTable = ({}) => {
|
|||
return selectedProfileChannels.find((channel) => row.id == channel.id)
|
||||
.enabled;
|
||||
},
|
||||
mantineTableBodyCellProps: {
|
||||
align: 'center',
|
||||
},
|
||||
Cell: ({ row, cell }) => (
|
||||
<Switch
|
||||
size="xs"
|
||||
|
|
@ -407,7 +410,7 @@ const ChannelsTable = ({}) => {
|
|||
},
|
||||
{
|
||||
header: 'Logo',
|
||||
accessorKey: 'logo.url',
|
||||
accessorKey: 'logo',
|
||||
enableSorting: false,
|
||||
size: 55,
|
||||
mantineTableBodyCellProps: {
|
||||
|
|
@ -425,9 +428,7 @@ const ChannelsTable = ({}) => {
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
cell.getValue() ? cell.getValue().replace(/^\/data/, '') : logo
|
||||
}
|
||||
src={cell.getValue() ? cell.getValue().cache_url : logo}
|
||||
height="20"
|
||||
alt="channel logo"
|
||||
/>
|
||||
|
|
@ -880,12 +881,10 @@ const ChannelsTable = ({}) => {
|
|||
size="xs"
|
||||
value={selectedProfileId}
|
||||
onChange={setSelectedProfileId}
|
||||
data={[{ label: 'All', value: '0' }].concat(
|
||||
Object.values(profiles).map((profile) => ({
|
||||
label: profile.name,
|
||||
value: `${profile.id}`,
|
||||
}))
|
||||
)}
|
||||
data={Object.values(profiles).map((profile) => ({
|
||||
label: profile.name,
|
||||
value: `${profile.id}`,
|
||||
}))}
|
||||
renderOption={renderProfileOption}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ export default function TVChannelGuide({ startDate, endDate }) {
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={channel.logo.url.replace(/^\/data/, '') || logo}
|
||||
src={channel.logo.cache_url || logo}
|
||||
alt={channel.name}
|
||||
style={{
|
||||
width: '100%',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { create } from 'zustand';
|
|||
import api from '../api';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
const defaultProfiles = { 0: { name: 'All', channels: [] } };
|
||||
const defaultProfiles = { 0: { id: '0', name: 'All', channels: [] } };
|
||||
|
||||
const useChannelsStore = create((set, get) => ({
|
||||
channels: [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue