'use strict'; (() => { // ==================== // DOM 요소 참조 // ==================== const searchKeyword = document.getElementById('search_keyword'); const searchBtn = document.getElementById('search_btn'); const searchResults = document.getElementById('search_results'); const sentinel = document.getElementById('sentinel'); const sentinelLoading = document.getElementById('sentinel_loading'); const playerWrapper = document.getElementById('player-wrapper'); const initialMessage = document.getElementById('initial_message'); // ==================== // 상태 변수 // ==================== let currentPage = 1; let isLoading = false; let hasMore = true; let art = null; let lastPreviewUrl = ''; // ==================== // AJAX 헬퍼 // ==================== const postAjax = (url, data) => { return fetch(`/${package_name}/ajax${url}`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: new URLSearchParams(data), }) .then(res => res.json()) .then(ret => { if (ret.msg) notify(ret.msg, ret.ret); return ret; }) .catch(err => { console.error('[YouTube-DL] AJAX Error:', err); notify('요청 실패', 'danger'); return { ret: 'error' }; }); }; // ==================== // 유틸리티 함수 // ==================== const formatDuration = (seconds) => { if (!seconds) return '--:--'; const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hrs > 0) return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; return `${mins}:${secs.toString().padStart(2, '0')}`; }; const formatUploadDate = (item) => { const dateVal = item.upload_date || item.publication_date || item.date; if (!dateVal) return ''; const dateStr = String(dateVal); if (dateStr.length === 8 && /^\d+$/.test(dateStr)) { return `${dateStr.slice(0, 4)}.${dateStr.slice(4, 6)}.${dateStr.slice(6, 8)}`; } return dateStr; }; const getBestThumbnail = (item) => { if (item.thumbnail && typeof item.thumbnail === 'string') return item.thumbnail; if (item.thumbnails && item.thumbnails.length > 0) { const sorted = [...item.thumbnails].sort((a, b) => (b.width || 0) - (a.width || 0)); return sorted[0].url; } return '/static/img/no_image.png'; }; // ==================== // 플레이어 초기화 // ==================== const initArtplayer = (videoUrl) => { playerWrapper.style.display = 'block'; if (art) { art.switchUrl(videoUrl); return; } art = new Artplayer({ container: '#player-wrapper', url: videoUrl, autoplay: true, muted: false, volume: 1.0, autoSize: false, // 컨테이너 크기에 맞춤 aspectRatio: true, // 16:9 비율 유지 pip: true, setting: true, playbackRate: true, fullscreen: true, fullscreenWeb: true, theme: '#38bdf8', autoMini: true, // 스크롤 시 미니 플레이어로 자동 전환 customType: { m3u8: (video, url) => { if (Hls.isSupported()) { const hls = new Hls(); hls.loadSource(url); hls.attachMedia(video); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = url; } } } }); }; // ==================== // 결과 카드 생성 // ==================== const makeResultCard = (item) => { const videoId = item.id || item.url; const url = (item.url && item.url.startsWith('http')) ? item.url : `https://www.youtube.com/watch?v=${videoId}`; const thumbnail = getBestThumbnail(item); const duration = formatDuration(item.duration); const uploader = item.uploader || item.channel || ''; const uploadDate = formatUploadDate(item); return `
${item.title}
${duration}
${item.title}
${uploader ? `
${uploader}
` : ''} ${uploadDate ? `
${uploadDate}
` : ''}
`; }; // ==================== // 검색 결과 렌더링 // ==================== const renderResults = (data) => { const fragment = document.createDocumentFragment(); data.forEach(item => { if (!item) return; // null 아이템 스킵 const col = document.createElement('div'); col.className = 'col-12 col-sm-6 col-lg-4 col-xl-3 mb-4'; col.innerHTML = makeResultCard(item); // 이벤트 바인딩 col.querySelector('.download-video-btn')?.addEventListener('click', (e) => { e.preventDefault(); triggerDownload(e.currentTarget); }); col.querySelector('.preview-trigger')?.addEventListener('click', (e) => { e.preventDefault(); triggerPreview(e.currentTarget); }); fragment.appendChild(col); }); searchResults.appendChild(fragment); }; // ==================== // 검색 수행 // ==================== const performSearch = (isNew = true) => { const keyword = searchKeyword.value.trim(); if (!keyword) { if (isNew) notify('검색어를 입력하세요.', 'warning'); return; } // 이미 로딩 중이면 무시 if (isLoading) return; // 새 검색 시 상태 초기화 if (isNew) { currentPage = 1; hasMore = true; searchResults.innerHTML = `

유튜브 검색 중...

`; playerWrapper.style.display = 'none'; if (art) { art.destroy(); art = null; } } else { if (!hasMore) return; if (sentinelLoading) sentinelLoading.style.display = 'block'; } isLoading = true; console.log(`[YouTube-DL] Search: "${keyword}" (Page ${currentPage})`); postAjax('/basic/search', { keyword, page: currentPage }) .then(ret => { // 새 검색이면 기존 로딩 스피너 제거 if (isNew) searchResults.innerHTML = ''; // 초기 안내 메시지 숨기기 if (initialMessage) initialMessage.style.display = 'none'; if (ret.ret === 'success' && ret.data && ret.data.length > 0) { renderResults(ret.data); // 다음 페이지 체크 if (ret.data.length < 20) { hasMore = false; } else { currentPage++; } } else { if (isNew) { searchResults.innerHTML = `

검색 결과가 없습니다.

`; } hasMore = false; } }) .finally(() => { isLoading = false; if (sentinelLoading) sentinelLoading.style.display = 'none'; }); }; // ==================== // 미리보기 & 다운로드 // ==================== const triggerPreview = (el) => { const targetUrl = el.dataset.url; if (!targetUrl) return; if (targetUrl === lastPreviewUrl && art) { window.scrollTo({ top: 0, behavior: 'smooth' }); art.play(); return; } lastPreviewUrl = targetUrl; postAjax('/basic/preview', { url: targetUrl }).then(ret => { if (ret.ret === 'success' && ret.data) { initArtplayer(ret.data); window.scrollTo({ top: 0, behavior: 'smooth' }); } }); }; const triggerDownload = (btn) => { postAjax('/basic/download', { url: btn.dataset.url, filename: '%(title)s-%(id)s.%(ext)s', format: 'bestvideo+bestaudio/best', postprocessor: '' }).then(res => { if (res.ret === 'success' || res.ret === 'info') { btn.disabled = true; btn.innerHTML = '추가됨'; btn.classList.replace('btn-primary', 'btn-success'); } }); }; // ==================== // 인피니티 스크롤 (단순화) // ==================== // 1초마다 sentinel 위치 체크 (가장 확실한 방법) setInterval(() => { if (isLoading || !hasMore) return; if (!sentinel) return; const rect = sentinel.getBoundingClientRect(); // sentinel이 화면 아래 800px 이내에 들어오면 다음 페이지 로드 if (rect.top < window.innerHeight + 800) { console.log('[YouTube-DL] Loading next page...'); performSearch(false); } }, 1000); // ==================== // 이벤트 바인딩 // ==================== searchBtn.addEventListener('click', (e) => { e.preventDefault(); performSearch(true); }); searchKeyword.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(true); }); })();