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

This commit is contained in:
2025-12-28 20:10:43 +09:00
parent 6dbeff13d3
commit 028562ea18
3 changed files with 111 additions and 31 deletions

View File

@@ -246,7 +246,7 @@ class FfmpegQueue(object):
logger.info(f"=== END COMMAND ===") logger.info(f"=== END COMMAND ===")
# m3u8 URL인 경우 다운로드 방법 설정에 따라 분기 # 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") download_method = P.ModelSetting.get(f"{self.name}_download_method")

View File

@@ -135,8 +135,14 @@ class YtdlpDownloader:
bufsize=1 bufsize=1
) )
# 여러 진행률 형식 매칭
# [download] 10.5% of ~100.00MiB at 2.45MiB/s # [download] 10.5% of ~100.00MiB at 2.45MiB/s
prog_re = re.compile(r'\[download\]\s+(?P<percent>[\d\.]+)%\s+of\s+.*?\s+at\s+(?P<speed>.*?)(\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<percent>[\d\.]+)%\s+of\s+.*?(?:\s+at\s+(?P<speed>[\d\.]+\s*\w+/s))?'),
re.compile(r'\[download\]\s+(?P<percent>[\d\.]+)%'),
]
for line in self.process.stdout: for line in self.process.stdout:
if self.cancelled: if self.cancelled:
@@ -146,18 +152,27 @@ class YtdlpDownloader:
line = line.strip() line = line.strip()
if not line: continue if not line: continue
match = prog_re.search(line) # 디버깅: 모든 출력 로깅 (너무 많으면 주석 해제)
if match: if '[download]' in line or 'fragment' in line.lower():
try: logger.debug(f"yt-dlp: {line}")
self.percent = float(match.group('percent'))
self.current_speed = match.group('speed').strip() for prog_re in prog_patterns:
if self.start_time: match = prog_re.search(line)
elapsed = time.time() - self.start_time if match:
self.elapsed_time = self.format_time(elapsed) try:
if self.callback: self.percent = float(match.group('percent'))
self.callback(percent=int(self.percent), current=int(self.percent), total=100, speed=self.current_speed, elapsed=self.elapsed_time) speed_group = match.groupdict().get('speed')
except: pass if speed_group:
elif 'error' in line.lower() or 'security' in line.lower() or 'unable' in line.lower(): 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}") logger.warning(f"yt-dlp output notice: {line}")
self.error_output.append(line) self.error_output.append(line)

View File

@@ -21,15 +21,17 @@
</div> </div>
</div> </div>
</div> </div>
<form id="program_list"> <div id="main_content" style="display: none; opacity: 0; transition: opacity 0.3s ease-in;">
{{ macros.setting_input_text_and_buttons('code', '작품 Code', <form id="program_list">
[['analysis_btn', '분석'], ['go_anilife_btn', 'Go 애니라이프']], desc='예) {{ macros.setting_input_text_and_buttons('code', '작품 Code',
"https://anilife.live/g/l?id=f6e83ec6-bd25-4d6c-9428-c10522687604" 이나 "f6e83ec6-bd25-4d6c-9428-c10522687604"') [['analysis_btn', '분석'], ['go_anilife_btn', 'Go 애니라이프']], desc='예)
}} "https://anilife.live/g/l?id=f6e83ec6-bd25-4d6c-9428-c10522687604" 이나 "f6e83ec6-bd25-4d6c-9428-c10522687604"')
</form> }}
<form id="program_auto_form"> </form>
<div id="episode_list"></div> <form id="program_auto_form">
</form> <div id="episode_list"></div>
</form>
</div>
</div> </div>
<!--전체--> <!--전체-->
<script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script> <script src="{{ url_for('.static', filename='js/sjva_ui14.js') }}"></script>
@@ -123,6 +125,13 @@
function make_program(data) { function make_program(data) {
current_data = data; current_data = data;
// console.log("current_data::", current_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 = ''; str = '';
tmp = '<div class="form-inline">' tmp = '<div class="form-inline">'
tmp += m_button('check_download_btn', '선택 다운로드 추가', []); tmp += m_button('check_download_btn', '선택 다운로드 추가', []);
@@ -146,7 +155,7 @@
if (data.image && data.image.includes('cdn.anilife.live')) { if (data.image && data.image.includes('cdn.anilife.live')) {
proxyImgSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?image_url=' + encodeURIComponent(data.image); proxyImgSrc = '/' + package_name + '/ajax/' + sub + '/proxy_image?image_url=' + encodeURIComponent(data.image);
} }
tmp = '<img src="' + proxyImgSrc + '" class="img-fluid" onerror="this.src=\'../static/img_loader_x200.svg\'">'; tmp = '<img src="' + proxyImgSrc + '" class="img-fluid series-main-img" onerror="this.src=\'../static/img_loader_x200.svg\'">';
} }
str += m_col(3, tmp) str += m_col(3, tmp)
tmp = '' tmp = ''
@@ -182,7 +191,7 @@
str += '<div class="episode-card">'; str += '<div class="episode-card">';
str += '<div class="episode-thumb">'; str += '<div class="episode-thumb">';
if (epThumbSrc) { if (epThumbSrc) {
str += '<img src="' + epThumbSrc + '" onerror="this.src=\'../static/img_loader_x200.svg\'">'; str += '<img src="' + epThumbSrc + '" loading="lazy" onerror="this.src=\'../static/img_loader_x200.svg\'">';
} }
str += '<span class="episode-num">' + data.episode[i].ep_num + '화</span>'; str += '<span class="episode-num">' + data.episode[i].ep_num + '화</span>';
str += '</div>'; str += '</div>';
@@ -199,8 +208,50 @@
str += '</div>'; str += '</div>';
} }
str += '</div>'; str += '</div>';
document.getElementById("episode_list").innerHTML = str; episodeList.innerHTML = str;
$('input[id^="checkbox_"]').bootstrapToggle() $('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 () { $(function () {
@@ -240,11 +291,25 @@
}) })
$(document).ready(function () { $(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({{ arg['code'] }})
// console.log('wr_id::', params.wr_id)
}); });
$("#analysis_btn").unbind("click").bind('click', function (e) { $("#analysis_btn").unbind("click").bind('click', function (e) {