// app.js — HarmoSplit フロントエンドロジック(Stripe 認証対応) const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const startBtn = document.getElementById('startBtn'); const instVol = document.getElementById('instVol'); const instVolLabel= document.getElementById('instVolLabel'); const modelSelect = document.getElementById('modelSelect'); const useMdx = document.getElementById('useMdx'); const uploadSection = document.getElementById('uploadSection'); const progressSection= document.getElementById('progressSection'); const resultSection = document.getElementById('resultSection'); const errorSection = document.getElementById('errorSection'); const progressBar = document.getElementById('progressBar'); const progressPct = document.getElementById('progressPct'); const progressFile = document.getElementById('progressFile'); const logBox = document.getElementById('logBox'); const downloadBtn = document.getElementById('downloadBtn'); const errorMsg = document.getElementById('errorMsg'); let selectedFile = null; let verifiedToken = ''; // ── 起動時: Stripe 有効かどうか確認 ──────────────────────── async function initAuth() { try { const res = await fetch('/auth-mode'); const data = await res.json(); if (!data.free_mode) { // 有料モード: トークンエリアを表示 document.getElementById('tokenArea').style.display = 'block'; // ローカルストレージに保存済みトークンがあれば復元 const saved = localStorage.getItem('hmsplit_token'); if (saved) { document.getElementById('tokenInput').value = saved; await verifyToken(saved, false); } } else { // 無料モード: トークン不要 verifiedToken = 'FREE'; } } catch (e) { verifiedToken = 'FREE'; // エラー時は無料扱い } } // ── トークン検証 ───────────────────────────────────────────── async function verifyToken(token, showAlert = true) { try { const res = await fetch('/verify-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }), }); const data = await res.json(); if (data.valid) { verifiedToken = token; localStorage.setItem('hmsplit_token', token); document.getElementById('tokenOk').classList.remove('hidden'); document.getElementById('tokenInput').style.borderColor = 'var(--success)'; } else { verifiedToken = ''; localStorage.removeItem('hmsplit_token'); document.getElementById('tokenOk').classList.add('hidden'); document.getElementById('tokenInput').style.borderColor = 'var(--error)'; if (showAlert) alert('トークンが無効です。料金ページから登録してください。'); } return data.valid; } catch { return false; } } document.getElementById('verifyBtn')?.addEventListener('click', async () => { const token = document.getElementById('tokenInput').value.trim(); if (!token) { alert('トークンを入力してください'); return; } await verifyToken(token, true); }); // ── ファイル選択 ────────────────────────────────────────────── function selectFile(file) { selectedFile = file; dropZone.classList.add('has-file'); dropZone.querySelector('.drop-icon').textContent = fileIsVideo(file.name) ? '🎬' : '🎵'; const sub = dropZone.querySelector('.drop-sub'); sub.textContent = file.name; sub.classList.add('drop-filename'); updateStartBtn(); } function fileIsVideo(name) { return /\.(mp4|mov|avi|mkv|m4v|flv|webm|ts)$/i.test(name); } function updateStartBtn() { const hasFile = !!selectedFile; const hasToken = !!verifiedToken; startBtn.disabled = !(hasFile && hasToken); } dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); }); dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); dropZone.addEventListener('drop', e => { e.preventDefault(); dropZone.classList.remove('dragover'); if (e.dataTransfer.files.length) selectFile(e.dataTransfer.files[0]); }); dropZone.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', () => { if (fileInput.files.length) selectFile(fileInput.files[0]); }); instVol.addEventListener('input', () => { instVolLabel.textContent = Math.round(instVol.value * 100) + '%'; }); // ── 処理開始 ────────────────────────────────────────────────── startBtn.addEventListener('click', async () => { if (!selectedFile || !verifiedToken) return; uploadSection.classList.add('hidden'); progressSection.classList.remove('hidden'); logBox.innerHTML = ''; progressBar.style.width = '0%'; progressPct.textContent = '0%'; progressFile.textContent = selectedFile.name; const fd = new FormData(); fd.append('file', selectedFile); fd.append('inst_vol', instVol.value); fd.append('model', modelSelect.value); fd.append('use_mdx', useMdx.checked ? 'true' : 'false'); fd.append('token', verifiedToken === 'FREE' ? '' : verifiedToken); let jobId; try { const res = await fetch('/upload', { method: 'POST', body: fd }); const data = await res.json(); if (!res.ok || data.error) throw new Error(data.error || 'アップロード失敗'); jobId = data.job_id; } catch (e) { showError(e.message); return; } const sse = new EventSource(`/progress/${jobId}`); sse.addEventListener('message', e => { let payload; try { payload = JSON.parse(e.data); } catch { return; } if (payload.status) { sse.close(); if (payload.status === 'done') showResult(jobId); else showError('処理中にエラーが発生しました。ログを確認してください。'); return; } if (payload.msg) { const p = document.createElement('p'); p.textContent = payload.msg; if (payload.msg.includes('✅') || payload.msg.includes('完了')) p.classList.add('ok'); if (payload.msg.includes('❌')) p.classList.add('err'); logBox.appendChild(p); logBox.scrollTop = logBox.scrollHeight; } if (payload.pct !== undefined) { progressBar.style.width = payload.pct + '%'; progressPct.textContent = payload.pct + '%'; } }); sse.onerror = () => { sse.close(); setTimeout(async () => { try { const res = await fetch(`/status/${jobId}`); const data = await res.json(); if (data.status === 'done') showResult(jobId); else showError(data.error || '接続エラー'); } catch { showError('サーバーとの接続が切れました'); } }, 500); }; }); function showResult(jobId) { progressSection.classList.add('hidden'); resultSection.classList.remove('hidden'); downloadBtn.href = `/download/${jobId}`; } function showError(msg) { progressSection.classList.add('hidden'); errorSection.classList.remove('hidden'); errorMsg.textContent = msg; } function reset() { selectedFile = null; fileInput.value = ''; dropZone.classList.remove('has-file', 'dragover'); dropZone.querySelector('.drop-icon').textContent = '🎵'; const sub = dropZone.querySelector('.drop-sub'); sub.textContent = 'または'; sub.classList.remove('drop-filename'); progressBar.style.width = '0%'; progressPct.textContent = '0%'; logBox.innerHTML = ''; uploadSection.classList.remove('hidden'); progressSection.classList.add('hidden'); resultSection.classList.add('hidden'); errorSection.classList.add('hidden'); updateStartBtn(); } document.getElementById('resetBtn').addEventListener('click', reset); document.getElementById('resetBtnErr').addEventListener('click', reset); // 起動 initAuth().then(() => updateStartBtn());