From 028562ea1829f3b42822fd31513cd57f0332fe25 Mon Sep 17 00:00:00 2001 From: projectdx Date: Sun, 28 Dec 2025 20:10:43 +0900 Subject: [PATCH] feat: Refine UI content loading with fade-in effects and lazy image handling, expand M3U8 URL detection to include `gcdn.app`, and enhance yt-dlp download progress --- lib/ffmpeg_queue_v1.py | 2 +- lib/ytdlp_downloader.py | 41 +++++--- .../anime_downloader_anilife_request.html | 99 +++++++++++++++---- 3 files changed, 111 insertions(+), 31 deletions(-) diff --git a/lib/ffmpeg_queue_v1.py b/lib/ffmpeg_queue_v1.py index 37ac90a..b29f45d 100644 --- a/lib/ffmpeg_queue_v1.py +++ b/lib/ffmpeg_queue_v1.py @@ -246,7 +246,7 @@ class FfmpegQueue(object): logger.info(f"=== END COMMAND ===") # m3u8 URL인 경우 다운로드 방법 설정에 따라 분기 - if video_url.endswith('.m3u8') or 'master.txt' in video_url: + if video_url.endswith('.m3u8') or 'master.txt' in video_url or 'gcdn.app' in video_url: # 다운로드 방법 설정 확인 download_method = P.ModelSetting.get(f"{self.name}_download_method") diff --git a/lib/ytdlp_downloader.py b/lib/ytdlp_downloader.py index 43b0cc5..20dd9f7 100644 --- a/lib/ytdlp_downloader.py +++ b/lib/ytdlp_downloader.py @@ -135,8 +135,14 @@ class YtdlpDownloader: bufsize=1 ) + # 여러 진행률 형식 매칭 # [download] 10.5% of ~100.00MiB at 2.45MiB/s - prog_re = re.compile(r'\[download\]\s+(?P[\d\.]+)%\s+of\s+.*?\s+at\s+(?P.*?)(\s+ETA|$)') + # [download] 10.5% of 100.00MiB at 2.45MiB/s ETA 00:30 + # [download] 100% of 100.00MiB + prog_patterns = [ + re.compile(r'\[download\]\s+(?P[\d\.]+)%\s+of\s+.*?(?:\s+at\s+(?P[\d\.]+\s*\w+/s))?'), + re.compile(r'\[download\]\s+(?P[\d\.]+)%'), + ] for line in self.process.stdout: if self.cancelled: @@ -146,18 +152,27 @@ class YtdlpDownloader: line = line.strip() if not line: continue - match = prog_re.search(line) - if match: - try: - self.percent = float(match.group('percent')) - self.current_speed = match.group('speed').strip() - if self.start_time: - elapsed = time.time() - self.start_time - self.elapsed_time = self.format_time(elapsed) - if self.callback: - self.callback(percent=int(self.percent), current=int(self.percent), total=100, speed=self.current_speed, elapsed=self.elapsed_time) - except: pass - elif 'error' in line.lower() or 'security' in line.lower() or 'unable' in line.lower(): + # 디버깅: 모든 출력 로깅 (너무 많으면 주석 해제) + if '[download]' in line or 'fragment' in line.lower(): + logger.debug(f"yt-dlp: {line}") + + for prog_re in prog_patterns: + match = prog_re.search(line) + if match: + try: + self.percent = float(match.group('percent')) + speed_group = match.groupdict().get('speed') + if speed_group: + self.current_speed = speed_group.strip() + if self.start_time: + elapsed = time.time() - self.start_time + self.elapsed_time = self.format_time(elapsed) + if self.callback: + self.callback(percent=int(self.percent), current=int(self.percent), total=100, speed=self.current_speed, elapsed=self.elapsed_time) + except: pass + break # 한 패턴이 매칭되면 중단 + + if 'error' in line.lower() or 'security' in line.lower() or 'unable' in line.lower(): logger.warning(f"yt-dlp output notice: {line}") self.error_output.append(line) diff --git a/templates/anime_downloader_anilife_request.html b/templates/anime_downloader_anilife_request.html index 7d5867f..c270119 100644 --- a/templates/anime_downloader_anilife_request.html +++ b/templates/anime_downloader_anilife_request.html @@ -21,15 +21,17 @@ -
- {{ macros.setting_input_text_and_buttons('code', '작품 Code', - [['analysis_btn', '분석'], ['go_anilife_btn', 'Go 애니라이프']], desc='예) - "https://anilife.live/g/l?id=f6e83ec6-bd25-4d6c-9428-c10522687604" 이나 "f6e83ec6-bd25-4d6c-9428-c10522687604"') - }} -
-
-
-
+ @@ -123,6 +125,13 @@ function make_program(data) { current_data = data; // console.log("current_data::", current_data) + + // 에피소드 목록을 완전히 숨긴 상태로 시작 (visibility로 레이아웃 시프트 방지) + const episodeList = document.getElementById("episode_list"); + episodeList.style.visibility = 'hidden'; + episodeList.style.opacity = '0'; + episodeList.style.transition = 'opacity 0.3s ease-in'; + str = ''; tmp = '
' tmp += m_button('check_download_btn', '선택 다운로드 추가', []); @@ -146,7 +155,7 @@ if (data.image && data.image.includes('cdn.anilife.live')) { proxyImgSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?image_url=' + encodeURIComponent(data.image); } - tmp = ''; + tmp = ''; } str += m_col(3, tmp) tmp = '' @@ -182,7 +191,7 @@ str += '
'; str += '
'; if (epThumbSrc) { - str += ''; + str += ''; } str += '' + data.episode[i].ep_num + '화'; str += '
'; @@ -199,8 +208,50 @@ str += '
'; } str += '
'; - document.getElementById("episode_list").innerHTML = str; - $('input[id^="checkbox_"]').bootstrapToggle() + episodeList.innerHTML = str; + $('input[id^="checkbox_"]').bootstrapToggle(); + + // 이미지 로딩 완료 후 표시 (최대 2초 대기) + const images = episodeList.querySelectorAll('img'); + let loadedCount = 0; + const totalImages = images.length; + + function showContent() { + episodeList.style.visibility = 'visible'; + episodeList.style.opacity = '1'; + } + + if (totalImages === 0) { + showContent(); + } else { + // 최대 2초 후 강제 표시 + const forceShowTimeout = setTimeout(showContent, 2000); + + images.forEach(img => { + if (img.complete) { + loadedCount++; + if (loadedCount >= totalImages) { + clearTimeout(forceShowTimeout); + showContent(); + } + } else { + img.addEventListener('load', function() { + loadedCount++; + if (loadedCount >= totalImages) { + clearTimeout(forceShowTimeout); + showContent(); + } + }); + img.addEventListener('error', function() { + loadedCount++; + if (loadedCount >= totalImages) { + clearTimeout(forceShowTimeout); + showContent(); + } + }); + } + }); + } } $(function () { @@ -240,11 +291,25 @@ }) $(document).ready(function () { - $("#loader").css("display", 'none') + // DOM 로딩 완료 후 콘텐츠 표시 + const mainContent = document.getElementById('main_content'); + const preloader = document.getElementById('preloader'); + + // 메인 콘텐츠 보이기 (fade-in 효과) + mainContent.style.display = 'block'; + setTimeout(function() { + mainContent.style.opacity = '1'; + // preloader 숨기기 + if (preloader) { + preloader.style.opacity = '0'; + setTimeout(function() { + preloader.style.display = 'none'; + }, 300); + } + }, 100); + + $("#loader").css("display", 'none'); console.log({{ arg['code'] }}) - // console.log('wr_id::', params.wr_id) - - }); $("#analysis_btn").unbind("click").bind('click', function (e) {