77from server .dependencies import CurrentUser
88from server .request import APIRequest
99
10+ MAX_200_SIZE = 1024 * 1024 * 12
11+
1012
1113class ProxyResponse (Response ):
1214 content_type = "video/mp4"
1315
1416
17+ def get_file_size (file_name : str ) -> int :
18+ """Get the size of a file"""
19+ if not os .path .exists (file_name ):
20+ raise nebula .NotFoundException ("File not found" )
21+ return os .stat (file_name ).st_size
22+
23+
1524async def get_bytes_range (file_name : str , start : int , end : int ) -> bytes :
1625 """Get a range of bytes from a file"""
1726 async with aiofiles .open (file_name , mode = "rb" ) as f :
1827 await f .seek (start )
1928 pos = start
20- # read_size = min(CHUNK_SIZE, end - pos + 1)
2129 read_size = end - pos + 1
2230 return await f .read (read_size )
2331
2432
2533def _get_range_header (range_header : str , file_size : int ) -> tuple [int , int ]:
34+ """
35+ Parse the Range header to determine the start and end byte positions.
36+
37+ Args:
38+ range_header (str): The value of the Range header from the HTTP request.
39+ file_size (int): The total size of the file in bytes.
40+
41+ Returns:
42+ tuple[int, int]: A tuple containing the start and end byte positions.
43+
44+ Raises:
45+ HTTPException: If the range is invalid or cannot be parsed.
46+ """
47+
2648 def _invalid_range () -> HTTPException :
2749 return HTTPException (
2850 status .HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE ,
@@ -45,15 +67,24 @@ async def range_requests_response(
4567 request : Request , file_path : str , content_type : str
4668) -> ProxyResponse :
4769 """Returns StreamingResponse using Range Requests of a given file"""
48- file_size = os .stat (file_path ).st_size
49- max_chunk_size = 1024 * 1024 # 2MB
70+
71+ file_size = get_file_size (file_path )
72+
73+ max_chunk_size = 1024 * 1024 * 4
5074 range_header = request .headers .get ("range" )
75+ max_200_size = MAX_200_SIZE
76+
77+ # screw firefox
78+ if ua := request .headers .get ("user-agent" ):
79+ if "firefox" in ua .lower ():
80+ max_chunk_size = file_size
81+ elif "safari" in ua .lower ():
82+ max_200_size = 0
5183
5284 headers = {
5385 "content-type" : content_type ,
54- "accept-ranges" : "bytes" ,
55- "content-encoding" : "identity" ,
5686 "content-length" : str (file_size ),
87+ "accept-ranges" : "bytes" ,
5788 "access-control-expose-headers" : (
5889 "content-type, accept-ranges, content-length, "
5990 "content-range, content-encoding"
@@ -63,16 +94,31 @@ async def range_requests_response(
6394 end = file_size - 1
6495 status_code = status .HTTP_200_OK
6596
66- if range_header is not None :
97+ if file_size <= max_200_size :
98+ # if the file has a sane size, we return the whole thing
99+ # in one go. That allows the browser to cache the video
100+ # and prevent unnecessary requests.
101+
102+ headers ["content-range" ] = f"bytes 0-{ end } /{ file_size } "
103+
104+ elif range_header is not None :
67105 start , end = _get_range_header (range_header , file_size )
68- end = min (end , start + max_chunk_size - 1 )
106+ end = min (end , start + max_chunk_size - 1 , file_size - 1 )
107+
69108 size = end - start + 1
70109 headers ["content-length" ] = str (size )
71110 headers ["content-range" ] = f"bytes { start } -{ end } /{ file_size } "
72- status_code = status .HTTP_206_PARTIAL_CONTENT
111+
112+ if size == file_size :
113+ status_code = status .HTTP_200_OK
114+ else :
115+ status_code = status .HTTP_206_PARTIAL_CONTENT
73116
74117 payload = await get_bytes_range (file_path , start , end )
75118
119+ if status_code == status .HTTP_200_OK :
120+ headers ["cache-control" ] = "private, max-age=600"
121+
76122 return ProxyResponse (
77123 content = payload ,
78124 headers = headers ,
@@ -87,9 +133,9 @@ class ServeProxy(APIRequest):
87133 the file in media players that support HTTPS pseudo-streaming.
88134 """
89135
90- name : str = "proxy"
91- path : str = "/proxy/{id_asset}"
92- title : str = "Serve proxy"
136+ name = "proxy"
137+ path = "/proxy/{id_asset}"
138+ title = "Serve proxy"
93139 methods = ["GET" ]
94140
95141 async def handle (
@@ -98,7 +144,6 @@ async def handle(
98144 id_asset : int ,
99145 user : CurrentUser ,
100146 ) -> ProxyResponse :
101- assert user
102147 sys_settings = nebula .settings .system
103148 proxy_storage_path = nebula .storages [sys_settings .proxy_storage ].local_path
104149 proxy_path_template = os .path .join (proxy_storage_path , sys_settings .proxy_path )
0 commit comments