From 094b9ec73396ea4066ddb7f3d13fe5feb8917a3a Mon Sep 17 00:00:00 2001 From: projectdx Date: Wed, 31 Dec 2025 20:20:25 +0900 Subject: [PATCH] feat: Implement video playback for completed downloads via a modal player and a new streaming endpoint. --- .gitignore | 1 + mod_ohli24.py | 74 ++++++++++++- templates/anime_downloader_ohli24_list.html | 115 +++++++++++++++++++- 3 files changed, 182 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 2c05a5b..61b3d78 100644 --- a/.gitignore +++ b/.gitignore @@ -159,3 +159,4 @@ test.ipynb unanalyzed.py test.ipynb .DS_Store +test*.py diff --git a/mod_ohli24.py b/mod_ohli24.py index 8cfdfd7..e8c4e0c 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -286,12 +286,12 @@ class LogicOhli24(PluginModuleBase): thread.start() return jsonify("") elif sub == "web_list3": - print("web_list3") - print(request) - P.logger.debug(req) - P.logger.debug("web_list3") + # print("web_list3") + # print(request) + # P.logger.debug(req) + # P.logger.debug("web_list3") ret = ModelOhli24Item.web_list(req) - print(ret) + # print(ret) return jsonify(ret) elif sub == "web_list2": @@ -318,6 +318,70 @@ class LogicOhli24(PluginModuleBase): except Exception as e: logger.error(f"Exception: {e}") logger.error(traceback.format_exc()) + + elif sub == "stream_video": + # 비디오 스트리밍 (MP4 파일 직접 서빙) + try: + from flask import send_file, Response + import mimetypes + + file_path = request.args.get("path", "") + logger.info(f"Stream video request: {file_path}") + + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found"}), 404 + + # 보안 체크: 다운로드 폴더 내부인지 확인 + download_path = P.ModelSetting.get("ohli24_download_path") + if not file_path.startswith(download_path): + return jsonify({"error": "Access denied"}), 403 + + # Range 요청 지원 (비디오 시킹) + file_size = os.path.getsize(file_path) + range_header = request.headers.get('Range', None) + + if range_header: + byte_start, byte_end = 0, None + match = re.search(r'bytes=(\d+)-(\d*)', range_header) + if match: + byte_start = int(match.group(1)) + byte_end = int(match.group(2)) if match.group(2) else file_size - 1 + + if byte_end is None or byte_end >= file_size: + byte_end = file_size - 1 + + length = byte_end - byte_start + 1 + + def generate(): + with open(file_path, 'rb') as f: + f.seek(byte_start) + remaining = length + while remaining > 0: + chunk_size = min(8192, remaining) + data = f.read(chunk_size) + if not data: + break + remaining -= len(data) + yield data + + resp = Response( + generate(), + status=206, + mimetype=mimetypes.guess_type(file_path)[0] or 'video/mp4', + direct_passthrough=True + ) + resp.headers.add('Content-Range', f'bytes {byte_start}-{byte_end}/{file_size}') + resp.headers.add('Accept-Ranges', 'bytes') + resp.headers.add('Content-Length', length) + return resp + else: + return send_file(file_path, mimetype=mimetypes.guess_type(file_path)[0] or 'video/mp4') + + except Exception as e: + logger.error(f"Stream video error: {e}") + logger.error(traceback.format_exc()) + return jsonify({"error": str(e)}), 500 + except Exception as e: P.logger.error(f"Exception: {e}") P.logger.error(traceback.format_exc()) diff --git a/templates/anime_downloader_ohli24_list.html b/templates/anime_downloader_ohli24_list.html index b401629..7cec5a6 100644 --- a/templates/anime_downloader_ohli24_list.html +++ b/templates/anime_downloader_ohli24_list.html @@ -31,6 +31,29 @@
+ + + + + + + @@ -149,6 +172,46 @@ global_sub_request_search('1') }); + // 비디오 보기 버튼 클릭 핸들러 + var videoPlayer = null; + + $("body").on('click', '.btn-watch', function (e) { + e.preventDefault(); + var filePath = $(this).data('path'); + var streamUrl = '/' + package_name + '/ajax/' + sub + '/stream_video?path=' + encodeURIComponent(filePath); + + // Video.js 초기화 또는 소스 변경 + if (videoPlayer) { + videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + } else { + videoPlayer = videojs('video-player', { + controls: true, + autoplay: false, + preload: 'auto', + fluid: true, + playbackRates: [0.5, 1, 1.5, 2], + controlBar: { + skipButtons: { forward: 10, backward: 10 } + } + }); + videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + } + + // 모달 열기 + $('#videoModal').modal('show'); + }); + + // 모달 닫을 때 비디오 정지 + 포커스 해제 (aria-hidden 경고 방지) + $('#videoModal').on('hide.bs.modal', function () { + // 포커스된 요소에서 포커스 해제 (aria-hidden 경고 방지) + document.activeElement.blur(); + }); + + $('#videoModal').on('hidden.bs.modal', function () { + if (videoPlayer) { + videoPlayer.pause(); + } + }); function make_list(data) { let str = ''; @@ -219,6 +282,7 @@ + ${item.status == 'completed' ? `` : ''} @@ -332,9 +396,9 @@ /* General Layout Tweaks */ .container-fluid { - padding-left: 10px !important; - padding-right: 10px !important; - max-width: 1600px; + padding-left: 4px !important; + padding-right: 4px !important; + max-width: 100%; margin: 0 auto; } @@ -645,6 +709,13 @@ $(document).ready(function(){ }); {% endblock %} \ No newline at end of file