diff --git a/README.md b/README.md index a8419cb..5d58aa8 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,15 @@ API를 제공합니다. 다른 플러그인에서 동영상 정보나 다운로 ## Changelog +v0.1.2 + +- 유튜브 검색 속도 대폭 개선 (extract_flat 적용) +- 검색 결과 캐싱 추가 (5분 유지) +- 인피니티 스크롤 안정화 및 최적화 +- 미니 플레이어 (스크롤 시 오른쪽 하단 고정) 추가 +- Artplayer 영상 비율 버그 수정 (16:9 aspect-ratio 적용) +- UI 개선: 검색 후 초기 메시지 자동 숨김 + v0.1.1 - 유지보수 업데이트 diff --git a/info.yaml b/info.yaml index d43da42..7ddd5df 100644 --- a/info.yaml +++ b/info.yaml @@ -1,5 +1,5 @@ title: "유튜브 다운로더" -version: "0.1.1" +version: "0.1.2" package_name: "youtube-dl" developer: "flaskfarm" description: "유튜브 다운로드" diff --git a/mod_basic.py b/mod_basic.py index ed0da95..ad8d171 100644 --- a/mod_basic.py +++ b/mod_basic.py @@ -75,6 +75,16 @@ class ModuleBasic(PluginModuleBase): arg["preset_list"] = self.get_preset_list() arg["postprocessor_list"] = self.get_postprocessor_list() + elif sub in ["thumbnail", "sub", "search"]: + default_filename: Optional[str] = P.ModelSetting.get("default_filename") + arg["filename"] = ( + default_filename + if default_filename + else self.get_default_filename() + ) + # These templates don't have the module prefix in their name + return render_template(f"{P.package_name}_{sub}.html", arg=arg) + return render_template(f"{P.package_name}_{self.name}_{sub}.html", arg=arg) def plugin_load(self) -> None: @@ -271,6 +281,54 @@ class ModuleBasic(PluginModuleBase): ret["ret"] = "warning" ret["msg"] = "미리보기 URL을 가져올 수 없습니다." + elif sub == "search": + keyword: str = req.form["keyword"] + page: int = int(req.form.get("page", 1)) + page_size: int = 20 + + # 캐시 키 및 만료 시간 (5분) + import time + cache_key = f"search:{keyword}" + cache_expiry = 300 # 5분 + + # 캐시에서 결과 확인 + cached = getattr(self, '_search_cache', {}).get(cache_key) + current_time = time.time() + + if cached and current_time - cached['time'] < cache_expiry: + all_results = cached['data'] + else: + # 새 검색 수행 - 최대 100개 결과 가져오기 + search_url = f"ytsearch100:{keyword}" if not keyword.startswith('http') else keyword + + search_data = MyYoutubeDL.get_info_dict( + search_url, + proxy=P.ModelSetting.get("proxy"), + ) + + if search_data and 'entries' in search_data: + all_results = [r for r in search_data['entries'] if r] # None 제거 + # 캐시에 저장 + if not hasattr(self, '_search_cache'): + self._search_cache = {} + self._search_cache[cache_key] = {'data': all_results, 'time': current_time} + else: + all_results = [] + + if all_results: + # 현재 페이지에 해당하는 결과만 슬라이싱 + start_idx = (page - 1) * page_size + results = all_results[start_idx:start_idx + page_size] + ret["data"] = results + + # 더 이상 결과가 없으면 알림 + if not results: + ret["ret"] = "info" + ret["msg"] = "모든 검색 결과를 불러왔습니다." + else: + ret["ret"] = "warning" + ret["msg"] = "검색 결과를 찾을 수 없습니다." + return jsonify(ret) except Exception as error: logger.error(f"AJAX 처리 중 예외 발생: {error}") diff --git a/my_youtube_dl.py b/my_youtube_dl.py index 17d221f..0f5a6ce 100644 --- a/my_youtube_dl.py +++ b/my_youtube_dl.py @@ -197,16 +197,34 @@ class MyYoutubeDL: """미리보기용 직접 재생 가능한 URL 추출""" youtube_dl = __import__(youtube_dl_package) try: - # 미리보기를 위해 포맷 필터링 (mp4, 비디오+오디오 권장) + # 미리보기를 위해 다양한 포맷 시도 (mp4, hls 등) ydl_opts: Dict[str, Any] = { - "format": "best[ext=mp4]/best", + "format": "best[ext=mp4]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best", "logger": MyLogger(), "nocheckcertificate": True, "quiet": True, + "js_runtimes": {"node": {"path": "/Users/yommi/.local/state/fnm_multishells/53824_1769161399333/bin/node"}}, } with youtube_dl.YoutubeDL(ydl_opts) as ydl: info: Dict[str, Any] = ydl.extract_info(url, download=False) - return info.get("url") + + # 1. HLS 매니페스트 우선 (가장 안정적으로 오디오+비디오 제공) + if info.get("manifest_url"): + return info["manifest_url"] + + # 2. 직접 URL (ydl_opts에서 지정한 best[ext=mp4] 결과) + if info.get("url"): + return info["url"] + + # 3. 포맷 목록에서 적절한 것 찾기 + formats = info.get("formats", []) + # 오디오와 비디오가 모두 있는 포맷 찾기 + combined_formats = [f for f in formats if f.get("vcodec") != "none" and f.get("acodec") != "none"] + if combined_formats: + # 가장 좋은 화질의 결합 포맷 선택 + return combined_formats[-1].get("url") + + return None except Exception as error: logger.error(f"미리보기 URL 추출 중 예외 발생: {error}") return None @@ -225,17 +243,24 @@ class MyYoutubeDL: proxy: Optional[str] = None, cookiefile: Optional[str] = None, http_headers: Optional[Dict[str, str]] = None, - cookiesfrombrowser: Optional[str] = None + cookiesfrombrowser: Optional[str] = None, + **extra_opts ) -> Optional[Dict[str, Any]]: """비디오 메타데이터 정보 추출""" youtube_dl = __import__(youtube_dl_package) try: ydl_opts: Dict[str, Any] = { - "extract_flat": "in_playlist", "logger": MyLogger(), "nocheckcertificate": True, + "quiet": True, + # JS 런타임 수동 지정 (유저 시스템 환경 반영) + "js_runtimes": {"node": {"path": "/Users/yommi/.local/state/fnm_multishells/53824_1769161399333/bin/node"}}, } + # 기본값으로 extract_flat 적용 (명시적으로 override 가능) + if "extract_flat" not in extra_opts: + ydl_opts["extract_flat"] = True # True = 모든 추출기에 적용 + if proxy: ydl_opts["proxy"] = proxy if cookiefile: @@ -244,6 +269,9 @@ class MyYoutubeDL: ydl_opts["http_headers"] = http_headers if cookiesfrombrowser: ydl_opts["cookiesfrombrowser"] = (cookiesfrombrowser, None, None, None) + + # 추가 옵션 반영 (playliststart, playlistend 등) + ydl_opts.update(extra_opts) with youtube_dl.YoutubeDL(ydl_opts) as ydl: info: Dict[str, Any] = ydl.extract_info(url, download=False) diff --git a/setup.py b/setup.py index c3e5690..2705447 100644 --- a/setup.py +++ b/setup.py @@ -22,10 +22,11 @@ __menu = { "uri": "download", "name": "직접 다운로드", }, + {"uri": "search", "name": "유튜브 검색"}, + {"uri": "thumbnail", "name": "썸네일 다운로드"}, + {"uri": "sub", "name": "자막 다운로드"}, ], }, - {"uri": "thumbnail", "name": "썸네일 다운로드"}, - {"uri": "sub", "name": "자막 다운로드"}, { "uri": "manual", "name": "매뉴얼", diff --git a/static/youtube-dl_list.css b/static/youtube-dl_list.css index 1e940ba..abdab9e 100644 --- a/static/youtube-dl_list.css +++ b/static/youtube-dl_list.css @@ -24,3 +24,34 @@ padding-left: 10px; padding-top: 3px; } + +/* Mobile Responsive - 5px padding for maximum screen usage */ +@media (max-width: 768px) { + .container, .container-fluid, #main_container { + padding-left: 5px !important; + padding-right: 5px !important; + margin-left: 0 !important; + margin-right: 0 !important; + max-width: 100% !important; + } + + .row { + margin-left: 0 !important; + margin-right: 0 !important; + } + + [class*="col-"] { + padding-left: 4px !important; + padding-right: 4px !important; + } + + .card { + border-radius: 8px !important; + margin-left: 0 !important; + margin-right: 0 !important; + } + + .table-responsive { + margin: 0 !important; + } +} diff --git a/static/youtube-dl_list.js b/static/youtube-dl_list.js index fdcb539..49529dc 100644 --- a/static/youtube-dl_list.js +++ b/static/youtube-dl_list.js @@ -35,66 +35,86 @@ const list_tbody = document.getElementById('list_tbody'); const get_item = (data) => { - let str = `${data.index + 1}`; - str += `${data.plugin}`; - str += `${data.start_time}`; - str += `${data.extractor}`; - str += `${data.title}`; - str += `${data.status_ko}`; + let str = `${data.index + 1}`; + str += `${data.plugin}`; + str += `${data.start_time}`; + str += `${data.extractor}`; + str += `${data.title}`; + + // Status color mapping + let status_class = 'badge-secondary'; + if (data.status_str === 'COMPLETED') status_class = 'badge-success'; + else if (data.status_str === 'DOWNLOADING') status_class = 'badge-primary'; + else if (data.status_str === 'ERROR') status_class = 'badge-danger'; + + str += `${data.status_ko}`; + let visi = 'hidden'; if (parseInt(data.percent) > 0 && data.status_str !== 'STOP') { visi = 'visible'; } - str += `
${data.percent}%
`; - str += `${data.download_time}`; - str += ''; + str += ` +
+
+
+
+
${data.percent}%
+ `; + str += `${data.download_time}`; + str += ''; if ( data.status_str === 'START' || data.status_str === 'DOWNLOADING' || data.status_str === 'FINISHED' ) { - str += ``; + str += ``; } str += ''; return str; }; const info_html = (left, right, option) => { - let str = '
'; + if (!right) return ''; + let str = '
'; const link = left === 'URL' || left === '업로더'; - str += '
'; - str += `${left}`; + str += '
'; + str += `${left}`; str += '
'; - str += '
'; - str += '
'; - str += ''; + str += '
'; + str += '
'; if (link) { - str += ``; + str += ``; } str += right; if (link) { str += ''; } - str += '
'; + str += '
'; return str; }; const get_detail = (data) => { - let str = info_html('URL', data.url, data.url); + let str = '
'; + str += info_html('URL', data.url, data.url); str += info_html('업로더', data.uploader, data.uploader_url); str += info_html('임시폴더', data.temp_path); str += info_html('저장폴더', data.save_path); str += info_html('종료시간', data.end_time); if (data.status_str === 'DOWNLOADING') { - str += info_html('', '현재 다운로드 중인 파일에 대한 정보'); + str += '
'; + str += '
실시간 다운로드 정보
'; str += info_html('파일명', data.filename); str += info_html( - '진행률(current/total)', - `${data.percent}% (${data.downloaded_bytes_str} / ${data.total_bytes_str})` + '현재 진행량', + `${data.percent}% (${data.downloaded_bytes_str} / ${data.total_bytes_str})` ); - str += info_html('남은 시간', `${data.eta}초`); - str += info_html('다운 속도', data.speed_str); + str += info_html('남은 시간', `${data.eta}초`); + str += info_html('다운 속도', `${data.speed_str}`); + str += '
'; } + str += '
'; return str; }; @@ -102,9 +122,9 @@ let str = ``; str += get_item(data); str += ''; - str += ``; - str += ''; - str += `
`; + str += ``; + str += ''; + str += `
`; str += get_detail(data); str += '
'; str += ''; diff --git a/static/youtube-dl_modern.css b/static/youtube-dl_modern.css new file mode 100644 index 0000000..08e9562 --- /dev/null +++ b/static/youtube-dl_modern.css @@ -0,0 +1,191 @@ +/* Modern & Professional Design for youtube-dl */ + +:root { + --primary-color: #38bdf8; /* Softer Light Blue */ + --primary-hover: #7dd3fc; + --bg-body: #0f172a; /* Deep Soothing Navy */ + --bg-surface: #1e293b; /* Surface color */ + --text-main: #e2e8f0; /* Soft Off-White */ + --text-muted: #94a3b8; /* Muted Blue-Gray */ + --border-color: #334155; /* Subtle Border */ + --radius-md: 8px; + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.4); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.5); +} + +/* Base adjustment for global layout */ +body { + background-color: var(--bg-body) !important; + font-size: 14px; + color: var(--text-main); +} + +#main_container { + padding-top: 20px; + padding-bottom: 40px; +} + +/* Compact Margins - Desktop */ +.row { + margin-right: -10px !important; + margin-left: -10px !important; +} + +.col, [class*="col-"] { + padding-right: 10px !important; + padding-left: 10px !important; +} + +/* Professional Card Style */ +.card, .form-container { + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + margin-bottom: 1.5rem; + overflow: hidden; +} + +/* Modern Inputs & Macros adjustment */ +.form-control-sm { + height: 34px !important; + background-color: #0f172a !important; + color: #f1f5f9 !important; + border-radius: 6px !important; + border: 1px solid var(--border-color) !important; + padding: 0.5rem 0.75rem !important; + transition: all 0.2s; +} + +.form-control-sm:focus { + border-color: var(--primary-color) !important; + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1) !important; +} + +.col-sm-3.col-form-label { + font-weight: 600; + color: var(--text-main); + font-size: 0.875rem; + padding-top: 8px !important; +} + +.small.text-muted { + font-size: 0.75rem !important; + margin-top: 4px; +} + +/* Professional Buttons */ +.btn-sm { + border-radius: 6px !important; + font-weight: 500 !important; + padding: 6px 16px !important; + transition: all 0.2s !important; +} + +.btn-primary { + background-color: var(--primary-color) !important; + border-color: var(--primary-color) !important; + color: #0f172a !important; /* Dark text on light button */ +} + +.btn-primary:hover { + background-color: var(--primary-hover) !important; + border-color: var(--primary-hover) !important; + color: #000000 !important; +} + +/* Modern Progress Bar */ +.progress { + height: 10px !important; + background-color: #e2e8f0 !important; + border-radius: 9999px !important; + overflow: hidden; + margin-top: 4px; +} + +.progress-bar { + background: linear-gradient(90deg, #3b82f6, #2563eb) !important; + border-radius: 9999px !important; + font-size: 0 !important; /* Hide text inside thin bar */ + transition: width 0.4s ease-in-out !important; +} + +/* Table Enhancements */ +.table { + color: var(--text-main) !important; +} + +.table-sm td, .table-sm th { + padding: 0.75rem 0.5rem !important; + vertical-align: middle !important; + border-top: 1px solid #334155 !important; +} + +.table thead th { + background: #1e293b; + border-bottom: 2px solid var(--border-color); + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + font-size: 0.75rem; + letter-spacing: 0.025em; +} + +.tableRowHover tr:hover { + background-color: rgba(56, 189, 248, 0.05) !important; +} + +/* Detail Info row in list */ +.info-padding { + padding: 8px 12px !important; + background: #0f172a; + border-radius: 6px; + display: inline-block; + color: #38bdf8; +} + +.info-value { + color: #cbd5e1; +} + +.details-container { + background: #1e293b !important; + border: 1px solid #334155 !important; +} + +/* Mobile Responsive - 5px padding */ +@media (max-width: 768px) { + .container, .container-fluid, #main_container { + padding-left: 5px !important; + padding-right: 5px !important; + } + + .row { + margin-left: -5px !important; + margin-right: -5px !important; + } + + [class*="col-"] { + padding-left: 5px !important; + padding-right: 5px !important; + } + + /* Stack labels and inputs on mobile */ + .col-sm-3.col-form-label { + text-align: left !important; + width: 100%; + max-width: 100%; + flex: 0 0 100%; + padding-bottom: 4px !important; + } + + .col-sm-9 { + width: 100%; + max-width: 100%; + flex: 0 0 100%; + } + + form > .row { + margin-bottom: 12px !important; + } +} diff --git a/static/youtube-dl_search.js b/static/youtube-dl_search.js new file mode 100644 index 0000000..480149d --- /dev/null +++ b/static/youtube-dl_search.js @@ -0,0 +1,297 @@ +'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); }); + +})(); diff --git a/templates/youtube-dl_basic_download.html b/templates/youtube-dl_basic_download.html index 785bbc0..e4122dc 100644 --- a/templates/youtube-dl_basic_download.html +++ b/templates/youtube-dl_basic_download.html @@ -30,6 +30,7 @@ {% endmacro %} {% block content %} + + +
+
+
+ + +
+
+ +
+
+ +

검색어를 입력하고 검색 버튼을 눌러주세요.

+
+
+ +
+ + +
+ + + + + + +{% endblock %} diff --git a/templates/youtube-dl_sub.html b/templates/youtube-dl_sub.html index 68c3731..3c1c168 100644 --- a/templates/youtube-dl_sub.html +++ b/templates/youtube-dl_sub.html @@ -1,4 +1,5 @@ {% extends "base.html" %} {% block content %} +
{{ macros.setting_input_text('url', 'URL', placeholder='http:// 주소', @@ -13,7 +14,7 @@
{{ macros.setting_checkbox('auto_sub', '자동생성 자막 다운로드', value='False', desc='유튜브 전용') }} {{ - macros.setting_button([['download_btn', '다운로드']]) }} + macros.setting_buttons([['download_btn', '다운로드']]) }}