| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>WeBrake · HandBrake, poured over the web</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
- <style>
- :root{
- --bg:#F6F0E2; /* cream */
- --bg-raise:#FDFAF2;
- --bg-sunken:#EFE7D3;
- --ink:#33271B;
- --ink-soft:#6E5E49;
- --line:#E2D7BE;
- --gold:#B98A1E; /* gold accent */
- --gold-bright:#D9A82E;
- --gold-soft:#F0E3C0;
- --ok:#5E7C3A; --err:#A8442F; --warn:#B0761D;
- --shadow:0 1px 2px rgba(51,39,27,.06),0 8px 24px rgba(51,39,27,.07);
- --radius:14px;
- }
- [data-theme="dark"]{
- --bg:#221610; /* dark roast */
- --bg-raise:#2C1E15;
- --bg-sunken:#1A100B;
- --ink:#F0E4D0;
- --ink-soft:#B39B7E;
- --line:#3E2C1E;
- --gold:#D4A937;
- --gold-bright:#E8C158;
- --gold-soft:#4A3617;
- --ok:#8FB061; --err:#D3745E; --warn:#D9A54B;
- --shadow:0 1px 2px rgba(0,0,0,.35),0 10px 28px rgba(0,0,0,.35);
- }
- *{box-sizing:border-box;margin:0;padding:0}
- html{color-scheme:light}
- [data-theme="dark"] html,[data-theme="dark"]{color-scheme:dark}
- body{
- font-family:'Inter',system-ui,sans-serif;font-size:14.5px;line-height:1.5;
- background:var(--bg);color:var(--ink);
- transition:background .3s,color .3s;min-height:100vh;
- }
- h1,h2,h3{font-family:'Fraunces',serif;font-weight:700;letter-spacing:-.01em}
- code,.mono,textarea.mono,input.mono{font-family:'JetBrains Mono',monospace;font-size:12.5px}
- a{color:var(--gold)}
- button{font:inherit;cursor:pointer;border:none;background:none;color:inherit}
- .wrap{max-width:1100px;margin:0 auto;padding:0 20px 80px}
- /* ---------- header ---------- */
- header{display:flex;align-items:center;gap:14px;padding:26px 0 20px;flex-wrap:wrap}
- .logo{display:flex;align-items:center;gap:12px}
- .logo svg{display:block}
- .logo h1{font-size:26px}
- .logo .tag{font-size:12px;color:var(--ink-soft);margin-top:-2px}
- .chips{display:flex;gap:8px;flex-wrap:wrap;margin-left:auto;align-items:center}
- .chip{font-size:11.5px;font-weight:600;padding:4px 10px;border-radius:999px;
- border:1px solid var(--line);background:var(--bg-raise);color:var(--ink-soft)}
- .chip.hw{border-color:var(--gold);color:var(--gold);background:transparent}
- .theme-btn{width:38px;height:38px;border-radius:50%;border:1px solid var(--line);
- background:var(--bg-raise);display:grid;place-items:center;transition:transform .2s}
- .theme-btn:hover{transform:rotate(20deg)}
- /* ---------- cards ---------- */
- .card{background:var(--bg-raise);border:1px solid var(--line);border-radius:var(--radius);
- box-shadow:var(--shadow);padding:22px;margin-bottom:22px}
- .card h2{font-size:19px;margin-bottom:4px}
- .card .sub{color:var(--ink-soft);font-size:13px;margin-bottom:16px}
- /* ---------- dropzone ---------- */
- .drop{border:2px dashed var(--line);border-radius:var(--radius);padding:34px;
- text-align:center;color:var(--ink-soft);transition:.2s;cursor:pointer;background:var(--bg-sunken)}
- .drop.hover{border-color:var(--gold);color:var(--gold);background:var(--gold-soft)}
- .drop strong{color:var(--ink)}
- .upbar{margin-top:14px;display:none}
- .upbar.show{display:block}
- /* ---------- progress (signature: gold pour) ---------- */
- .bar{height:10px;border-radius:999px;background:var(--bg-sunken);overflow:hidden;
- border:1px solid var(--line);position:relative}
- .bar>i{display:block;height:100%;width:0;border-radius:999px;
- background:linear-gradient(90deg,var(--gold) 0%,var(--gold-bright) 50%,var(--gold) 100%);
- background-size:200% 100%;animation:pour 1.6s linear infinite;transition:width .4s ease}
- .bar.static>i{animation:none}
- @keyframes pour{from{background-position:0 0}to{background-position:-200% 0}}
- @media (prefers-reduced-motion:reduce){.bar>i{animation:none}}
- /* ---------- sources ---------- */
- .src-list{display:flex;flex-direction:column;gap:8px;margin-top:14px}
- .src{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--line);
- border-radius:10px;background:var(--bg-sunken);cursor:pointer;transition:.15s}
- .src:hover{border-color:var(--gold)}
- .src.sel{border-color:var(--gold);background:var(--gold-soft);box-shadow:inset 0 0 0 1px var(--gold)}
- .src .name{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
- .src .meta{margin-left:auto;color:var(--ink-soft);font-size:12px;white-space:nowrap}
- .src .del{color:var(--ink-soft);padding:2px 6px;border-radius:6px}
- .src .del:hover{color:var(--err)}
- /* ---------- tabs & fields ---------- */
- .tabs{display:flex;gap:2px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:6px 0 18px}
- .tab{padding:9px 14px;font-weight:600;font-size:13px;color:var(--ink-soft);
- border-radius:8px 8px 0 0;border-bottom:2px solid transparent;margin-bottom:-1px}
- .tab.on{color:var(--gold);border-bottom-color:var(--gold)}
- .pane{display:none}.pane.on{display:block}
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(215px,1fr));gap:14px 16px}
- .field label{display:block;font-size:12px;font-weight:600;color:var(--ink-soft);
- margin-bottom:5px;text-transform:uppercase;letter-spacing:.04em}
- .field input[type=text],.field input[type=number],.field select,.field textarea{
- width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:8px;
- background:var(--bg-sunken);color:var(--ink);font:inherit;font-size:13.5px}
- .field input:focus,.field select:focus,.field textarea:focus{
- outline:2px solid var(--gold);outline-offset:1px;border-color:var(--gold)}
- .field.wide{grid-column:1/-1}
- .check{display:flex;align-items:center;gap:8px;font-size:13.5px;padding:6px 0}
- .check input{accent-color:var(--gold);width:16px;height:16px}
- .hint{font-size:11.5px;color:var(--ink-soft);margin-top:4px}
- textarea{resize:vertical;min-height:70px}
- /* ---------- buttons ---------- */
- .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:10px;
- font-weight:600;font-size:14px;border:1px solid var(--line);background:var(--bg-sunken);
- color:var(--ink);transition:.15s}
- .btn:hover{border-color:var(--gold)}
- .btn.gold{background:var(--gold);border-color:var(--gold);color:#fff}
- [data-theme="dark"] .btn.gold{color:#221610}
- .btn.gold:hover{background:var(--gold-bright);border-color:var(--gold-bright)}
- .btn.sm{padding:6px 12px;font-size:12.5px;border-radius:8px}
- .btn.danger:hover{border-color:var(--err);color:var(--err)}
- .btn:disabled{opacity:.45;cursor:not-allowed}
- .actions{display:flex;gap:10px;align-items:center;margin-top:18px;flex-wrap:wrap}
- /* ---------- jobs ---------- */
- .job{border:1px solid var(--line);border-radius:12px;padding:16px;margin-top:12px;background:var(--bg-sunken)}
- .job .row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
- .job .fname{font-weight:600}
- .status{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
- padding:3px 9px;border-radius:999px;border:1px solid var(--line);color:var(--ink-soft)}
- .status.running,.status.scanning{color:var(--gold);border-color:var(--gold)}
- .status.done{color:var(--ok);border-color:var(--ok)}
- .status.failed{color:var(--err);border-color:var(--err)}
- .status.cancelled{color:var(--warn);border-color:var(--warn)}
- .job .stats{margin-left:auto;color:var(--ink-soft);font-size:12.5px;display:flex;gap:14px}
- .job .bar{margin-top:10px}
- .job .btns{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
- .job .errline{color:var(--err);font-size:12.5px;margin-top:8px}
- .logbox{display:none;margin-top:10px;background:var(--bg);border:1px solid var(--line);
- border-radius:8px;padding:10px;max-height:260px;overflow:auto;white-space:pre-wrap;font-size:11.5px}
- .logbox.show{display:block}
- .empty{color:var(--ink-soft);text-align:center;padding:26px;font-size:13.5px}
- .toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(20px);
- background:var(--ink);color:var(--bg);padding:10px 20px;border-radius:10px;font-size:13.5px;
- opacity:0;pointer-events:none;transition:.25s;z-index:50;box-shadow:var(--shadow)}
- .toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
- footer{color:var(--ink-soft);font-size:12px;text-align:center;padding-top:8px}
- @media(max-width:640px){.job .stats{margin-left:0;width:100%}}
- </style>
- </head>
- <body>
- <div class="wrap">
- <header>
- <div class="logo">
- <!-- coffee-ring / reel mark -->
- <svg width="42" height="42" viewBox="0 0 42 42" fill="none" aria-hidden="true">
- <circle cx="21" cy="21" r="17" stroke="var(--gold)" stroke-width="3.5"/>
- <circle cx="21" cy="21" r="9" stroke="var(--gold)" stroke-width="2" stroke-dasharray="4 5"/>
- <circle cx="21" cy="21" r="3" fill="var(--gold)"/>
- </svg>
- <div>
- <h1>WeBrake</h1>
- <div class="tag">HandBrake, poured over the web</div>
- </div>
- </div>
- <div class="chips" id="chips">
- <span class="chip" id="chip-hb">HandBrake …</span>
- <button class="theme-btn" id="themeBtn" title="Toggle dark roast" aria-label="Toggle theme">
- <svg id="themeIcon" width="18" height="18" viewBox="0 0 24 24" fill="none"
- stroke="currentColor" stroke-width="2" stroke-linecap="round"></svg>
- </button>
- </div>
- </header>
- <!-- ============ upload ============ -->
- <section class="card">
- <h2>1 · Pour in your media</h2>
- <div class="sub">Drop a video file to upload it to the container for remuxing or re-encoding.</div>
- <div class="drop" id="drop" tabindex="0" role="button" aria-label="Upload a video file">
- <strong>Drop a file here</strong> or click to browse
- <div class="hint">MKV, MP4, AVI, TS, MOV, WebM — anything HandBrake can scan.</div>
- </div>
- <input type="file" id="fileInput" hidden>
- <div class="upbar" id="upbar">
- <div class="bar"><i id="upfill"></i></div>
- <div class="hint" id="uptext">Uploading…</div>
- </div>
- <div class="src-list" id="srcList"></div>
- </section>
- <!-- ============ options ============ -->
- <section class="card">
- <h2>2 · Dial in the brew</h2>
- <div class="sub">Every HandBrake feature is here — pick a preset, fine-tune the groups, or drop raw flags in Advanced.</div>
- <div class="grid" style="margin-bottom:4px">
- <div class="field">
- <label for="preset">Preset</label>
- <select id="preset"><option value="">(none — manual settings)</option></select>
- </div>
- <div class="field">
- <label for="o_format">Container</label>
- <select id="o_format">
- <option value="av_mkv">MKV (av_mkv)</option>
- <option value="av_mp4">MP4 (av_mp4)</option>
- <option value="av_webm">WebM (av_webm)</option>
- </select>
- </div>
- <div class="field">
- <label for="o_output_name">Output name (no extension)</label>
- <input type="text" id="o_output_name" placeholder="defaults to source name">
- </div>
- </div>
- <div class="tabs" id="tabs">
- <button class="tab on" data-pane="p-src">Source</button>
- <button class="tab" data-pane="p-vid">Video</button>
- <button class="tab" data-pane="p-aud">Audio</button>
- <button class="tab" data-pane="p-sub">Subtitles</button>
- <button class="tab" data-pane="p-pic">Picture</button>
- <button class="tab" data-pane="p-fil">Filters</button>
- <button class="tab" data-pane="p-adv">Advanced</button>
- </div>
- <!-- Source -->
- <div class="pane on" id="p-src"><div class="grid">
- <div class="field"><label>Title #</label><input type="number" id="o_title" min="0" placeholder="auto"></div>
- <div class="field"><label>Chapters</label><input type="text" id="o_chapters" placeholder="e.g. 1-3"></div>
- <div class="field"><label>Angle</label><input type="number" id="o_angle" min="1" placeholder=""></div>
- <div class="field"><label>Min duration (s)</label><input type="number" id="o_min_duration" placeholder=""></div>
- <div class="field"><label>Start at</label><input type="text" id="o_start_at" placeholder="seconds:30 / frames:100 / duration:00:01:00"></div>
- <div class="field"><label>Stop at</label><input type="text" id="o_stop_at" placeholder="same units as start"></div>
- <div class="field"><label>Previews</label><input type="text" id="o_previews" placeholder="e.g. 30:1"></div>
- <div class="field"><label>Start at preview #</label><input type="number" id="o_start_at_preview" placeholder=""></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_main_feature"> Main feature title only (--main-feature)</label>
- <label class="check"><input type="checkbox" id="o_markers" checked> Chapter markers (--markers)</label>
- <label class="check"><input type="checkbox" id="o_optimize"> Web optimized / fast start (--optimize)</label>
- <label class="check"><input type="checkbox" id="o_align_av"> Align A/V start (--align-av)</label>
- <label class="check"><input type="checkbox" id="o_inline_parameter_sets"> Inline parameter sets (--inline-parameter-sets)</label>
- </div>
- <div class="field wide" id="scanBox">
- <button class="btn sm" id="scanBtn" type="button">Scan source for titles & tracks</button>
- <div class="hint mono" id="scanOut"></div>
- </div>
- </div></div>
- <!-- Video -->
- <div class="pane" id="p-vid"><div class="grid">
- <div class="field"><label>Encoder</label><select id="o_encoder"><option value="">(preset default)</option></select>
- <div class="hint" id="hwHint"></div></div>
- <div class="field"><label>Encoder preset</label><input type="text" id="o_encoder_preset" placeholder="e.g. medium / p5 / quality"></div>
- <div class="field"><label>Encoder tune</label><input type="text" id="o_encoder_tune" placeholder="e.g. film, animation"></div>
- <div class="field"><label>Profile</label><input type="text" id="o_encoder_profile" placeholder="e.g. main, high, main10"></div>
- <div class="field"><label>Level</label><input type="text" id="o_encoder_level" placeholder="e.g. 4.1"></div>
- <div class="field"><label>Quality (CQ/RF)</label><input type="number" id="o_quality" step="0.25" placeholder="e.g. 22"></div>
- <div class="field"><label>Bitrate kb/s (--vb)</label><input type="number" id="o_vb" placeholder="overrides quality"></div>
- <div class="field"><label>Framerate (--rate)</label><input type="text" id="o_rate" placeholder="e.g. 23.976, 30"></div>
- <div class="field"><label>Framerate mode</label>
- <select id="o_fr_mode"><option value="">source default</option>
- <option value="cfr">Constant (--cfr)</option>
- <option value="vfr">Variable (--vfr)</option>
- <option value="pfr">Peak (--pfr)</option></select></div>
- <div class="field wide"><label>Extra encoder options (--encopts)</label>
- <input type="text" class="mono" id="o_encopts" placeholder="key=value:key=value"></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_two_pass"> Two-pass encode (--two-pass)</label>
- <label class="check"><input type="checkbox" id="o_turbo"> Turbo first pass (--turbo)</label>
- <label class="check"><input type="checkbox" id="o_hwdec"> Hardware decoding (--enable-hw-decoding)</label>
- </div>
- </div></div>
- <!-- Audio -->
- <div class="pane" id="p-aud"><div class="grid">
- <div class="field"><label>Tracks (--audio)</label><input type="text" id="o_audio" placeholder="e.g. 1,2 or leave blank"></div>
- <div class="field"><label>Language list</label><input type="text" id="o_audio_lang_list" placeholder="e.g. eng,jpn"></div>
- <div class="field"><label>Encoder(s) (--aencoder)</label><input type="text" id="o_aencoder" placeholder="e.g. copy, av_aac, opus, flac"></div>
- <div class="field"><label>Copy mask</label><input type="text" id="o_audio_copy_mask" placeholder="e.g. aac,ac3,dtshd,truehd"></div>
- <div class="field"><label>Fallback encoder</label><input type="text" id="o_audio_fallback" placeholder="e.g. av_aac"></div>
- <div class="field"><label>Bitrate(s) kb/s (--ab)</label><input type="text" id="o_ab" placeholder="e.g. 160,256"></div>
- <div class="field"><label>Quality(ies) (--aq)</label><input type="text" id="o_aq" placeholder=""></div>
- <div class="field"><label>Compression (--ac)</label><input type="text" id="o_ac" placeholder=""></div>
- <div class="field"><label>Mixdown</label><input type="text" id="o_mixdown" placeholder="e.g. stereo, 5point1"></div>
- <div class="field"><label>Normalize mix</label><input type="text" id="o_normalize_mix" placeholder="0 or 1"></div>
- <div class="field"><label>Samplerate kHz (--arate)</label><input type="text" id="o_arate" placeholder="e.g. 48"></div>
- <div class="field"><label>DRC (--drc)</label><input type="text" id="o_drc" placeholder="1.0–4.0"></div>
- <div class="field"><label>Gain dB</label><input type="text" id="o_gain" placeholder="e.g. 2"></div>
- <div class="field"><label>Dither (--adither)</label><input type="text" id="o_adither" placeholder="auto / none / …"></div>
- <div class="field wide"><label>Track name(s) (--aname)</label><input type="text" id="o_aname" placeholder="comma-separated"></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_all_audio"> Include all audio tracks (--all-audio)</label>
- <label class="check"><input type="checkbox" id="o_first_audio"> First audio track only (--first-audio)</label>
- </div>
- </div></div>
- <!-- Subtitles -->
- <div class="pane" id="p-sub"><div class="grid">
- <div class="field"><label>Tracks (--subtitle)</label><input type="text" id="o_subtitle" placeholder="e.g. 1,2 / scan"></div>
- <div class="field"><label>Language list</label><input type="text" id="o_subtitle_lang_list" placeholder="e.g. eng"></div>
- <div class="field"><label>Forced only (--subtitle-forced)</label><input type="text" id="o_subtitle_forced" placeholder="track #s or blank"></div>
- <div class="field"><label>Burn in (--subtitle-burned)</label><input type="text" id="o_subtitle_burned" placeholder="track # / native / none"></div>
- <div class="field"><label>Default track</label><input type="text" id="o_subtitle_default" placeholder="track # / none"></div>
- <div class="field"><label>Track name(s) (--subname)</label><input type="text" id="o_subname" placeholder=""></div>
- <div class="field"><label>Native language</label><input type="text" id="o_native_language" placeholder="e.g. eng"></div>
- <div class="field wide"><label>External SRT file path (--srt-file)</label>
- <input type="text" class="mono" id="o_srt_file" placeholder="/var/lib/webrake/uploads/…"></div>
- <div class="field"><label>SRT codeset</label><input type="text" id="o_srt_codeset" placeholder="UTF-8"></div>
- <div class="field"><label>SRT offset ms</label><input type="text" id="o_srt_offset" placeholder=""></div>
- <div class="field"><label>SRT language</label><input type="text" id="o_srt_lang" placeholder="e.g. eng"></div>
- <div class="field"><label>SRT burn (track #)</label><input type="text" id="o_srt_burn" placeholder=""></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_all_subtitles"> Include all subtitles (--all-subtitles)</label>
- <label class="check"><input type="checkbox" id="o_first_subtitle"> First subtitle only (--first-subtitle)</label>
- <label class="check"><input type="checkbox" id="o_native_dub"> Prefer native-language dub (--native-dub)</label>
- </div>
- </div></div>
- <!-- Picture -->
- <div class="pane" id="p-pic"><div class="grid">
- <div class="field"><label>Width</label><input type="number" id="o_width" placeholder=""></div>
- <div class="field"><label>Height</label><input type="number" id="o_height" placeholder=""></div>
- <div class="field"><label>Max width</label><input type="number" id="o_maxWidth" placeholder=""></div>
- <div class="field"><label>Max height</label><input type="number" id="o_maxHeight" placeholder=""></div>
- <div class="field"><label>Crop T:B:L:R</label><input type="text" id="o_crop" placeholder="e.g. 0:0:0:0"></div>
- <div class="field"><label>Crop mode</label>
- <select id="o_crop_mode"><option value="">default</option>
- <option>auto</option><option>conservative</option><option>none</option></select></div>
- <div class="field"><label>Anamorphic</label>
- <select id="o_anamorphic"><option value="">default</option>
- <option value="auto_anamorphic">auto</option>
- <option value="loose_anamorphic">loose</option>
- <option value="custom_anamorphic">custom</option>
- <option value="non_anamorphic">none</option></select></div>
- <div class="field"><label>Display width</label><input type="number" id="o_display_width" placeholder=""></div>
- <div class="field"><label>Pixel aspect X:Y</label><input type="text" id="o_pixel_aspect" placeholder="e.g. 1:1"></div>
- <div class="field"><label>Modulus</label><input type="number" id="o_modulus" placeholder="2"></div>
- <div class="field"><label>Color matrix</label><input type="text" id="o_color_matrix" placeholder="709 / 601 / 2020 / …"></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_keep_display_aspect"> Keep display aspect (--keep-display-aspect)</label>
- </div>
- </div></div>
- <!-- Filters -->
- <div class="pane" id="p-fil"><div class="grid">
- <div class="field"><label>Comb detect</label><input type="text" id="o_comb_detect" placeholder="blank=off · 'default' or settings"></div>
- <div class="field"><label>Deinterlace</label><input type="text" id="o_deinterlace" placeholder="'default' or preset/settings"></div>
- <div class="field"><label>Decomb</label><input type="text" id="o_decomb" placeholder="'default' or settings"></div>
- <div class="field"><label>Detelecine</label><input type="text" id="o_detelecine" placeholder="'default' or settings"></div>
- <div class="field"><label>Denoise hqdn3d</label><input type="text" id="o_hqdn3d" placeholder="light / medium / strong / custom"></div>
- <div class="field"><label>Denoise NLMeans</label><input type="text" id="o_nlmeans" placeholder="ultralight … strong"></div>
- <div class="field"><label>NLMeans tune</label><input type="text" id="o_nlmeans_tune" placeholder="film / grain / …"></div>
- <div class="field"><label>Chroma smooth</label><input type="text" id="o_chroma_smooth" placeholder=""></div>
- <div class="field"><label>Unsharp</label><input type="text" id="o_unsharp" placeholder="light / medium / strong"></div>
- <div class="field"><label>Lapsharp</label><input type="text" id="o_lapsharp" placeholder=""></div>
- <div class="field"><label>Deblock</label><input type="text" id="o_deblock" placeholder=""></div>
- <div class="field"><label>Rotate</label><input type="text" id="o_rotate" placeholder="angle=90:hflip=0"></div>
- <div class="field"><label>Pad</label><input type="text" id="o_pad" placeholder="W:H:color:X:Y"></div>
- <div class="field"><label>Colorspace</label><input type="text" id="o_colorspace" placeholder="bt709 / bt2020 / …"></div>
- <div class="field wide">
- <label class="check"><input type="checkbox" id="o_grayscale"> Grayscale (--grayscale)</label>
- </div>
- <div class="hint wide field">Filter fields map to HandBrake's optional-value flags: enter <code>default</code> to enable with defaults, or paste full settings strings.</div>
- </div></div>
- <!-- Advanced -->
- <div class="pane" id="p-adv"><div class="grid">
- <div class="field wide"><label>Raw HandBrakeCLI flags — everything else lives here</label>
- <textarea id="o_extra_args" class="mono" placeholder="--preset-import-file /path/custom.json --subtitle-lang-list eng --queue-import-file …"></textarea>
- <div class="hint">Appended verbatim to the command line, so the entire HandBrake flag surface is available. See <code>HandBrakeCLI --help</code>.</div>
- </div>
- <div class="field wide"><label>Command preview</label>
- <textarea id="cmdPreview" class="mono" readonly rows="3"></textarea></div>
- </div></div>
- <div class="actions">
- <button class="btn gold" id="startBtn">Start encode</button>
- <span class="hint" id="startHint">Select a source above first.</span>
- </div>
- </section>
- <!-- ============ queue ============ -->
- <section class="card">
- <h2>3 · The counter</h2>
- <div class="sub">Live queue — watch progress, then download the finished cup back to this PC.</div>
- <div id="jobs"><div class="empty">Nothing brewing yet.</div></div>
- </section>
- <footer>WeBrake · <span id="verline"></span> · <a href="https://gogs.av2x.dev/av2x/WeBrake">source</a></footer>
- </div>
- <div class="toast" id="toast"></div>
- <script>
- "use strict";
- /* ---------------- helpers ---------------- */
- const $ = s => document.querySelector(s);
- const $$ = s => [...document.querySelectorAll(s)];
- let API_TOKEN = localStorage.getItem("webrake_token") || "";
- function headers(extra){ const h = extra||{}; if(API_TOKEN) h["X-API-Token"]=API_TOKEN; return h; }
- async function api(path, opts={}){
- opts.headers = headers(opts.headers);
- const r = await fetch(path, opts);
- if(r.status===401){
- API_TOKEN = prompt("This WeBrake requires an API token (see token.json in /opt/webrake):")||"";
- localStorage.setItem("webrake_token", API_TOKEN);
- if(API_TOKEN) return api(path, opts);
- }
- if(!r.ok){ let e={}; try{e=await r.json()}catch{} throw new Error(e.error||r.statusText); }
- return r;
- }
- function toast(msg){ const t=$("#toast"); t.textContent=msg; t.classList.add("show");
- clearTimeout(t._h); t._h=setTimeout(()=>t.classList.remove("show"),2600); }
- function fmtSize(b){ if(b>1e9)return (b/1e9).toFixed(2)+" GB"; if(b>1e6)return (b/1e6).toFixed(1)+" MB";
- return (b/1e3).toFixed(0)+" KB"; }
- function fmtEta(s){ if(!s)return ""; const h=~~(s/3600),m=~~(s%3600/60),ss=~~(s%60);
- return (h?h+"h ":"")+(m?m+"m ":"")+ss+"s"; }
- function esc(x){ const d=document.createElement("div"); d.textContent=x??""; return d.innerHTML; }
- /* ---------------- theme ---------------- */
- const sunPath = '<circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>';
- const moonPath = '<path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/>';
- function setTheme(t){
- document.documentElement.dataset.theme=t;
- localStorage.setItem("webrake_theme",t);
- $("#themeIcon").innerHTML = t==="dark" ? sunPath : moonPath;
- }
- setTheme(localStorage.getItem("webrake_theme") ||
- (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"));
- $("#themeBtn").onclick = () =>
- setTheme(document.documentElement.dataset.theme==="dark" ? "light" : "dark");
- /* ---------------- tabs ---------------- */
- $$("#tabs .tab").forEach(t => t.onclick = () => {
- $$("#tabs .tab").forEach(x=>x.classList.remove("on"));
- $$(".pane").forEach(x=>x.classList.remove("on"));
- t.classList.add("on"); $("#"+t.dataset.pane).classList.add("on");
- });
- /* ---------------- capabilities / presets ---------------- */
- let CAPS = {};
- async function loadMeta(){
- try{
- const v = await (await api("/api/version")).json();
- $("#verline").textContent = "v"+v.webrake+" · HandBrake "+(v.handbrake||"not found");
- CAPS = await (await api("/api/capabilities")).json();
- $("#chip-hb").textContent = CAPS.handbrake_found
- ? "HandBrake "+(CAPS.handbrake_version||"") : "HandBrake missing!";
- const chips = $("#chips"), btn = $("#themeBtn");
- for(const [k,label] of [["qsv","Intel QSV"],["nvenc","NVENC"],["vce","AMD VCE"],["vaapi","VAAPI"]]){
- if(CAPS[k]){ const c=document.createElement("span"); c.className="chip hw";
- c.textContent="⚡ "+label; chips.insertBefore(c,btn); }
- }
- const sel = $("#o_encoder");
- (CAPS.encoders||[]).forEach(e=>{
- const o=document.createElement("option"); o.value=e;
- o.textContent=(CAPS.hw_encoders||[]).includes(e) ? "⚡ "+e+" (GPU)" : e;
- sel.appendChild(o);
- });
- const hw=(CAPS.hw_encoders||[]);
- $("#hwHint").textContent = hw.length
- ? "GPU passthru detected — ⚡ encoders run on hardware."
- : "No hardware encoders detected; software encoders only.";
- const p = await (await api("/api/presets")).json();
- let lastCat=null, group=null, ps=$("#preset");
- (p.presets||[]).forEach(pr=>{
- if(pr.category!==lastCat){ group=document.createElement("optgroup");
- group.label=pr.category; ps.appendChild(group); lastCat=pr.category; }
- const o=document.createElement("option"); o.value=pr.name; o.textContent=pr.name;
- group.appendChild(o);
- });
- }catch(e){ toast("Backend unreachable: "+e.message); }
- }
- /* ---------------- uploads ---------------- */
- let SELECTED = null;
- const drop=$("#drop"), fi=$("#fileInput");
- drop.onclick=()=>fi.click();
- drop.onkeydown=e=>{ if(e.key==="Enter"||e.key===" ") fi.click(); };
- ["dragover","dragenter"].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.add("hover");}));
- ["dragleave","drop"].forEach(ev=>drop.addEventListener(ev,e=>{e.preventDefault();drop.classList.remove("hover");}));
- drop.addEventListener("drop",e=>{ if(e.dataTransfer.files.length) upload(e.dataTransfer.files[0]); });
- fi.onchange=()=>{ if(fi.files.length) upload(fi.files[0]); fi.value=""; };
- function upload(file){
- const xhr=new XMLHttpRequest(), fd=new FormData(); fd.append("file",file);
- $("#upbar").classList.add("show");
- const fill=$("#upfill"), txt=$("#uptext");
- xhr.upload.onprogress=e=>{
- if(e.lengthComputable){
- const pct=(e.loaded/e.total*100);
- fill.style.width=pct.toFixed(1)+"%";
- txt.textContent=`Uploading ${file.name} — ${pct.toFixed(1)}% of ${fmtSize(e.total)}`;
- }};
- xhr.onload=()=>{
- $("#upbar").classList.remove("show"); fill.style.width="0";
- if(xhr.status===200){ toast("Uploaded "+file.name); refreshUploads(); }
- else toast("Upload failed ("+xhr.status+")");
- };
- xhr.onerror=()=>{ $("#upbar").classList.remove("show"); toast("Upload failed"); };
- xhr.open("POST","/api/upload");
- if(API_TOKEN) xhr.setRequestHeader("X-API-Token",API_TOKEN);
- xhr.send(fd);
- }
- async function refreshUploads(){
- try{
- const d = await (await api("/api/uploads")).json();
- const list=$("#srcList"); list.innerHTML="";
- d.uploads.forEach(u=>{
- const row=document.createElement("div");
- row.className="src"+(SELECTED===u.upload_id?" sel":"");
- row.innerHTML=`<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--gold)" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m10 9 5 3-5 3z" fill="var(--gold)"/></svg>
- <span class="name">${esc(u.filename)}</span>
- <span class="meta">${fmtSize(u.size)}</span>
- <button class="del" title="Delete upload" aria-label="Delete upload">✕</button>`;
- row.onclick=e=>{
- if(e.target.classList.contains("del")) return;
- SELECTED=u.upload_id; refreshUploads(); updateStartState(); previewCmd();
- };
- row.querySelector(".del").onclick=async ()=>{
- await api("/api/uploads/"+encodeURIComponent(u.upload_id),{method:"DELETE"});
- if(SELECTED===u.upload_id){SELECTED=null;updateStartState();}
- refreshUploads(); toast("Upload deleted");
- };
- list.appendChild(row);
- });
- if(!d.uploads.length) list.innerHTML='<div class="empty">No sources uploaded yet.</div>';
- }catch(e){/* backend down; header toast already covers it */}
- }
- function updateStartState(){
- $("#startBtn").disabled=!SELECTED;
- $("#startHint").textContent=SELECTED?"":"Select a source above first.";
- }
- /* ---------------- scan ---------------- */
- $("#scanBtn").onclick=async ()=>{
- if(!SELECTED){ toast("Select a source first"); return; }
- $("#scanOut").textContent="Scanning…";
- try{
- const d = await (await api("/api/scan/"+encodeURIComponent(SELECTED))).json();
- const lines=(d.TitleList||[]).map(t=>
- `Title ${t.Index}: ${t.Duration?`${t.Duration.Hours}:${String(t.Duration.Minutes).padStart(2,"0")}:${String(t.Duration.Seconds).padStart(2,"0")}`:""} · `+
- `${t.Geometry?t.Geometry.Width+"×"+t.Geometry.Height:""} · `+
- `${(t.AudioList||[]).length} audio · ${(t.SubtitleList||[]).length} subs`);
- $("#scanOut").textContent=lines.join("\n")||"No titles found.";
- }catch(e){ $("#scanOut").textContent="Scan failed: "+e.message; }
- };
- /* ---------------- options collection ---------------- */
- const TEXT_KEYS=["title","chapters","angle","min_duration","start_at","stop_at","previews",
- "start_at_preview","encoder","encoder_preset","encoder_tune","encoder_profile","encoder_level",
- "quality","vb","rate","encopts","audio","audio_lang_list","aencoder","audio_copy_mask",
- "audio_fallback","ab","aq","ac","mixdown","normalize_mix","arate","drc","gain","adither","aname",
- "subtitle","subtitle_lang_list","subtitle_forced","subtitle_burned","subtitle_default","subname",
- "native_language","srt_file","srt_codeset","srt_offset","srt_lang","srt_burn",
- "width","height","maxWidth","maxHeight","crop","crop_mode","display_width","pixel_aspect",
- "modulus","color_matrix","comb_detect","deinterlace","decomb","detelecine","hqdn3d","nlmeans",
- "nlmeans_tune","chroma_smooth","unsharp","lapsharp","deblock","rotate","pad","colorspace",
- "output_name","extra_args","preset_import_file"];
- const BOOL_KEYS=["main_feature","markers","optimize","align_av","inline_parameter_sets",
- "two_pass","turbo","all_audio","first_audio","all_subtitles","first_subtitle","native_dub",
- "keep_display_aspect","grayscale"];
- const FILTER_KEYS=new Set(["comb_detect","deinterlace","decomb","detelecine","hqdn3d","nlmeans",
- "chroma_smooth","unsharp","lapsharp","deblock","rotate","subtitle_forced","subtitle_burned",
- "subtitle_default","srt_burn"]);
- function collectOptions(){
- const o={};
- const preset=$("#preset").value; if(preset) o.preset=preset;
- o.format=$("#o_format").value;
- for(const k of TEXT_KEYS){
- const el=document.getElementById("o_"+k); if(!el) continue;
- let v=el.value.trim(); if(v==="") continue;
- if(FILTER_KEYS.has(k) && v.toLowerCase()==="default") v=true;
- o[k]=v;
- }
- for(const k of BOOL_KEYS){
- const el=document.getElementById("o_"+k);
- if(el && el.checked) o[k]=true;
- }
- const fr=$("#o_fr_mode").value; if(fr) o[fr]=true;
- const an=$("#o_anamorphic").value; if(an) o[an]=true;
- if($("#o_hwdec").checked) o.enable_hw_decoding =
- CAPS.nvenc ? "nvdec" : (CAPS.qsv ? "qsv" : "videotoolbox");
- return o;
- }
- function previewCmd(){
- const o=collectOptions(), parts=["HandBrakeCLI --json -i <source> -o <output>"];
- for(const [k,v] of Object.entries(o)){
- if(k==="output_name") continue;
- const flag="--"+k.replace(/_/g,"-");
- if(v===true) parts.push(flag);
- else if(k==="extra_args") parts.push(v);
- else parts.push(flag+" "+v);
- }
- $("#cmdPreview").value=parts.join(" ");
- }
- document.addEventListener("input",previewCmd);
- document.addEventListener("change",previewCmd);
- /* ---------------- jobs ---------------- */
- $("#startBtn").onclick=async ()=>{
- if(!SELECTED) return;
- try{
- await api("/api/jobs",{method:"POST",
- headers:{"Content-Type":"application/json"},
- body:JSON.stringify({upload_id:SELECTED,options:collectOptions()})});
- toast("Job queued"); refreshJobs();
- }catch(e){ toast("Could not queue job: "+e.message); }
- };
- const openLogs=new Set();
- async function refreshJobs(){
- try{
- const d=await (await api("/api/jobs")).json();
- const box=$("#jobs");
- if(!d.jobs.length){ box.innerHTML='<div class="empty">Nothing brewing yet.</div>'; return; }
- box.innerHTML="";
- d.jobs.forEach(j=>{
- const el=document.createElement("div"); el.className="job"; el.dataset.id=j.id;
- const running=["running","scanning","queued"].includes(j.status);
- const stats=[];
- if(j.fps) stats.push(j.fps+" fps");
- if(j.eta) stats.push("ETA "+fmtEta(j.eta));
- if(j.pass&&j.pass>0) stats.push("pass "+j.pass);
- el.innerHTML=`
- <div class="row">
- <span class="fname">${esc(j.filename)}</span>
- <span class="status ${j.status}">${j.status}</span>
- <span class="stats">${stats.map(esc).join(" · ")}</span>
- </div>
- <div class="bar ${running?"":"static"}"><i style="width:${j.progress||0}%"></i></div>
- ${j.error?`<div class="errline">${esc(j.error)}</div>`:""}
- <div class="btns">
- ${j.status==="done"?`<a class="btn sm gold" href="/api/download/${j.id}${API_TOKEN?"?token="+encodeURIComponent(API_TOKEN):""}" download>Download</a>`:""}
- ${running?`<button class="btn sm danger" data-act="cancel">Cancel</button>`:""}
- <button class="btn sm" data-act="log">${openLogs.has(j.id)?"Hide log":"Log"}</button>
- ${!running?`<button class="btn sm danger" data-act="del">Delete</button>`:""}
- </div>
- <pre class="logbox ${openLogs.has(j.id)?"show":""}"></pre>`;
- el.querySelectorAll("[data-act]").forEach(b=>b.onclick=async ()=>{
- const act=b.dataset.act;
- if(act==="cancel"){ await api(`/api/jobs/${j.id}/cancel`,{method:"POST"}); toast("Cancelling…"); }
- if(act==="del"){ await api(`/api/jobs/${j.id}`,{method:"DELETE"}); toast("Job deleted"); }
- if(act==="log"){ openLogs.has(j.id)?openLogs.delete(j.id):openLogs.add(j.id); }
- refreshJobs();
- });
- if(openLogs.has(j.id)){
- api(`/api/jobs/${j.id}/log`).then(r=>r.text()).then(t=>{
- const lb=el.querySelector(".logbox"); lb.textContent=t; lb.scrollTop=lb.scrollHeight;
- }).catch(()=>{});
- }
- box.appendChild(el);
- });
- }catch(e){/* transient */}
- }
- /* ---------------- boot ---------------- */
- loadMeta(); refreshUploads(); refreshJobs(); updateStartState(); previewCmd();
- setInterval(refreshJobs,2000);
- setInterval(refreshUploads,15000);
- </script>
- </body>
- </html>
|