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:
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user