fix: stream_video - encode Korean filename in Content-Disposition header for external players
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
title: "애니 다운로더"
|
title: "애니 다운로더"
|
||||||
version: "0.5.22"
|
version: "0.5.23"
|
||||||
package_name: "anime_downloader"
|
package_name: "anime_downloader"
|
||||||
developer: "projectdx"
|
developer: "projectdx"
|
||||||
description: "anime downloader"
|
description: "anime downloader"
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
return jsonify(self.install_system_browser())
|
return jsonify(self.install_system_browser())
|
||||||
|
|
||||||
elif sub == "stream_video":
|
elif sub == "stream_video":
|
||||||
# 비디오 스트리밍 (MP4 파일 직접 서빙)
|
# 비디오 스트리밍 (MP4 파일 직접 서빙) - 외부 플레이어(MXPlayer, VLC 등) 호환
|
||||||
try:
|
try:
|
||||||
from flask import send_file, Response
|
from flask import send_file, Response
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@@ -560,10 +560,30 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
if not file_path.startswith(download_path):
|
if not file_path.startswith(download_path):
|
||||||
return jsonify({"error": "Access denied"}), 403
|
return jsonify({"error": "Access denied"}), 403
|
||||||
|
|
||||||
# Range 요청 지원 (비디오 시킹)
|
|
||||||
file_size = os.path.getsize(file_path)
|
file_size = os.path.getsize(file_path)
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
mimetype = mimetypes.guess_type(file_path)[0] or 'video/mp4'
|
||||||
range_header = request.headers.get('Range', None)
|
range_header = request.headers.get('Range', None)
|
||||||
|
|
||||||
|
# 공통 헤더 (외부 플레이어 호환성)
|
||||||
|
# RFC 5987: 비ASCII 문자는 UTF-8 인코딩 필요
|
||||||
|
encoded_filename = urllib.parse.quote(filename)
|
||||||
|
common_headers = {
|
||||||
|
'Accept-Ranges': 'bytes',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Range, Content-Type',
|
||||||
|
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Accept-Ranges',
|
||||||
|
'Content-Disposition': f"inline; filename*=UTF-8''{encoded_filename}",
|
||||||
|
}
|
||||||
|
|
||||||
|
# OPTIONS 요청 처리 (CORS preflight)
|
||||||
|
if request.method == 'OPTIONS':
|
||||||
|
resp = Response('', status=200)
|
||||||
|
for k, v in common_headers.items():
|
||||||
|
resp.headers[k] = v
|
||||||
|
return resp
|
||||||
|
|
||||||
if range_header:
|
if range_header:
|
||||||
byte_start, byte_end = 0, None
|
byte_start, byte_end = 0, None
|
||||||
match = re.search(r'bytes=(\d+)-(\d*)', range_header)
|
match = re.search(r'bytes=(\d+)-(\d*)', range_header)
|
||||||
@@ -581,7 +601,7 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
f.seek(byte_start)
|
f.seek(byte_start)
|
||||||
remaining = length
|
remaining = length
|
||||||
while remaining > 0:
|
while remaining > 0:
|
||||||
chunk_size = min(8192, remaining)
|
chunk_size = min(65536, remaining) # 64KB chunks for better streaming
|
||||||
data = f.read(chunk_size)
|
data = f.read(chunk_size)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
@@ -591,15 +611,25 @@ class LogicOhli24(AnimeModuleBase):
|
|||||||
resp = Response(
|
resp = Response(
|
||||||
generate(),
|
generate(),
|
||||||
status=206,
|
status=206,
|
||||||
mimetype=mimetypes.guess_type(file_path)[0] or 'video/mp4',
|
mimetype=mimetype,
|
||||||
direct_passthrough=True
|
direct_passthrough=True
|
||||||
)
|
)
|
||||||
resp.headers.add('Content-Range', f'bytes {byte_start}-{byte_end}/{file_size}')
|
resp.headers['Content-Range'] = f'bytes {byte_start}-{byte_end}/{file_size}'
|
||||||
resp.headers.add('Accept-Ranges', 'bytes')
|
resp.headers['Content-Length'] = length
|
||||||
resp.headers.add('Content-Length', length)
|
for k, v in common_headers.items():
|
||||||
|
resp.headers[k] = v
|
||||||
return resp
|
return resp
|
||||||
else:
|
else:
|
||||||
return send_file(file_path, mimetype=mimetypes.guess_type(file_path)[0] or 'video/mp4')
|
# Non-range request - 전체 파일 전송
|
||||||
|
resp = send_file(
|
||||||
|
file_path,
|
||||||
|
mimetype=mimetype,
|
||||||
|
as_attachment=False,
|
||||||
|
download_name=filename
|
||||||
|
)
|
||||||
|
for k, v in common_headers.items():
|
||||||
|
resp.headers[k] = v
|
||||||
|
return resp
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Stream video error: {e}")
|
logger.error(f"Stream video error: {e}")
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ var VideoModal = (function() {
|
|||||||
{ name: 'nPlayer', img: imgBase + 'nplayer.webp', url: 'nplayer-' + streamUrl },
|
{ name: 'nPlayer', img: imgBase + 'nplayer.webp', url: 'nplayer-' + streamUrl },
|
||||||
{ name: 'Infuse', img: imgBase + 'infuse.webp', url: 'infuse://x-callback-url/play?url=' + streamUrl },
|
{ name: 'Infuse', img: imgBase + 'infuse.webp', url: 'infuse://x-callback-url/play?url=' + streamUrl },
|
||||||
{ name: 'OmniPlayer', img: imgBase + 'omniplayer.webp', url: 'omniplayer://weblink?url=' + streamUrl },
|
{ name: 'OmniPlayer', img: imgBase + 'omniplayer.webp', url: 'omniplayer://weblink?url=' + streamUrl },
|
||||||
{ name: 'MX Player', img: imgBase + 'mxplayer.webp', url: 'intent:' + streamUrl + '#Intent;package=com.mxtech.videoplayer.ad;S.title=' + encodeURIComponent(filename) + ';end' },
|
{ name: 'MX Player', img: imgBase + 'mxplayer.webp', url: 'intent:' + streamUrl + '#Intent;package=com.mxtech.videoplayer.ad;type=video/mp4;S.title=' + encodeURIComponent(filename) + ';end' },
|
||||||
{ name: 'MPV', img: imgBase + 'mpv.webp', url: 'mpv://' + doubleEncodedUrl },
|
{ name: 'MPV', img: imgBase + 'mpv.webp', url: 'mpv://' + doubleEncodedUrl },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -289,3 +289,21 @@ var VideoModal = (function() {
|
|||||||
getCurrentIndex: getCurrentIndex
|
getCurrentIndex: getCurrentIndex
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready (uses global package_name and sub variables)
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Use global variables if available (set by each page)
|
||||||
|
var pkgName = (typeof package_name !== 'undefined') ? package_name : 'anime_downloader';
|
||||||
|
var subName = (typeof sub !== 'undefined') ? sub : 'ohli24';
|
||||||
|
|
||||||
|
VideoModal.init({ package_name: pkgName, sub: subName });
|
||||||
|
|
||||||
|
// Auto-bind btn-watch click handler
|
||||||
|
$('body').on('click', '.btn-watch', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var filePath = $(this).data('path');
|
||||||
|
if (filePath) {
|
||||||
|
VideoModal.openWithPath(filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user