From 681fc0790c260452970855e5cedb43e001f6a486 Mon Sep 17 00:00:00 2001 From: projectdx Date: Wed, 31 Dec 2025 22:33:56 +0900 Subject: [PATCH] feat: Implement video playlist feature with navigation controls and auto-play. --- info.yaml | 2 +- mod_ohli24.py | 66 ++++++ templates/anime_downloader_ohli24_list.html | 250 ++++++++++++++++++-- 3 files changed, 299 insertions(+), 19 deletions(-) diff --git a/info.yaml b/info.yaml index 3852fd8..778e5c7 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "애니 다운로더" -version: "0.2.3" +version: "0.3.0" package_name: "anime_downloader" developer: "projectdx" description: "anime downloader" diff --git a/mod_ohli24.py b/mod_ohli24.py index a773f45..4bd3ee5 100644 --- a/mod_ohli24.py +++ b/mod_ohli24.py @@ -381,6 +381,72 @@ class LogicOhli24(PluginModuleBase): logger.error(f"Stream video error: {e}") logger.error(traceback.format_exc()) return jsonify({"error": str(e)}), 500 + + elif sub == "get_playlist": + # 현재 파일과 같은 폴더에서 다음 에피소드들 찾기 + try: + file_path = request.args.get("path", "") + if not file_path or not os.path.exists(file_path): + return jsonify({"error": "File not found", "playlist": [], "current_index": 0}), 404 + + # 보안 체크 + download_path = P.ModelSetting.get("ohli24_download_path") + if not file_path.startswith(download_path): + return jsonify({"error": "Access denied", "playlist": [], "current_index": 0}), 403 + + folder = os.path.dirname(file_path) + current_file = os.path.basename(file_path) + + # 파일명에서 SxxExx 패턴 추출 + ep_match = re.search(r'\.S(\d+)E(\d+)\.', current_file, re.IGNORECASE) + if not ep_match: + # 패턴 없으면 현재 파일만 반환 + return jsonify({ + "playlist": [{"path": file_path, "name": current_file}], + "current_index": 0 + }) + + current_season = int(ep_match.group(1)) + current_episode = int(ep_match.group(2)) + + # 같은 폴더의 모든 mp4 파일 가져오기 + all_files = [] + for f in os.listdir(folder): + if f.endswith('.mp4'): + match = re.search(r'\.S(\d+)E(\d+)\.', f, re.IGNORECASE) + if match: + s = int(match.group(1)) + e = int(match.group(2)) + all_files.append({ + "path": os.path.join(folder, f), + "name": f, + "season": s, + "episode": e + }) + + # 시즌/에피소드 순으로 정렬 + all_files.sort(key=lambda x: (x["season"], x["episode"])) + + # 현재 에피소드 이상인 것만 필터링 (현재 + 다음 에피소드들) + playlist = [] + current_index = 0 + for i, f in enumerate(all_files): + if f["season"] == current_season and f["episode"] >= current_episode: + entry = {"path": f["path"], "name": f["name"]} + if f["episode"] == current_episode: + current_index = len(playlist) + playlist.append(entry) + + logger.info(f"Playlist: {len(playlist)} items, current_index: {current_index}") + return jsonify({ + "playlist": playlist, + "current_index": current_index + }) + + except Exception as e: + logger.error(f"Get playlist error: {e}") + logger.error(traceback.format_exc()) + return jsonify({"error": str(e), "playlist": [], "current_index": 0}), 500 except Exception as e: P.logger.error(f"Exception: {e}") diff --git a/templates/anime_downloader_ohli24_list.html b/templates/anime_downloader_ohli24_list.html index 7cec5a6..5187150 100644 --- a/templates/anime_downloader_ohli24_list.html +++ b/templates/anime_downloader_ohli24_list.html @@ -50,7 +50,67 @@ + +
+ +
+ +
+
+
+
+ + +
+ + + +
+ + @@ -172,45 +232,199 @@ global_sub_request_search('1') }); - // 비디오 보기 버튼 클릭 핸들러 + // 비디오 보기 버튼 클릭 핸들러 (플레이리스트 지원) var videoPlayer = null; + var playlist = []; + var currentPlaylistIndex = 0; + + function playVideoAtIndex(index) { + if (index < 0 || index >= playlist.length) return; + currentPlaylistIndex = index; + var item = playlist[index]; + var streamUrl = '/' + package_name + '/ajax/' + sub + '/stream_video?path=' + encodeURIComponent(item.path); + + if (videoPlayer) { + videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + videoPlayer.play(); + } + + // 플레이리스트 UI 업데이트 (현재 파일명, 버튼 상태, 목록 표시) + updatePlaylistUI(); + } $("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 } + // 플레이리스트 API 호출 + $.ajax({ + url: '/' + package_name + '/ajax/' + sub + '/get_playlist?path=' + encodeURIComponent(filePath), + type: 'GET', + dataType: 'json', + success: function(data) { + playlist = data.playlist || []; + currentPlaylistIndex = data.current_index || 0; + currentPlayingPath = filePath; // 실시간 갱신용 경로 저장 + + var streamUrl = '/' + package_name + '/ajax/' + sub + '/stream_video?path=' + encodeURIComponent(filePath); + + // Video.js 초기화 + if (!videoPlayer) { + 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.on('ended', function() { + if (currentPlaylistIndex < playlist.length - 1) { + currentPlaylistIndex++; + playVideoAtIndex(currentPlaylistIndex); + } + }); } - }); - videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + + videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + + // 플레이리스트 UI 업데이트 + updatePlaylistUI(); + + // 모달 열기 + $('#videoModal').modal('show'); + }, + error: function() { + // 에러 시 기본 동작 + var streamUrl = '/' + package_name + '/ajax/' + sub + '/stream_video?path=' + encodeURIComponent(filePath); + if (!videoPlayer) { + videoPlayer = videojs('video-player', { controls: true, fluid: true }); + } + videoPlayer.src({ type: 'video/mp4', src: streamUrl }); + $('#videoModal').modal('show'); + } + }); + }); + + // 플레이리스트 UI 업데이트 함수 + function updatePlaylistUI() { + if (!playlist || playlist.length === 0) return; + + var currentFile = playlist[currentPlaylistIndex]; + $('#current-video-title').text(currentFile ? currentFile.name : ''); + $('#playlist-progress').text((currentPlaylistIndex + 1) + ' / ' + playlist.length + ' 에피소드'); + + // 이전/다음 버튼 표시 + if (currentPlaylistIndex > 0) { + $('#btn-prev-ep').show(); + } else { + $('#btn-prev-ep').hide(); + } + if (currentPlaylistIndex < playlist.length - 1) { + $('#btn-next-ep').show(); + } else { + $('#btn-next-ep').hide(); } - // 모달 열기 - $('#videoModal').modal('show'); + // 에피소드 목록 렌더링 + var listHtml = ''; + for (var i = 0; i < playlist.length; i++) { + var isActive = (i === currentPlaylistIndex) ? 'active' : ''; + listHtml += '
'; + listHtml += 'E' + (i + 1) + ''; + listHtml += '' + playlist[i].name + ''; + listHtml += '
'; + } + $('#playlist-list').html(listHtml); + } + + // 이전 에피소드 버튼 + $('#btn-prev-ep').click(function() { + if (currentPlaylistIndex > 0) { + currentPlaylistIndex--; + playVideoAtIndex(currentPlaylistIndex); + } + }); + + // 다음 에피소드 버튼 + $('#btn-next-ep').click(function() { + if (currentPlaylistIndex < playlist.length - 1) { + currentPlaylistIndex++; + playVideoAtIndex(currentPlaylistIndex); + } + }); + + // 목록 토글 버튼 + $('#btn-toggle-playlist').click(function() { + $(this).toggleClass('active'); + $('#playlist-list-container').slideToggle(200); + }); + + // 목록 아이템 클릭 + $(document).on('click', '.playlist-item', function() { + var index = parseInt($(this).data('index')); + if (index !== currentPlaylistIndex) { + currentPlaylistIndex = index; + playVideoAtIndex(index); + } }); // 모달 닫을 때 비디오 정지 + 포커스 해제 (aria-hidden 경고 방지) + var playlistRefreshInterval = null; + var currentPlayingPath = null; // 현재 재생 중인 파일 경로 저장 + + function startPlaylistRefresh() { + if (playlistRefreshInterval) return; + playlistRefreshInterval = setInterval(function() { + if (!currentPlayingPath) return; + $.ajax({ + url: '/' + package_name + '/ajax/' + sub + '/get_playlist?path=' + encodeURIComponent(currentPlayingPath), + type: 'GET', + dataType: 'json', + success: function(data) { + var newPlaylist = data.playlist || []; + // 새 에피소드 추가됐는지 확인 + if (newPlaylist.length > playlist.length) { + var addedCount = newPlaylist.length - playlist.length; + playlist = newPlaylist; + updatePlaylistUI(); + // 알림 표시 + $.notify(' ' + addedCount + '개 에피소드 추가됨!', {type: 'success'}); + } + } + }); + }, 10000); // 10초마다 체크 + } + + function stopPlaylistRefresh() { + if (playlistRefreshInterval) { + clearInterval(playlistRefreshInterval); + playlistRefreshInterval = null; + } + } + + $('#videoModal').on('show.bs.modal', function() { + startPlaylistRefresh(); + }); + $('#videoModal').on('hide.bs.modal', function () { + stopPlaylistRefresh(); // 포커스된 요소에서 포커스 해제 (aria-hidden 경고 방지) document.activeElement.blur(); + // 목록 닫기 + $('#playlist-list-container').hide(); + $('#btn-toggle-playlist').removeClass('active'); }); $('#videoModal').on('hidden.bs.modal', function () { if (videoPlayer) { videoPlayer.pause(); } + currentPlayingPath = null; }); function make_list(data) {