553 lines
28 KiB
HTML
553 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>just download stuff lol</title>
|
|
<style>
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
:root{
|
|
--bg:#0d0d0f;--bg2:#141416;--bg3:#1a1a1e;--bg4:#202026;
|
|
--border:#2a2a32;--border2:#35353f;
|
|
--text:#f0f0f5;--text2:#a0a0b0;--text3:#606070;
|
|
--accent:#7c6ff7;--accent2:#9d98f5;--accent-bg:rgba(124,111,247,0.12);
|
|
--yt:#ff4444;--sp:#1db954;--ig:#e1306c;
|
|
--radius:12px;--radius-sm:8px;
|
|
}
|
|
body{background:var(--bg);color:var(--text);font-family:system-ui,sans-serif;min-height:100vh;padding:0}
|
|
.wrap{max-width:780px;margin:0 auto;padding:2rem 1.25rem 4rem}
|
|
.logo{display:flex;align-items:center;gap:10px;margin-bottom:2.5rem}
|
|
.logo-icon{width:36px;height:36px;background:var(--accent);border-radius:10px;display:flex;align-items:center;justify-content:center}
|
|
.logo-icon svg{width:18px;height:18px;fill:none;stroke:white;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
.logo-name{font-size:17px;font-weight:500}
|
|
.logo-sub{font-size:11px;color:var(--text3);margin-top:1px}
|
|
.api-badge{display:inline-flex;align-items:center;gap:5px;background:var(--bg3);border:1px solid var(--border);border-radius:20px;padding:3px 10px;font-size:11px;color:var(--text3);margin-left:8px;vertical-align:middle}
|
|
.api-dot{width:6px;height:6px;border-radius:50%;background:#1db954}
|
|
|
|
.input-zone{background:var(--bg2);border:1.5px solid var(--border);border-radius:16px;padding:1.25rem;margin-bottom:1.5rem;transition:border-color 0.2s}
|
|
.input-zone.drag-over{border-color:var(--accent)}
|
|
.input-row{display:flex;gap:10px;align-items:center}
|
|
.url-input{flex:1;background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.65rem 1rem;color:var(--text);font-size:14px;outline:none;transition:border-color 0.2s}
|
|
.url-input::placeholder{color:var(--text3)}
|
|
.url-input:focus{border-color:var(--accent)}
|
|
.btn-detect{background:var(--accent);color:white;border:none;border-radius:var(--radius-sm);padding:0.65rem 1.25rem;font-size:13px;font-weight:500;cursor:pointer;transition:opacity 0.15s,transform 0.1s}
|
|
.btn-detect:hover{opacity:0.88}
|
|
.btn-detect:active{transform:scale(0.97)}
|
|
.btn-detect:disabled{opacity:0.5;cursor:not-allowed}
|
|
.drag-hint{font-size:12px;color:var(--text3);margin-top:0.65rem;text-align:center}
|
|
|
|
.platform-tabs{display:flex;gap:8px;margin-bottom:1.25rem}
|
|
.tab{flex:1;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.6rem;font-size:13px;color:var(--text2);cursor:pointer;transition:all 0.15s;display:flex;align-items:center;justify-content:center;gap:6px;font-weight:500}
|
|
.tab:hover{border-color:var(--border2);color:var(--text)}
|
|
.tab.active-yt{background:rgba(255,68,68,0.1);border-color:var(--yt);color:var(--yt)}
|
|
.tab.active-ig{background:rgba(225,48,108,0.1);border-color:var(--ig);color:var(--ig)}
|
|
.tab-dot{width:8px;height:8px;border-radius:50%}
|
|
.tab-dot-yt{background:var(--yt)}.tab-dot-ig{background:var(--ig)}
|
|
|
|
.preview-card{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem;margin-bottom:1.25rem;display:none}
|
|
.preview-card.show{display:flex;gap:1rem}
|
|
.preview-thumb{width:120px;height:68px;border-radius:var(--radius-sm);background:var(--bg4);overflow:hidden;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
|
.preview-thumb img{width:100%;height:100%;object-fit:cover}
|
|
.preview-info{flex:1;min-width:0}
|
|
.preview-title{font-size:14px;font-weight:500;line-height:1.4;margin-bottom:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.preview-meta{display:flex;gap:8px;flex-wrap:wrap}
|
|
.meta-pill{background:var(--bg4);border-radius:20px;padding:3px 10px;font-size:11px;color:var(--text2)}
|
|
.preview-quality{font-size:11px;color:var(--text3);margin-top:8px}
|
|
|
|
.skeleton{background:linear-gradient(90deg,var(--bg3) 25%,var(--bg4) 50%,var(--bg3) 75%);background-size:200%;animation:skel 1.4s infinite;border-radius:6px}
|
|
@keyframes skel{0%{background-position:200%}100%{background-position:-200%}}
|
|
|
|
.section-label{font-size:11px;color:var(--text3);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:0.75rem;font-weight:500}
|
|
.options-grid{display:grid;gap:10px;margin-bottom:1.25rem}
|
|
.options-grid.cols3{grid-template-columns:1fr 1fr 1fr}
|
|
.opt-card{background:var(--bg2);border:1.5px solid var(--border);border-radius:var(--radius);padding:1rem;cursor:pointer;transition:all 0.15s;position:relative}
|
|
.opt-card:hover{border-color:var(--border2);background:var(--bg3)}
|
|
.opt-card.selected-yt{border-color:var(--yt);background:rgba(255,68,68,0.08)}
|
|
.opt-card.selected-ig{border-color:var(--ig);background:rgba(225,48,108,0.08)}
|
|
.opt-icon{font-size:20px;margin-bottom:8px;display:block}
|
|
.opt-title{font-size:13px;font-weight:500;margin-bottom:3px}
|
|
.opt-desc{font-size:11px;color:var(--text3)}
|
|
.opt-check{position:absolute;top:10px;right:10px;width:18px;height:18px;border-radius:50%;background:var(--accent);display:none;align-items:center;justify-content:center}
|
|
.opt-card.selected-yt .opt-check,.opt-card.selected-ig .opt-check{display:flex}
|
|
.opt-check svg{width:10px;height:10px;fill:none;stroke:white;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
|
|
|
|
.sub-panel{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem;margin-bottom:1.25rem}
|
|
.form-row{display:flex;align-items:center;gap:12px;margin-bottom:0.75rem}
|
|
.form-row:last-child{margin-bottom:0}
|
|
.form-label{font-size:13px;color:var(--text2);min-width:90px;flex-shrink:0}
|
|
.form-select{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.45rem 0.75rem;color:var(--text);font-size:13px;outline:none;cursor:pointer;flex:1}
|
|
.form-toggle{display:flex}
|
|
.toggle-btn{background:var(--bg3);border:1px solid var(--border);padding:0.4rem 0.85rem;font-size:12px;color:var(--text2);cursor:pointer;transition:all 0.15s;font-weight:500}
|
|
.toggle-btn:first-child{border-radius:var(--radius-sm) 0 0 var(--radius-sm)}
|
|
.toggle-btn:last-child{border-radius:0 var(--radius-sm) var(--radius-sm) 0;border-left:none}
|
|
.toggle-btn:not(:first-child):not(:last-child){border-left:none}
|
|
.toggle-btn.on{background:var(--accent-bg);border-color:var(--accent);color:var(--accent2)}
|
|
.time-row{display:flex;gap:8px;align-items:center}
|
|
.time-input{background:var(--bg3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.45rem 0.6rem;color:var(--text);font-size:13px;width:90px;outline:none;text-align:center}
|
|
.time-input:focus{border-color:var(--accent)}
|
|
.time-sep{color:var(--text3);font-size:12px}
|
|
|
|
.progress-wrap{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem;margin-bottom:1.25rem;display:none}
|
|
.progress-wrap.show{display:block}
|
|
.progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}
|
|
.progress-label{font-size:13px;font-weight:500}
|
|
.progress-pct{font-size:13px;color:var(--accent2)}
|
|
.progress-bar-bg{background:var(--bg4);border-radius:20px;height:6px;overflow:hidden;margin-bottom:8px}
|
|
.progress-bar{height:6px;border-radius:20px;background:var(--accent);width:0%;transition:width 0.4s}
|
|
.progress-status{font-size:11px;color:var(--text3)}
|
|
|
|
.action-row{display:flex;gap:10px;margin-bottom:1.5rem}
|
|
.btn-main{flex:1;background:var(--accent);border:none;border-radius:var(--radius-sm);padding:0.8rem;font-size:14px;font-weight:500;color:white;cursor:pointer;transition:opacity 0.15s,transform 0.1s}
|
|
.btn-main:hover{opacity:0.88}
|
|
.btn-main:active{transform:scale(0.98)}
|
|
.btn-main:disabled{opacity:0.5;cursor:not-allowed}
|
|
.btn-secondary{background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0.8rem 1.25rem;font-size:13px;color:var(--text2);cursor:pointer;transition:all 0.15s}
|
|
.btn-secondary:hover{border-color:var(--border2);color:var(--text)}
|
|
.btn-secondary:disabled{opacity:0.5;cursor:not-allowed}
|
|
|
|
.history-section{margin-top:2rem}
|
|
.history-item{display:flex;align-items:center;gap:12px;padding:0.75rem 0;border-bottom:1px solid var(--border)}
|
|
.history-item:last-child{border-bottom:none}
|
|
.history-thumb{width:44px;height:28px;border-radius:5px;background:var(--bg4);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:500}
|
|
.history-info{flex:1;min-width:0}
|
|
.history-title{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.history-meta{font-size:11px;color:var(--text3);margin-top:2px}
|
|
.history-status{font-size:11px;color:var(--sp)}
|
|
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--bg3);border:1px solid var(--border2);border-radius:var(--radius-sm);padding:0.75rem 1.25rem;font-size:13px;display:flex;align-items:center;gap:8px;transform:translateY(80px);opacity:0;transition:all 0.25s;z-index:100;max-width:320px}
|
|
.toast.show{transform:translateY(0);opacity:1}
|
|
.toast-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
.panel{display:none}.panel.active{display:block}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<div class="logo">
|
|
<div class="logo-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M12 3v13M5 13l7 7 7-7"/><path d="M3 20h18"/></svg>
|
|
</div>
|
|
<div>
|
|
<div class="logo-name">just download stuff lol <span class="api-badge"><span class="api-dot"></span>server yea</span></div>
|
|
<div class="logo-sub">no cap, it just works™</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="input-zone" id="inputZone">
|
|
<div class="input-row">
|
|
<input class="url-input" id="urlInput" type="text" placeholder="paste literally any yt / instagram link..." autocomplete="off"/>
|
|
<button class="btn-detect" id="fetchBtn" onclick="handleUrl()">go</button>
|
|
</div>
|
|
<div class="drag-hint">or drag & drop a url here</div>
|
|
</div>
|
|
|
|
<div class="platform-tabs">
|
|
<div class="tab" id="tab-yt" onclick="switchPlatform('yt')"><span class="tab-dot tab-dot-yt"></span>YouTube</div>
|
|
<div class="tab" id="tab-ig" onclick="switchPlatform('ig')"><span class="tab-dot tab-dot-ig"></span>Instagram (only public account stuff)</div>
|
|
</div>
|
|
|
|
<div id="previewCard" class="preview-card">
|
|
<div class="preview-thumb" id="previewThumb">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color:var(--text3)"><rect x="2" y="2" width="20" height="20" rx="4"/><path d="M10 8l6 4-6 4V8z"/></svg>
|
|
</div>
|
|
<div class="preview-info">
|
|
<div class="preview-title" id="previewTitle">—</div>
|
|
<div class="preview-meta" id="previewMeta"></div>
|
|
<div class="preview-quality" id="previewQuality"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="panel-yt" class="panel">
|
|
<div class="section-label">what do u want gng</div>
|
|
<div class="options-grid cols3" style="margin-bottom:1.25rem">
|
|
<div class="opt-card selected-yt" id="yt-video" onclick="selectOpt('yt','yt-video')">
|
|
<span class="opt-icon">🎬</span><div class="opt-title">Video</div><div class="opt-desc">MP4 with audio</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="yt-audio" onclick="selectOpt('yt','yt-audio')">
|
|
<span class="opt-icon">🎵</span><div class="opt-title">Audio only</div><div class="opt-desc">MP3 extraction</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="yt-thumb" onclick="selectOpt('yt','yt-thumb')">
|
|
<span class="opt-icon">🖼️</span><div class="opt-title">Thumbnail</div><div class="opt-desc">JPG/WebP</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="yt-subs" onclick="selectOpt('yt','yt-subs')">
|
|
<span class="opt-icon">💬</span><div class="opt-title">Subtitles</div><div class="opt-desc">SRT / VTT</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="yt-channel" onclick="selectOpt('yt','yt-channel')">
|
|
<span class="opt-icon">👤</span><div class="opt-title">Channel art (not working rn nub)</div><div class="opt-desc">Avatar + banner</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-panel" id="yt-video-opts">
|
|
<div class="section-label">settings</div>
|
|
<div class="form-row">
|
|
<span class="form-label">Resolution</span>
|
|
<select class="form-select" id="yt-res">
|
|
<option value="best">Auto (best)</option>
|
|
<option value="2160">4K (2160p)</option>
|
|
<option value="1440">1440p</option>
|
|
<option value="1080">1080p</option>
|
|
<option value="720">720p</option>
|
|
<option value="480">480p</option>
|
|
<option value="360">360p</option>
|
|
<option value="240">240p</option>
|
|
<option value="144">144p</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-row">
|
|
<span class="form-label">Format</span>
|
|
<div class="form-toggle" id="yt-fmt-toggle">
|
|
<div class="toggle-btn on" onclick="toggler(this,'yt-fmt-toggle')">MP4</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'yt-fmt-toggle')">WebM</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'yt-fmt-toggle')">MKV</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-row" style="align-items:flex-start">
|
|
<span class="form-label" style="padding-top:4px">Trim</span>
|
|
<div>
|
|
<div class="form-toggle" style="margin-bottom:10px">
|
|
<div class="toggle-btn on" id="trim-full" onclick="setTrim('full')">Full</div>
|
|
<div class="toggle-btn" id="trim-custom" onclick="setTrim('custom')">Custom</div>
|
|
</div>
|
|
<div class="time-row" id="trim-inputs" style="display:none">
|
|
<input class="time-input" id="startTime" placeholder="00:00"/>
|
|
<span class="time-sep">→</span>
|
|
<input class="time-input" id="endTime" placeholder="10:24"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="panel-ig" class="panel">
|
|
<div class="section-label">what do you want nub</div>
|
|
<div class="options-grid cols3" style="margin-bottom:1.25rem">
|
|
<div class="opt-card selected-ig" id="ig-post" onclick="selectOpt('ig','ig-post')">
|
|
<span class="opt-icon">📸</span><div class="opt-title">Post</div><div class="opt-desc">Image or video</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="ig-reel" onclick="selectOpt('ig','ig-reel')">
|
|
<span class="opt-icon">🎞️</span><div class="opt-title">Reel</div><div class="opt-desc">Short video</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="ig-profile" onclick="selectOpt('ig','ig-profile')">
|
|
<span class="opt-icon">👤</span><div class="opt-title">Profile (not working bruh)</div><div class="opt-desc">Avatar</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
<div class="opt-card" id="ig-thumb" onclick="selectOpt('ig','ig-thumb')">
|
|
<span class="opt-icon">🖼️</span><div class="opt-title">Thumbnail</div><div class="opt-desc">Cover frame</div>
|
|
<div class="opt-check"><svg viewBox="0 0 12 12"><polyline points="2,6 5,9 10,4"/></svg></div>
|
|
</div>
|
|
</div>
|
|
<div class="sub-panel">
|
|
<div class="section-label">settings</div>
|
|
<div class="form-row">
|
|
<span class="form-label">Format</span>
|
|
<div class="form-toggle" id="ig-fmt-toggle">
|
|
<div class="toggle-btn on" onclick="toggler(this,'ig-fmt-toggle')">MP4</div>
|
|
<div class="toggle-btn" onclick="toggler(this,'ig-fmt-toggle')">JPG</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-wrap" id="progressWrap">
|
|
<div class="progress-header">
|
|
<span class="progress-label" id="progressLabel">downloading...</span>
|
|
<span class="progress-pct" id="progressPct">0%</span>
|
|
</div>
|
|
<div class="progress-bar-bg"><div class="progress-bar" id="progressBar"></div></div>
|
|
<div class="progress-status" id="progressStatus">connecting...</div>
|
|
</div>
|
|
|
|
<div class="action-row">
|
|
<button class="btn-main" id="dlBtn" onclick="startDownload()">⬇ download it</button>
|
|
<button class="btn-secondary" id="quickBtn" onclick="quickDownload()">quick (best quality)</button>
|
|
</div>
|
|
|
|
<div class="history-section" id="historySection" style="display:none">
|
|
<div class="section-label">stuff you grabbed</div>
|
|
<div id="historyList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"><div class="toast-dot" id="toastDot"></div><span id="toastMsg"></span></div>
|
|
|
|
<script>
|
|
const API = "http://192.168.0.107:8000";
|
|
let activePlatform = null;
|
|
let activeOpts = { yt: 'yt-video', ig: 'ig-post' };
|
|
let downloading = false;
|
|
let downloadHistory = [];
|
|
|
|
function detectPlatform(url) {
|
|
if (/youtube\.com|youtu\.be/.test(url)) return 'yt';
|
|
if (/instagram\.com|instagr\.am/.test(url)) return 'ig';
|
|
return null;
|
|
}
|
|
|
|
async function handleUrl() {
|
|
const url = document.getElementById('urlInput').value.trim();
|
|
if (!url) { showToast('paste something first lol', '#e24b4a'); return; }
|
|
const platform = detectPlatform(url);
|
|
if (!platform) { showToast('only yt and instagram for now', '#e24b4a'); return; }
|
|
switchPlatform(platform);
|
|
showSkeleton();
|
|
const btn = document.getElementById('fetchBtn');
|
|
btn.disabled = true; btn.textContent = '...';
|
|
try {
|
|
const res = await fetch(`${API}/api/info/preview`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ url })
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
const data = await res.json();
|
|
renderPreview(data, platform);
|
|
showToast('fetched ✓', '#1db954');
|
|
} catch (e) {
|
|
document.getElementById('previewCard').classList.remove('show');
|
|
showToast('fetch failed: ' + e.message.slice(0, 60), '#e24b4a');
|
|
} finally {
|
|
btn.disabled = false; btn.textContent = 'go';
|
|
}
|
|
}
|
|
|
|
function showSkeleton() {
|
|
document.getElementById('previewCard').classList.add('show');
|
|
document.getElementById('previewTitle').innerHTML = '<div class="skeleton" style="height:14px;width:80%"></div>';
|
|
document.getElementById('previewMeta').innerHTML = '<div class="skeleton" style="height:20px;width:70px;border-radius:20px"></div><div class="skeleton" style="height:20px;width:60px;border-radius:20px"></div>';
|
|
document.getElementById('previewQuality').innerHTML = '';
|
|
document.getElementById('previewThumb').innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="color:var(--text3)"><rect x="2" y="2" width="20" height="20" rx="4"/><path d="M10 8l6 4-6 4V8z"/></svg>';
|
|
}
|
|
|
|
function renderPreview(data, platform) {
|
|
document.getElementById('previewCard').classList.add('show');
|
|
const thumbEl = document.getElementById('previewThumb');
|
|
if (data.thumbnail) thumbEl.innerHTML = `<img src="${data.thumbnail}" alt="" onerror="this.style.display='none'"/>`;
|
|
document.getElementById('previewTitle').textContent = data.title || data.owner || '—';
|
|
const color = platform === 'yt' ? 'var(--yt)' : 'var(--ig)';
|
|
const pills = [];
|
|
if (data.duration_str) pills.push(`<span class="meta-pill">${data.duration_str}</span>`);
|
|
if (data.view_count) pills.push(`<span class="meta-pill">${fmtNum(data.view_count)} views</span>`);
|
|
if (data.likes) pills.push(`<span class="meta-pill">${fmtNum(data.likes)} likes</span>`);
|
|
if (data.uploader) pills.push(`<span class="meta-pill" style="color:${color}">${data.uploader}</span>`);
|
|
if (data.owner) pills.push(`<span class="meta-pill" style="color:${color}">@${data.owner}</span>`);
|
|
document.getElementById('previewMeta').innerHTML = pills.join('');
|
|
const res = data.available_resolutions || [];
|
|
document.getElementById('previewQuality').textContent = res.length ? `up to ${res[0]}p available` : '';
|
|
}
|
|
|
|
function fmtNum(n) {
|
|
if (n >= 1e6) return (n/1e6).toFixed(1)+'M';
|
|
if (n >= 1e3) return (n/1e3).toFixed(1)+'K';
|
|
return String(n);
|
|
}
|
|
|
|
function switchPlatform(p) {
|
|
activePlatform = p;
|
|
['yt','ig'].forEach(id => {
|
|
document.getElementById('tab-'+id).className = 'tab'+(id===p?' active-'+p:'');
|
|
document.getElementById('panel-'+id).className = 'panel'+(id===p?' active':'');
|
|
});
|
|
}
|
|
|
|
function selectOpt(platform, id) {
|
|
document.querySelectorAll('#panel-'+platform+' .opt-card').forEach(c => c.className = 'opt-card');
|
|
document.getElementById(id).className = 'opt-card selected-'+platform;
|
|
activeOpts[platform] = id;
|
|
if (platform === 'yt') document.getElementById('yt-video-opts').style.display = id==='yt-video' ? 'block' : 'none';
|
|
}
|
|
|
|
function toggler(el, groupId) {
|
|
document.getElementById(groupId).querySelectorAll('.toggle-btn').forEach(b => b.classList.remove('on'));
|
|
el.classList.add('on');
|
|
}
|
|
|
|
function setTrim(mode) {
|
|
document.getElementById('trim-full').classList.toggle('on', mode==='full');
|
|
document.getElementById('trim-custom').classList.toggle('on', mode==='custom');
|
|
document.getElementById('trim-inputs').style.display = mode==='custom' ? 'flex' : 'none';
|
|
}
|
|
|
|
function startDownload() {
|
|
if (downloading) return;
|
|
if (!activePlatform) { showToast('pick a platform first', '#e24b4a'); return; }
|
|
runDownload(false);
|
|
}
|
|
|
|
function quickDownload() {
|
|
if (downloading) return;
|
|
if (!activePlatform) { showToast('paste a url first', '#e24b4a'); return; }
|
|
runDownload(true);
|
|
}
|
|
|
|
async function runDownload(quick) {
|
|
const url = document.getElementById('urlInput').value.trim();
|
|
if (!url) { showToast('paste a url first lol', '#e24b4a'); return; }
|
|
downloading = true;
|
|
setButtons(false);
|
|
|
|
const wrap = document.getElementById('progressWrap');
|
|
const bar = document.getElementById('progressBar');
|
|
const pct = document.getElementById('progressPct');
|
|
const stat = document.getElementById('progressStatus');
|
|
const lbl = document.getElementById('progressLabel');
|
|
|
|
wrap.className = 'progress-wrap show';
|
|
bar.style.width = '0%'; pct.textContent = '0%';
|
|
lbl.textContent = 'sending request...';
|
|
stat.textContent = 'connecting to tablet...';
|
|
|
|
let fakeP = 0;
|
|
const fakeInterval = setInterval(() => {
|
|
if (fakeP < 85) { fakeP += Math.random()*4+1; bar.style.width=fakeP+'%'; pct.textContent=Math.round(fakeP)+'%'; }
|
|
}, 400);
|
|
const statuses = ['fetching from yt-dlp...','downloading stream...','muxing audio+video...','almost done...'];
|
|
let si = 0;
|
|
const statInterval = setInterval(() => { if (si < statuses.length) stat.textContent = statuses[si++]; }, 3000);
|
|
|
|
try {
|
|
const endpoint = getEndpoint(quick);
|
|
const body = buildBody(url, quick);
|
|
lbl.textContent = 'downloading — ' + (activePlatform==='yt'?'YouTube':'Instagram');
|
|
|
|
const res = await fetch(`${API}${endpoint}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
if (!res.ok) throw new Error(await res.text());
|
|
|
|
bar.style.width = '95%'; pct.textContent = '95%'; stat.textContent = 'saving file...';
|
|
const blob = await res.blob();
|
|
const disposition = res.headers.get('content-disposition') || '';
|
|
const filename = disposition.split('filename=')[1]?.replace(/"/g,'') || guessFilename();
|
|
|
|
const a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = filename;
|
|
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
|
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
|
|
|
|
bar.style.width = '100%'; pct.textContent = '100%'; stat.textContent = 'done lol';
|
|
showToast('got it 👌', '#1db954');
|
|
addHistory(filename);
|
|
setTimeout(() => { wrap.className='progress-wrap'; bar.style.width='0%'; }, 2000);
|
|
} catch(e) {
|
|
clearInterval(fakeInterval); clearInterval(statInterval);
|
|
wrap.className = 'progress-wrap'; bar.style.width = '0%';
|
|
showToast('error: ' + e.message.slice(0,80), '#e24b4a');
|
|
} finally {
|
|
downloading = false; setButtons(true);
|
|
}
|
|
}
|
|
|
|
function getEndpoint(quick) {
|
|
if (activePlatform === 'yt') {
|
|
if (quick) return '/api/youtube/video';
|
|
const opt = activeOpts.yt;
|
|
if (opt==='yt-video') return '/api/youtube/video';
|
|
if (opt==='yt-audio') return '/api/youtube/audio';
|
|
if (opt==='yt-thumb') return '/api/youtube/thumbnail';
|
|
if (opt==='yt-subs') return '/api/youtube/subtitles';
|
|
if (opt==='yt-channel') return '/api/youtube/channel-art';
|
|
}
|
|
if (activePlatform === 'ig') {
|
|
const opt = activeOpts.ig;
|
|
if (opt==='ig-post') return '/api/instagram/post';
|
|
if (opt==='ig-reel') return '/api/instagram/reel';
|
|
if (opt==='ig-profile') return '/api/instagram/profile';
|
|
if (opt==='ig-thumb') return '/api/instagram/thumbnail';
|
|
}
|
|
return '/api/youtube/video';
|
|
}
|
|
|
|
function buildBody(url, quick) {
|
|
if (activePlatform === 'yt') {
|
|
if (quick) return { url, resolution: 'best', format: 'mp4' };
|
|
const opt = activeOpts.yt;
|
|
if (opt==='yt-video') {
|
|
const res = document.getElementById('yt-res').value;
|
|
const fmt = document.querySelector('#yt-fmt-toggle .on')?.textContent.toLowerCase() || 'mp4';
|
|
const start = document.getElementById('startTime').value || null;
|
|
const end = document.getElementById('endTime').value || null;
|
|
return { url, resolution: res, format: fmt, start_time: start, end_time: end };
|
|
}
|
|
if (opt==='yt-audio') return { url, quality: '192', format: 'mp3' };
|
|
if (opt==='yt-subs') return { url, lang: 'en' };
|
|
if (opt==='yt-channel') return { channel_url: url };
|
|
return { url };
|
|
}
|
|
if (activePlatform === 'ig') {
|
|
const opt = activeOpts.ig;
|
|
const fmt = document.querySelector('#ig-fmt-toggle .on')?.textContent.toLowerCase() || 'mp4';
|
|
if (opt==='ig-post' || opt==='ig-reel') return { url, format: fmt };
|
|
if (opt==='ig-profile') {
|
|
const m = url.match(/instagram\.com\/([^/?]+)/);
|
|
return { username: m ? m[1] : url };
|
|
}
|
|
return { url };
|
|
}
|
|
return { url };
|
|
}
|
|
|
|
function guessFilename() {
|
|
const opt = activeOpts[activePlatform] || '';
|
|
if (opt.includes('audio')) return 'audio.mp3';
|
|
if (opt.includes('thumb')) return 'thumbnail.jpg';
|
|
if (opt.includes('subs')) return 'subtitles.srt';
|
|
return activePlatform==='ig' ? 'post.mp4' : 'video.mp4';
|
|
}
|
|
|
|
function setButtons(enabled) {
|
|
['dlBtn','quickBtn','fetchBtn'].forEach(id => document.getElementById(id).disabled = !enabled);
|
|
}
|
|
|
|
function addHistory(filename) {
|
|
downloadHistory.unshift({ title: filename, platform: activePlatform, time: new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}) });
|
|
renderHistory();
|
|
}
|
|
|
|
function renderHistory() {
|
|
const sec = document.getElementById('historySection');
|
|
const list = document.getElementById('historyList');
|
|
sec.style.display = downloadHistory.length ? 'block' : 'none';
|
|
list.innerHTML = downloadHistory.slice(0,6).map(item=>`
|
|
<div class="history-item">
|
|
<div class="history-thumb" style="color:var(--${item.platform==='yt'?'yt':'ig'})">${item.platform.toUpperCase()}</div>
|
|
<div class="history-info"><div class="history-title">${item.title}</div><div class="history-meta">${item.time}</div></div>
|
|
<div class="history-status">✓ got it</div>
|
|
</div>`).join('');
|
|
}
|
|
|
|
let toastTimeout;
|
|
function showToast(msg, color) {
|
|
clearTimeout(toastTimeout);
|
|
const t = document.getElementById('toast');
|
|
document.getElementById('toastMsg').textContent = msg;
|
|
document.getElementById('toastDot').style.background = color;
|
|
t.classList.add('show');
|
|
toastTimeout = setTimeout(() => t.classList.remove('show'), 3000);
|
|
}
|
|
|
|
document.getElementById('urlInput').addEventListener('paste', () => setTimeout(handleUrl, 60));
|
|
document.getElementById('urlInput').addEventListener('keydown', e => { if(e.key==='Enter') handleUrl(); });
|
|
const zone = document.getElementById('inputZone');
|
|
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
|
|
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
zone.addEventListener('drop', e => {
|
|
e.preventDefault(); zone.classList.remove('drag-over');
|
|
const url = e.dataTransfer.getData('text');
|
|
if (url) { document.getElementById('urlInput').value = url; handleUrl(); }
|
|
});
|
|
|
|
switchPlatform('yt');
|
|
document.getElementById('yt-video-opts').style.display = 'block';
|
|
</script>
|
|
</body>
|
|
</html>
|