mirror of
https://github.com/wahyd4/links.git
synced 2026-08-08 21:04:53 +10:00
update video player!
This commit is contained in:
+78
-16
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user