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) {