import 'dotenv/config'; import readline from 'readline'; import fs from 'fs'; const SID = process.env.BYSE_SID; if (!SID) { console.error('BYSE_SID fehlt in .env'); process.exit(1); } const COOKIE = `sid=${SID}`; const BASE = 'https://byse.sx/api'; const PER_PAGE = 500; const DELETE_BATCH = 100; const THREE_MONTHS_AGO = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // ── Helpers ── function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'; return (bytes / 1073741824).toFixed(2) + ' GB'; } function formatAge(dateStr) { const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000); if (days < 30) return `${days}d`; if (days < 365) return `${Math.floor(days / 30)}mo`; return `${(days / 365).toFixed(1)}y`; } function ask(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise(r => rl.question(question, a => { rl.close(); r(a.trim()); })); } async function apiFetch(path) { const res = await fetch(`${BASE}${path}`, { headers: { cookie: COOKIE, 'content-type': 'application/json' } }); if (res.status === 401 || res.status === 403) { console.error('\nSession abgelaufen! Hol dir einen neuen SID-Cookie aus dem Browser.'); process.exit(1); } return res.json(); } async function apiDelete(fileIds) { const res = await fetch(`${BASE}/del_file`, { method: 'POST', headers: { cookie: COOKIE, 'content-type': 'application/json' }, body: JSON.stringify({ file_ids: fileIds }) }); return res.json(); } function progressBar(current, total, width = 30) { const pct = Math.floor((current / total) * 100); const filled = Math.floor((current / total) * width); const bar = '█'.repeat(filled) + '░'.repeat(width - filled); return `[${bar}] ${pct}% (${current}/${total})`; } // ── Scan ── async function scanAllVideos() { console.log('\nVerbindung pruefen...'); const overview = await apiFetch('/my_files_overview'); if (overview.status !== 'ok') { console.error('API-Fehler:', overview); process.exit(1); } const totalFiles = overview.totals.files; console.log(`Gefunden: ${totalFiles.toLocaleString('de-DE')} Videos\n`); const allFiles = []; let cursor = null; let batch = 0; const totalPages = Math.ceil(totalFiles / PER_PAGE); while (true) { batch++; process.stdout.write(`\rLade Videos... ${progressBar(Math.min(batch, totalPages), totalPages)} `); try { const url = cursor ? `/my_files_files?folder_id=0&per_page=${PER_PAGE}&cursor=${encodeURIComponent(cursor)}` : `/my_files_files?folder_id=0&per_page=${PER_PAGE}`; const data = await apiFetch(url); if (!data.files || data.files.length === 0) break; allFiles.push(...data.files); cursor = data.file_pagination?.next_cursor; if (!cursor) break; } catch (err) { console.warn(`\nFehler bei Batch ${batch}, ueberspringe... (${err.message})`); break; } // kleine Pause um Rate-Limits zu vermeiden if (batch % 10 === 0) await new Promise(r => setTimeout(r, 200)); } process.stdout.write('\n'); return allFiles; } // ── Filter ── function filterCandidates(files) { return files.filter(f => { const created = new Date(f.created); return f.views === 0 && created < THREE_MONTHS_AGO; }).sort((a, b) => new Date(a.created) - new Date(b.created)); // aelteste zuerst } // ── Display ── function showSummary(candidates, totalFiles) { const totalSize = candidates.reduce((sum, f) => sum + (f.size || 0), 0); const oldest = candidates[0]?.created?.split('T')[0] || '-'; const newest = candidates[candidates.length - 1]?.created?.split('T')[0] || '-'; console.log('\n════════════════════════════════════════════════'); console.log(' ERGEBNIS'); console.log('════════════════════════════════════════════════'); console.log(` Gesamt Videos: ${totalFiles.toLocaleString('de-DE')}`); console.log(` Kandidaten (0 Views,`); console.log(` aelter als 3 Mon.): ${candidates.length.toLocaleString('de-DE')}`); console.log(` Speicher freigeben: ${formatSize(totalSize)}`); console.log(` Aeltestes Video: ${oldest}`); console.log(` Neuestes Kandidat: ${newest}`); console.log('════════════════════════════════════════════════\n'); } function showTable(candidates, offset = 0, limit = 50) { const slice = candidates.slice(offset, offset + limit); const header = `${'#'.padStart(6)} ${'Titel'.padEnd(55)} ${'Alter'.padStart(6)} ${'Groesse'.padStart(10)} ${'Views'.padStart(5)}`; console.log(header); console.log('─'.repeat(header.length)); for (let i = 0; i < slice.length; i++) { const f = slice[i]; const idx = (offset + i + 1).toString().padStart(6); const title = (f.title || f.name || '???').substring(0, 55).padEnd(55); const age = formatAge(f.created).padStart(6); const size = formatSize(f.size || 0).padStart(10); const views = String(f.views).padStart(5); console.log(`${idx} ${title} ${age} ${size} ${views}`); } if (offset + limit < candidates.length) { console.log(`\n... und ${(candidates.length - offset - limit).toLocaleString('de-DE')} weitere`); } } // ── Delete ── async function deleteVideos(candidates) { const total = candidates.length; let deleted = 0; let errors = 0; console.log(`\nLoesche ${total.toLocaleString('de-DE')} Videos in ${Math.ceil(total / DELETE_BATCH)} Batches...\n`); for (let i = 0; i < total; i += DELETE_BATCH) { const batch = candidates.slice(i, i + DELETE_BATCH); const ids = batch.map(f => f.id); process.stdout.write(`\rLoeschen... ${progressBar(Math.min(i + DELETE_BATCH, total), total)} `); try { const result = await apiDelete(ids); if (result.status === 'ok') { deleted += batch.length; } else { errors += batch.length; console.warn(`\nBatch-Fehler: ${result.message || result.error}`); } } catch (err) { errors += batch.length; console.warn(`\nNetzwerk-Fehler: ${err.message}`); } // Pause zwischen Batches await new Promise(r => setTimeout(r, 500)); } process.stdout.write('\n'); console.log(`\nFertig! Geloescht: ${deleted.toLocaleString('de-DE')} | Fehler: ${errors}`); } // ── CSV Export ── function exportCSV(candidates, filename = 'byse-candidates.csv') { const header = 'ID,Code,Titel,Views,Erstellt,Groesse_MB,Alter_Tage\n'; const rows = candidates.map(f => { const days = Math.floor((Date.now() - new Date(f.created).getTime()) / 86400000); const sizeMB = ((f.size || 0) / 1048576).toFixed(1); const title = (f.title || '').replace(/"/g, '""'); return `${f.id},"${f.code}","${title}",${f.views},${f.created},${sizeMB},${days}`; }).join('\n'); fs.writeFileSync(filename, header + rows, 'utf8'); console.log(`CSV exportiert: ${filename} (${candidates.length} Eintraege)`); } // ── Main ── async function main() { console.log('╔══════════════════════════════════════╗'); console.log('║ Byse.sx Video Manager ║'); console.log('║ Videos ohne Views aufräumen ║'); console.log('╚══════════════════════════════════════╝'); const allFiles = await scanAllVideos(); const candidates = filterCandidates(allFiles); if (candidates.length === 0) { console.log('\nKeine Videos gefunden die aelter als 3 Monate sind UND 0 Views haben.'); process.exit(0); } showSummary(candidates, allFiles.length); let offset = 0; while (true) { console.log('\nOptionen:'); console.log(' [1] Liste anzeigen (naechste 50)'); console.log(' [2] CSV exportieren'); console.log(' [3] ALLE Kandidaten loeschen'); console.log(' [4] Nur Videos aelter als X Monate loeschen'); console.log(' [5] Beenden'); const choice = await ask('\nAuswahl: '); if (choice === '1') { showTable(candidates, offset, 50); offset += 50; if (offset >= candidates.length) offset = 0; } else if (choice === '2') { exportCSV(candidates); } else if (choice === '3') { const confirm = await ask(`\nSICHER? ${candidates.length.toLocaleString('de-DE')} Videos werden UNWIDERRUFLICH geloescht!\nTippe "LOESCHEN" zum Bestaetigen: `); if (confirm === 'LOESCHEN') { await deleteVideos(candidates); break; } else { console.log('Abgebrochen.'); } } else if (choice === '4') { const months = await ask('Mindest-Alter in Monaten (z.B. 6): '); const m = parseInt(months); if (isNaN(m) || m < 1) { console.log('Ungueltige Eingabe.'); continue; } const cutoff = new Date(Date.now() - m * 30 * 24 * 60 * 60 * 1000); const filtered = candidates.filter(f => new Date(f.created) < cutoff); console.log(`\n${filtered.length.toLocaleString('de-DE')} Videos aelter als ${m} Monate mit 0 Views.`); const totalSize = filtered.reduce((sum, f) => sum + (f.size || 0), 0); console.log(`Speicher: ${formatSize(totalSize)}`); if (filtered.length === 0) continue; showTable(filtered, 0, 30); const confirm = await ask(`\nTippe "LOESCHEN" um diese ${filtered.length.toLocaleString('de-DE')} Videos zu loeschen: `); if (confirm === 'LOESCHEN') { await deleteVideos(filtered); break; } else { console.log('Abgebrochen.'); } } else if (choice === '5') { console.log('Bye!'); break; } } } main().catch(err => { console.error('Fehler:', err.message); process.exit(1); });