266 lines
12 KiB
HTML
266 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>BatchTube</title>
|
|
<link rel="stylesheet" href="/style.css" />
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ═══════════════════════════ HEADER ═══════════════════════════ -->
|
|
<header class="app-header">
|
|
<div class="app-logo">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.1V9.01a6.33 6.33 0 00-.79-.05 6.34 6.34 0 00-6.34 6.34 6.34 6.34 0 006.34 6.34 6.34 6.34 0 006.33-6.34v-7a8.15 8.15 0 004.77 1.52v-3.4a4.85 4.85 0 01-1-.09z"/>
|
|
</svg>
|
|
<span class="app-title">BatchTube</span>
|
|
</div>
|
|
<nav class="header-nav">
|
|
<button class="nav-btn active" onclick="showView('jobs')" id="nav-jobs">Jobs</button>
|
|
<button class="nav-btn" onclick="showView('new')" id="nav-new">+ New Job</button>
|
|
<button class="nav-btn" onclick="showView('settings')" id="nav-settings">⚙ Settings</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<div class="app-body">
|
|
|
|
<!-- ═══════════════════════ SIDEBAR ═══════════════════════ -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-section">
|
|
<button class="new-job-btn" onclick="showView('new')">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
New Extraction Job
|
|
</button>
|
|
</div>
|
|
<div class="sidebar-section" style="flex:1; overflow-y:auto; border-bottom:none;">
|
|
<div class="sidebar-label">Recent Jobs</div>
|
|
<div class="job-list" id="sidebar-job-list">
|
|
<div class="text-muted" style="font-size:12px;padding:8px 4px;">No jobs yet</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ═══════════════════════ MAIN CONTENT ═══════════════════════ -->
|
|
<main class="main-content">
|
|
|
|
<!-- ─── VIEW: NEW JOB ─── -->
|
|
<div id="view-new" class="hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
|
|
<div>
|
|
<div class="card-title">New Extraction Job</div>
|
|
<div class="card-subtitle">Configure YouTube source and what to extract</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input Type -->
|
|
<div class="form-group">
|
|
<label class="form-label">Input Type</label>
|
|
<div class="type-pills">
|
|
<div class="type-pill selected" data-type="url" onclick="selectInputType('url')">🔗 Single URL</div>
|
|
<div class="type-pill" data-type="playlist" onclick="selectInputType('playlist')">📋 Playlist</div>
|
|
<div class="type-pill" data-type="handle" onclick="selectInputType('handle')">👤 Channel Handle</div>
|
|
<div class="type-pill" data-type="csv" onclick="selectInputType('csv')">📄 CSV List</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input Value -->
|
|
<div class="form-group" id="input-url-group">
|
|
<label class="form-label" id="input-label">YouTube URL</label>
|
|
<input type="text" class="form-input" id="input-value"
|
|
placeholder="https://www.youtube.com/watch?v=..." />
|
|
</div>
|
|
|
|
<!-- CSV textarea (only shown for csv type) -->
|
|
<div class="form-group hidden" id="input-csv-group">
|
|
<label class="form-label">CSV / List of URLs (one per line or comma-separated)</label>
|
|
<textarea class="form-textarea" id="input-csv-value" rows="5"
|
|
placeholder="https://youtu.be/abc123 https://youtu.be/xyz456 dQw4w9WgXcQ"></textarea>
|
|
</div>
|
|
|
|
<hr class="divider" />
|
|
|
|
<!-- Artifact Selection -->
|
|
<div class="form-group">
|
|
<label class="form-label">What to Extract (select all that apply)</label>
|
|
<div class="artifact-grid" id="artifact-grid">
|
|
<!-- populated by JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider" />
|
|
|
|
<!-- Output Folder -->
|
|
<div class="form-group">
|
|
<label class="form-label">Output Folder</label>
|
|
<div class="folder-display" id="folder-display" onclick="openFolderBrowser()">
|
|
<span>📁</span>
|
|
<span class="folder-path" id="folder-path-text">Click to choose a folder…</span>
|
|
</div>
|
|
<input type="hidden" id="output-folder" value="" />
|
|
</div>
|
|
|
|
<!-- Resolution (shown only when mp4 selected) -->
|
|
<div class="form-group hidden" id="resolution-group">
|
|
<label class="form-label">MP4 Resolution</label>
|
|
<select class="form-select" id="resolution">
|
|
<option value="best">Best Available</option>
|
|
<option value="2160">4K (2160p)</option>
|
|
<option value="1440">2K (1440p)</option>
|
|
<option value="1080" selected>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>
|
|
|
|
<!-- Bot Avoidance Delays -->
|
|
<div class="form-group">
|
|
<label class="form-label">Random Delay Between Downloads (seconds, for bot detection avoidance)</label>
|
|
<div class="range-row">
|
|
<div class="form-group" style="margin:0">
|
|
<label class="form-label" style="font-size:11px">Min Delay</label>
|
|
<input type="number" class="form-input" id="delay-min" value="2" min="0" max="60" step="0.5" style="width:100px" />
|
|
</div>
|
|
<div class="form-group" style="margin:0">
|
|
<label class="form-label" style="font-size:11px">Max Delay</label>
|
|
<input type="number" class="form-input" id="delay-max" value="7" min="0" max="120" step="0.5" style="width:100px" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="divider" />
|
|
|
|
<!-- API Settings -->
|
|
<div class="form-group">
|
|
<label class="form-label">OpenRouter API Key <span class="text-muted">(required for summaries)</span></label>
|
|
<input type="password" class="form-input" id="api-key"
|
|
placeholder="sk-or-v1-..." autocomplete="off" />
|
|
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">
|
|
Not stored — only used for this session's summary requests
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">OpenRouter Model</label>
|
|
<input type="text" class="form-input" id="or-model"
|
|
value="openai/gpt-4o-mini"
|
|
placeholder="openai/gpt-4o-mini" />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Summary Prompt <span class="text-muted">(leave blank for default)</span></label>
|
|
<textarea class="form-textarea" id="summary-prompt" rows="3"
|
|
placeholder="Custom system prompt for LLM summarization (optional)…"></textarea>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" onclick="createJob()" id="create-btn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="5 12 12 5 19 12"/><polyline points="12 19 12 5"/></svg>
|
|
Start Extraction
|
|
</button>
|
|
<button class="btn btn-outline" onclick="showView('jobs')">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── VIEW: JOB DETAIL ─── -->
|
|
<div id="view-jobs">
|
|
<div id="job-detail">
|
|
<!-- Welcome / empty state -->
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">🎬</div>
|
|
<div class="empty-state-title">BatchTube</div>
|
|
<div class="empty-state-body">
|
|
Extract titles, stats, transcripts, summaries, thumbnails, MP3, and MP4 from YouTube videos.<br/><br/>
|
|
Click <strong>+ New Job</strong> to get started.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── VIEW: SETTINGS ─── -->
|
|
<div id="view-settings" class="hidden">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title">⚙ Settings & Defaults</div>
|
|
<div class="card-subtitle">These defaults are used when creating new jobs</div>
|
|
</div>
|
|
<div class="settings-grid">
|
|
<div class="form-group">
|
|
<label class="form-label">Default Delay Min (s)</label>
|
|
<input type="number" class="form-input" id="s-delay-min" value="2" min="0" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Default Delay Max (s)</label>
|
|
<input type="number" class="form-input" id="s-delay-max" value="7" min="0" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Default MP4 Resolution</label>
|
|
<select class="form-select" id="s-resolution">
|
|
<option value="best">Best</option>
|
|
<option value="1080" selected>1080p</option>
|
|
<option value="720">720p</option>
|
|
<option value="480">480p</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Default OpenRouter Model</label>
|
|
<input type="text" class="form-input" id="s-model" value="openai/gpt-4o-mini" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Default Summary Prompt</label>
|
|
<textarea class="form-textarea" id="s-prompt" rows="4"></textarea>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-blue" onclick="saveSettings()">Save Defaults</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title">🔌 MCP Server Registration</div>
|
|
<div class="card-subtitle">Add this to your MCP settings to use with AI agents</div>
|
|
</div>
|
|
<pre id="mcp-config-snippet" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:14px;font-size:12px;color:var(--text-dim);overflow-x:auto;white-space:pre-wrap;word-break:break-all;"></pre>
|
|
<div class="mt-8">
|
|
<button class="btn btn-outline btn-sm" onclick="copyMcpConfig()">📋 Copy to Clipboard</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════ FOLDER BROWSER MODAL ═══════════════════════ -->
|
|
<div class="modal-overlay hidden" id="folder-modal">
|
|
<div class="modal" style="max-width:600px">
|
|
<div class="modal-title">📁 Choose Output Folder</div>
|
|
<div style="margin-bottom:10px;">
|
|
<div class="input-with-btn">
|
|
<input type="text" class="form-input" id="folder-modal-path" placeholder="Type path or browse below…" />
|
|
<button class="btn btn-blue btn-sm" onclick="navigateToTyped()">Go</button>
|
|
<button class="btn btn-outline btn-sm" onclick="createNewFolder()">+ New Folder</button>
|
|
</div>
|
|
</div>
|
|
<div class="folder-current" id="folder-current-path"></div>
|
|
<div class="folder-browser" id="folder-browser-list"></div>
|
|
<div class="btn-row mt-16">
|
|
<button class="btn btn-green" onclick="selectCurrentFolder()">✔ Select This Folder</button>
|
|
<button class="btn btn-outline" onclick="closeFolderModal()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════════════════ TOAST CONTAINER ═══════════════════════ -->
|
|
<div class="toast-container" id="toast-container"></div>
|
|
|
|
<script src="/app.js"></script>
|
|
</body>
|
|
</html>
|