update video player!

This commit is contained in:
2026-07-15 15:41:04 +10:00
parent 93f1a1ccae
commit d26f332ef9
+78 -16
View File
@@ -7,7 +7,7 @@ from pathlib import Path
from django.conf import settings
from django.db.models import F
from django.http import FileResponse, JsonResponse, Http404
from django.http import FileResponse, JsonResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.dateparse import parse_datetime
from django.views import View
@@ -47,6 +47,74 @@ def _save_uploaded_file(f):
)
def _stream_file(file_path, mime_type, disposition, range_header=None):
"""Return a streaming response for a media file that honours HTTP Range.
Browsers send a ``Range: bytes=start-end`` header to seek inside
``<video>`` / ``<audio>`` elements. Django's ``FileResponse`` ignores it,
so we parse the header ourselves and reply with ``206 Partial Content`` and
a ``Content-Range`` header. Without this, the media element can only play
sequentially and seeking fails.
"""
size = os.path.getsize(file_path)
# No Range header → serve the whole file (200 OK).
if not range_header or not range_header.startswith('bytes='):
response = FileResponse(open(file_path, 'rb'), content_type=mime_type)
response['Content-Length'] = size
response['Accept-Ranges'] = 'bytes'
response['Content-Disposition'] = disposition
return response
# Parse "bytes=start-end" (end optional; multiple ranges not supported here).
spec = range_header[len('bytes='):]
# Ignore multipart ranges; pick the first range.
if ',' in spec:
spec = spec.split(',', 1)[0].strip()
try:
start_str, end_str = spec.split('-', 1)
start = int(start_str) if start_str.strip() else 0
end = int(end_str) if end_str.strip() else size - 1
except (ValueError, IndexError):
response = HttpResponse(status=416) # Range Not Satisfiable
response['Content-Range'] = f'bytes */{size}'
return response
# Clamp to file bounds.
if start < 0:
start = 0
if end >= size:
end = size - 1
if start > end:
response = HttpResponse(status=416)
response['Content-Range'] = f'bytes */{size}'
return response
length = end - start + 1
def _range_iterator(_fh, _start, _length):
_fh.seek(_start)
remaining = _length
chunk_size = 64 * 1024
while remaining > 0:
read = min(chunk_size, remaining)
data = _fh.read(read)
if not data:
break
remaining -= len(data)
yield data
fh = open(file_path, 'rb')
response = FileResponse(_range_iterator(fh, start, length), content_type=mime_type, status=206)
response['Content-Length'] = length
response['Content-Range'] = f'bytes {start}-{end}/{size}'
response['Accept-Ranges'] = 'bytes'
response['Content-Disposition'] = disposition
# Ensure the file handle is closed when the response finalises.
response.close = fh.close
return response
# ── UI Views ──────────────────────────────────────────────────────────────────
class FileListView(View):
@@ -91,10 +159,9 @@ class FileDownloadView(View):
if not os.path.exists(record.file_path):
raise Http404("File not found on disk")
FileUpload.objects.filter(pk=pk).update(download_count=F('download_count') + 1)
disposition = 'inline' if (record.is_image or record.mime_type.startswith('video/')) else f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response
inline = record.is_image or record.mime_type.startswith('video/') or record.mime_type.startswith('audio/')
disposition = 'inline' if inline else f'attachment; filename="{record.name}"'
return _stream_file(record.file_path, record.mime_type, disposition, request.headers.get('Range'))
class FileDeleteView(View):
@@ -179,14 +246,10 @@ class PublicFileView(View):
FileUpload.objects.filter(pk=record.pk).update(download_count=F('download_count') + 1)
# Inline for media, attachment for everything else
if record.is_image or record.mime_type.startswith('video/'):
disposition = 'inline'
else:
disposition = f'attachment; filename="{record.name}"'
inline = record.is_image or record.mime_type.startswith('video/') or record.mime_type.startswith('audio/')
disposition = 'inline' if inline else f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response
return _stream_file(record.file_path, record.mime_type, disposition, request.headers.get('Range'))
# ── REST API ViewSet ──────────────────────────────────────────────────────────
@@ -246,10 +309,9 @@ class FileUploadViewSet(viewsets.ModelViewSet):
if not os.path.exists(record.file_path):
raise Http404("File not found")
FileUpload.objects.filter(pk=pk).update(download_count=F('download_count') + 1)
disposition = 'inline' if (record.is_image or record.mime_type.startswith('video/')) else f'attachment; filename="{record.name}"'
response = FileResponse(open(record.file_path, 'rb'), content_type=record.mime_type)
response['Content-Disposition'] = disposition
return response
inline = record.is_image or record.mime_type.startswith('video/') or record.mime_type.startswith('audio/')
disposition = 'inline' if inline else f'attachment; filename="{record.name}"'
return _stream_file(record.file_path, record.mime_type, disposition, request.headers.get('Range'))
def import_image_view(request, image_url):